Skip to content

Commit e8cc588

Browse files
authored
Use pill-style transaction filters in Stars and Gram Earnings (#153)
* feat(StarsUI): use pill-style transaction filters for Stars and Gram earnings * fix(StarsUI): preserve filter accessibility and visible selection * fix(StarsUI): keep filter row geometry stable across glass changes * fix(StarsUI): preserve selection springs and isolate regression runner
1 parent 6293afd commit e8cc588

5 files changed

Lines changed: 544 additions & 2 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import Cocoa
2+
import TGUIKit
3+
import TelegramTheme
4+
import GeneralRows
5+
6+
final class StarTransactionTabsRowItem: GeneralRowItem {
7+
let items: [ScrollableSegmentItem]
8+
let callback: (Int64) -> Void
9+
10+
static let contentInset: CGFloat = 16
11+
12+
init(_ initialSize: NSSize, stableId: AnyHashable, items: [ScrollableSegmentItem], viewType: GeneralViewType, callback: @escaping (Int64) -> Void) {
13+
self.items = items
14+
self.callback = callback
15+
super.init(initialSize, height: GlassSegmentView.height + Self.contentInset * 2, stableId: stableId, viewType: viewType)
16+
}
17+
18+
override var hasBorder: Bool {
19+
return false
20+
}
21+
22+
override func viewClass() -> AnyClass {
23+
return StarTransactionTabsRowView.self
24+
}
25+
26+
override var accessibilityLabel: String? {
27+
return items.first(where: { $0.selected })?.title ?? items.first?.title
28+
}
29+
30+
override var accessibilityRole: NSAccessibility.Role? {
31+
return .radioGroup
32+
}
33+
}
34+
35+
private final class StarTransactionTabAccessibilityElement: NSAccessibilityElement {
36+
weak var view: NSView?
37+
var frameInView: NSRect = .zero
38+
var press: (() -> Bool)?
39+
var reveal: (() -> Void)?
40+
41+
override func accessibilityFrame() -> NSRect {
42+
guard let view else { return .zero }
43+
return NSAccessibility.screenRect(fromView: view, rect: frameInView)
44+
}
45+
46+
override func accessibilityPerformPress() -> Bool {
47+
return press?() ?? false
48+
}
49+
50+
override func setAccessibilityFocused(_ accessibilityFocused: Bool) {
51+
if accessibilityFocused {
52+
reveal?()
53+
}
54+
super.setAccessibilityFocused(accessibilityFocused)
55+
}
56+
}
57+
58+
private final class StarTransactionTabsRowView: GeneralContainableRowView {
59+
private let scrollView = HorizontalScrollView(frame: .zero)
60+
private let documentView = View()
61+
private let segments = GlassSegmentView(frame: .zero)
62+
private var accessibilityTabs: [StarTransactionTabAccessibilityElement] = []
63+
private var focusSelection = false
64+
private var animateSelection = false
65+
66+
required init(frame frameRect: NSRect) {
67+
super.init(frame: frameRect)
68+
addSubview(scrollView)
69+
scrollView.documentView = documentView
70+
scrollView.layer?.backgroundColor = NSColor.clear.cgColor
71+
scrollView.contentView.drawsBackground = false
72+
documentView.addSubview(segments)
73+
segments.didSelect = { [weak self] index in
74+
guard let item = self?.item as? StarTransactionTabsRowItem, item.items.indices.contains(index) else {
75+
return
76+
}
77+
item.callback(item.items[index].uniqueId)
78+
}
79+
}
80+
81+
required init?(coder: NSCoder) {
82+
fatalError("init(coder:) has not been implemented")
83+
}
84+
85+
override func accessibilityChildren() -> [Any]? {
86+
return TGUIKitAccessibility.isEnabled ? accessibilityTabs : nil
87+
}
88+
89+
override func set(item: TableRowItem, animated: Bool = false) {
90+
super.set(item: item, animated: animated)
91+
guard let item = item as? StarTransactionTabsRowItem else { return }
92+
let selected = item.items.firstIndex(where: { $0.selected })
93+
focusSelection = focusSelection || segments.selectedIndex != selected || segments.titles != item.items.map { $0.title }
94+
animateSelection = animated && !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
95+
segments.updateLocalizationAndTheme(theme: theme)
96+
segments.set(titles: item.items.map { $0.title }, selected: selected, animated: animateSelection)
97+
let previous = accessibilityTabs
98+
let frames = segments.itemFrames
99+
accessibilityTabs = item.items.enumerated().map { index, tab in
100+
let identifier = String(tab.uniqueId)
101+
let element = previous.first(where: { $0.accessibilityIdentifier() == identifier }) ?? StarTransactionTabAccessibilityElement()
102+
element.view = segments
103+
element.frameInView = frames[index]
104+
element.setAccessibilityElement(true)
105+
element.setAccessibilityParent(self)
106+
element.setAccessibilityIdentifier(identifier)
107+
element.setAccessibilityRole(.radioButton)
108+
element.setAccessibilityLabel(tab.accessibilityLabel?() ?? tab.title)
109+
element.setAccessibilityValue(NSNumber(value: tab.selected))
110+
element.setAccessibilitySelected(tab.selected)
111+
element.reveal = { [weak self] in
112+
self?.revealTab(tab.uniqueId)
113+
}
114+
element.press = { [weak self] in
115+
guard let self, let item = self.item as? StarTransactionTabsRowItem, item.items.contains(where: { $0.uniqueId == tab.uniqueId }) else {
116+
return false
117+
}
118+
self.revealTab(tab.uniqueId)
119+
item.callback(tab.uniqueId)
120+
return true
121+
}
122+
return element
123+
}
124+
needsLayout = true
125+
}
126+
127+
private func revealTab(_ id: Int64) {
128+
guard let item = item as? StarTransactionTabsRowItem, let index = item.items.firstIndex(where: { $0.uniqueId == id }) else {
129+
return
130+
}
131+
layoutSubtreeIfNeeded()
132+
scrollToSegment(at: index, animated: false)
133+
}
134+
135+
private func scrollToSegment(at index: Int, animated: Bool) {
136+
guard segments.itemFrames.indices.contains(index) else { return }
137+
let frame = segments.convert(segments.itemFrames[index], to: documentView)
138+
let maxOffset = max(0, documentView.frame.width - scrollView.clipView.bounds.width)
139+
let offset = min(max(0, frame.midX - scrollView.clipView.bounds.width / 2), maxOffset)
140+
let point = NSPoint(x: offset, y: 0)
141+
if animated && !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion {
142+
if point != scrollView.documentOffset {
143+
scrollView.clipView.scroll(to: point, animated: true)
144+
}
145+
} else {
146+
scrollView.clipView.updateBounds(to: point, cancel: true)
147+
scrollView.clipView.layer?.removeAnimation(forKey: "bounds")
148+
}
149+
scrollView.reflectScrolledClipView(scrollView.clipView)
150+
}
151+
152+
override func updateColors() {
153+
super.updateColors()
154+
segments.updateLocalizationAndTheme(theme: theme)
155+
scrollView.layer?.backgroundColor = NSColor.clear.cgColor
156+
needsLayout = true
157+
}
158+
159+
override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) {
160+
super.updateLayout(size: size, transition: transition)
161+
let contentSize = segments.contentSize
162+
let inset = StarTransactionTabsRowItem.contentInset
163+
let documentSize = NSMakeSize(contentSize.width + inset * 2, contentSize.height + inset * 2)
164+
let viewportWidth = min(max(0, containerView.bounds.width), documentSize.width)
165+
let frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (containerView.bounds.width - viewportWidth) / 2), 0, viewportWidth, documentSize.height)
166+
let resized = scrollView.frame.size != frame.size || documentView.frame.size != documentSize
167+
transition.updateFrame(view: scrollView, frame: frame)
168+
documentView.setFrameSize(documentSize)
169+
segments.frame = NSRect(origin: NSPoint(x: inset, y: inset), size: contentSize)
170+
if (focusSelection || resized), let selected = segments.selectedIndex {
171+
scrollToSegment(at: selected, animated: focusSelection && animateSelection && !resized)
172+
} else if resized {
173+
let maxOffset = max(0, documentSize.width - viewportWidth)
174+
let offset = min(max(0, scrollView.clipView.bounds.minX), maxOffset)
175+
scrollView.clipView.updateBounds(to: NSPoint(x: offset, y: 0), cancel: true)
176+
scrollView.clipView.layer?.removeAnimation(forKey: "bounds")
177+
scrollView.reflectScrolledClipView(scrollView.clipView)
178+
}
179+
focusSelection = false
180+
animateSelection = false
181+
}
182+
}

‎packages/StarsUI/Sources/StarsUI/Star_ListScreen.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
12051205
index += 1
12061206

12071207
entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_transaction_mode, equatable: .init(state.transactionMode), comparable: nil, item: { initialSize, stableId in
1208-
return SegmentTypesRowItem(initialSize, stableId: stableId, items: transactionSegments(state.transactionMode), viewType: .firstItem, callback: { uniqueId in
1208+
return StarTransactionTabsRowItem(initialSize, stableId: stableId, items: transactionSegments(state.transactionMode), viewType: .firstItem, callback: { uniqueId in
12091209
switch uniqueId {
12101210
case 0:
12111211
arguments.toggleFilterMode(.all)

‎packages/TGUIKit/Sources/GlassSegmentView.swift‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ public final class GlassSegmentView: View {
5050
}
5151
self.selectedIndex = index
5252
self.selectionPosition = index.flatMap { CGFloat($0) }
53+
let reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
54+
let animated = animated && !reduceMotion
55+
if reduceMotion {
56+
selectionView.layer?.removeAllAnimations()
57+
}
5358
updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
5459
}
5560

5661
public func setSelection(position: CGFloat, animated: Bool) {
62+
let animated = animated && !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
5763
if !animated {
5864
selectionView.layer?.removeAllAnimations()
5965
}
@@ -78,7 +84,7 @@ public final class GlassSegmentView: View {
7884
}
7985
}
8086

81-
private var itemFrames: [NSRect] {
87+
public var itemFrames: [NSRect] {
8288
var x: CGFloat = 0
8389
return labels.map { label in
8490
let frame = NSMakeRect(x, 0, label.frame.width + GlassSegmentView.itemInset * 2, GlassSegmentView.height)

0 commit comments

Comments
 (0)