Files
TelegramSwift/packages/TGUIKit/Sources/SectionViewController.swift
Mike Renoir 135589f0bf new textview
2023-10-11 08:58:35 +04:00

395 lines
18 KiB
Swift

//
// SectionViewController.swift
// TGUIKit
//
// Created by keepcoder on 03/08/2017.
// Copyright © 2017 Telegram. All rights reserved.
//
import Cocoa
import SwiftSignalKit
import AppKit
private final class SectionControllerArguments {
let select:(Int)->Void
init(select:@escaping(Int)->Void) {
self.select = select
}
}
public class SectionControllerView : View {
fileprivate let header: View = View()
fileprivate let selector:View = View()
fileprivate let container: View = View()
private weak var current: ViewController?
public var selectorIndex:Int = 0 {
didSet {
if selectorIndex != oldValue {
var index:Int = 0
for hContainer in header.subviews {
for t in hContainer.subviews {
if let t = t as? TextView {
t.update(TextViewLayout(.initialize(string: t.textLayout?.attributedString.string, color: selectorIndex == index ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle))
}
}
index += 1
}
}
}
}
public required init(frame frameRect: NSRect) {
super.init(frame: frameRect)
addSubview(header)
addSubview(container)
addSubview(selector)
updateLocalizationAndTheme(theme: presentation)
needsLayout = true
}
fileprivate func layout(sections: [SectionControllerItem], selected: Int, hasHeaderView: Bool, arguments: SectionControllerArguments) {
header.removeAllSubviews()
header.isHidden = !hasHeaderView
self.selectorIndex = selected
for i in 0 ..< sections.count {
let section = sections[i]
let headerContainer = Control(frame: NSMakeRect(0, 0, 0, 50))
let title: TextView = TextView()
title.isSelectable = false
title.userInteractionEnabled = false
title.backgroundColor = presentation.colors.background
title.update(TextViewLayout(.initialize(string: section.title(), color: i == selected ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle))
headerContainer.addSubview(title)
header.addSubview(headerContainer)
headerContainer.border = [.Bottom]
headerContainer.set(handler: { _ in
arguments.select(i)
}, for: .Click)
}
needsLayout = true
}
fileprivate func select(controller: ViewController, index: Int, animated: Bool, notifyApper: Bool = true) {
let previousIndex = self.selectorIndex
let previous = self.current
self.current = controller
selectorIndex = index
controller.view.frame = container.bounds
if notifyApper {
previous?.viewWillDisappear(animated)
}
let duration: Double = 0.2
container.addSubview(controller.view)
if animated {
CATransaction.begin()
let container = header.subviews[index]
selector.change(pos: NSMakePoint(container.frame.minX, selector.frame.minY), animated: animated, duration: duration, timingFunction: .spring)
let pto: NSPoint
let nfrom: NSPoint
if previousIndex < index {
pto = NSMakePoint(-container.frame.width, 0)
nfrom = NSMakePoint(container.frame.width, 0)
} else {
pto = NSMakePoint(container.frame.width, 0)
nfrom = NSMakePoint(-container.frame.width, 0)
}
previous?.view._change(pos: pto, animated: animated, duration: duration, timingFunction: .spring, completion: { [weak previous, weak controller] complete in
if complete {
previous?.view.removeFromSuperview()
previous?.viewDidDisappear(animated)
controller?.viewDidAppear(animated)
}
})
controller.view.layer?.animatePosition(from: nfrom, to: NSZeroPoint, duration: duration, timingFunction: .spring, removeOnCompletion: true)
CATransaction.commit()
} else {
container.removeAllSubviews()
previous?.viewDidDisappear(animated)
container.addSubview(controller.view)
controller.viewDidAppear(animated)
}
needsLayout = true
}
public override func updateLocalizationAndTheme(theme: PresentationTheme) {
super.updateLocalizationAndTheme(theme: theme)
self.backgroundColor = presentation.colors.background
selector.backgroundColor = presentation.colors.accent
container.backgroundColor = presentation.colors.background
var index:Int = 0
for hContainer in header.subviews {
hContainer.background = presentation.colors.background
for t in hContainer.subviews {
if let t = t as? TextView {
let layout = TextViewLayout(.initialize(string: t.textLayout?.attributedString.string, color: selectorIndex == index ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle)
layout.measure(width: hContainer.frame.width - 20)
t.update(layout)
t.center()
}
t.background = presentation.colors.background
}
index += 1
}
}
public override func layout() {
super.layout()
header.setFrameSize(NSMakeSize(frame.width, header.isHidden ? 0 : 50.0))
let width = floorToScreenPixels(backingScaleFactor, frame.width / CGFloat(max(header.subviews.count, 3)))
selector.frame = NSMakeRect(CGFloat(selectorIndex) * width, header.frame.height - .borderSize, width, .borderSize)
container.frame = NSMakeRect(0, header.frame.maxY, frame.width, frame.height - header.frame.height)
container.subviews.first?.frame = container.bounds
var x:CGFloat = 0
for i in 0 ..< header.subviews.count {
let hContainer = header.subviews[i]
let width = i == header.subviews.count - 1 ? frame.width - x : width
hContainer.frame = NSMakeRect(x, 0, width, hContainer.frame.height)
if let textView = hContainer.subviews.first as? TextView {
textView.textLayout?.measure(width: width - 10)
textView.update(textView.textLayout)
textView.center()
}
x += width
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class SectionControllerItem {
let title: ()->String
public let controller: ViewController
public init(title: @escaping()->String, controller: ViewController) {
self.title = title
self.controller = controller
}
}
open class SectionViewController: GenericViewController<SectionControllerView> {
private var sections:[SectionControllerItem] = []
public var selectedSection:SectionControllerItem
public private(set) var selectedIndex: Int = -1
private let disposable = MetaDisposable()
public var selectionUpdateHandler:((Int)->Void)?
public func addSection(_ section: SectionControllerItem) {
sections.append(section)
}
public override func updateLocalizationAndTheme(theme: PresentationTheme) {
let arguments = SectionControllerArguments { [weak self] index in
self?.select(index, true)
}
genericView.layout(sections: sections, selected: selectedIndex, hasHeaderView: self.hasHeaderView, arguments: arguments)
genericView.updateLocalizationAndTheme(theme: theme)
}
deinit {
disposable.dispose()
}
public func select(_ index:Int, _ animated: Bool, notifyApper: Bool = true) {
if selectedIndex != index || !animated {
selectedSection = sections[index]
let controller = sections[index].controller
controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height + 50)
controller.loadViewIfNeeded()
selectedIndex = index
selectionUpdateHandler?(index)
if notifyApper {
controller.viewWillAppear(animated)
}
disposable.set((sections[index].controller.ready.get() |> filter {$0} |> take(1)).start(next: { [weak self, weak controller] ready in
if let strongSelf = self, let controller = controller {
strongSelf.genericView.select(controller: controller, index: index, animated: animated, notifyApper: notifyApper)
}
}))
}
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
selectedSection.controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50)
selectedSection.controller.viewWillAppear(animated)
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
selectedSection.controller.viewWillDisappear(animated)
window?.remove(object: self, for: .Tab)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
selectedSection.controller.viewDidAppear(animated)
window?.set(handler: { [weak self] _ -> KeyHandlerResult in
guard let `self` = self else {return .rejected}
if !self.sections.isEmpty {
var index:Int = self.selectedIndex
if index == self.sections.count - 1 {
index = 0
} else {
index += 1
}
self.select(index, true)
}
return .invoked
}, with: self, for: .Tab, priority: .high)
window?.add(swipe: { [weak self] direction, _ -> SwipeHandlerResult in
guard let `self` = self, !self.sections.isEmpty else {return .nothing}
if !self.selectedSection.controller.supportSwipes {
return .nothing
}
switch direction {
case let .left(state):
switch state {
case .start:
if self.selectedIndex > 0 {
let new = self.sections[self.selectedIndex - 1].controller
new._frameRect = self.genericView.container.bounds
new.view.frame = self.genericView.container.bounds
new.viewWillAppear(false)
self.genericView.container.addSubview(new.view, positioned: .below, relativeTo: self.selectedSection.controller.view)
return .success(new)
}
case let .swiping(delta, controller):
let delta = min(max(0, delta), controller.frame.width)
// self.genericView.selector.change(pos: NSMakePoint(container.frame.minX, genericView.selector.frame.minY), animated: animated, timingFunction: CAMediaTimingFunctionName.spring)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
self.genericView.selector.setFrameOrigin(NSMakePoint(selectorFrame.minX - selectorFrame.width * (delta / controller.frame.width), self.genericView.selector.frame.minY))
self.selectedSection.controller.frame = NSMakeRect(delta, self.selectedSection.controller.frame.minY, self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.height)
controller.frame = NSMakeRect(delta - controller.frame.width, controller.frame.minY, controller.frame.width, controller.frame.height)
return .deltaUpdated(available: delta)
case let .success(_, controller):
controller.view._change(pos: NSMakePoint(0, controller.frame.minY), animated: true)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex - 1].frame
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: animated)
self.selectedSection.controller.view._change(pos: NSMakePoint(self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak self] completed in
if completed, let index = self?.sections.lastIndex(where: {$0.controller == controller}) {
self?.select(index, false, notifyApper: false)
}
})
case let .failed(_, controller):
controller.view._change(pos: NSMakePoint(-controller.frame.width, controller.frame.minY), animated: true)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: animated)
self.selectedSection.controller.view._change(pos: NSMakePoint(0, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak controller] completed in
if completed {
controller?.removeFromSuperview()
}
})
}
break
case let .right(state):
switch state {
case .start:
if self.selectedIndex < self.sections.count - 1 {
let new = self.sections[self.selectedIndex + 1].controller
new._frameRect = self.genericView.container.bounds
new.view.frame = self.genericView.container.bounds
new.viewWillAppear(false)
self.genericView.container.addSubview(new.view, positioned: .below, relativeTo: self.selectedSection.controller.view)
return .success(new)
}
case let .swiping(delta, controller):
let delta = min(max(0, delta), controller.frame.width)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
self.genericView.selector.setFrameOrigin(NSMakePoint(selectorFrame.minX + selectorFrame.width * ( delta / controller.frame.width), self.genericView.selector.frame.minY))
self.selectedSection.controller.frame = NSMakeRect(-delta, self.selectedSection.controller.frame.minY, self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.height)
controller.frame = NSMakeRect(controller.frame.width - delta, controller.frame.minY, controller.frame.width, controller.frame.height)
return .deltaUpdated(available: delta)
case let .success(_, controller):
controller.view._change(pos: NSMakePoint(0, controller.frame.minY), animated: true)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex + 1].frame
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: true)
self.selectedSection.controller.view._change(pos: NSMakePoint(-self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak self] completed in
if completed, let index = self?.sections.lastIndex(where: {$0.controller == controller}) {
self?.select(index, false, notifyApper: false)
}
})
case let .failed(_, controller):
controller.view._change(pos: NSMakePoint(controller.frame.width, controller.frame.minY), animated: true)
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: true)
self.selectedSection.controller.view._change(pos: NSMakePoint(0, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak controller] completed in
if completed {
controller?.removeFromSuperview()
}
})
}
case .none:
break
}
return .nothing
}, with: self.genericView, identifier: SwipeIdentifier("section-swipe"))
}
override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
selectedSection.controller.viewDidDisappear(animated)
}
open override func viewDidLoad() {
super.viewDidLoad()
let arguments = SectionControllerArguments { [weak self] index in
self?.select(index, true)
}
genericView.layout(sections: sections, selected: selectedIndex, hasHeaderView: self.hasHeaderView, arguments: arguments)
select(selectedIndex, false)
self.ready.set(sections[selectedIndex].controller.ready.get())
}
private let hasHeaderView: Bool
public init(sections: [SectionControllerItem], selected: Int = 0, hasHeaderView: Bool = true, hasBar: Bool = false) {
assert(!sections.isEmpty)
self.sections = sections
self.selectedSection = sections[selected]
self.selectedIndex = selected
self.hasHeaderView = hasHeaderView
super.init()
bar = .init(height: hasBar ? 50 : 0)
}
}