Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GlassControls",
module_name = "GlassControls",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,236 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import PlainButtonComponent
import BundleIconComponent
import MultilineTextComponent
public final class GlassControlGroupComponent: Component {
public final class Item: Equatable {
public enum Content: Hashable {
case icon(String)
case text(String)
}
public let id: AnyHashable
public let content: Content
public let action: (() -> Void)?
public init(id: AnyHashable, content: Content, action: (() -> Void)?) {
self.id = id
self.content = content
self.action = action
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.content != rhs.content {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
return true
}
}
public enum Background {
case panel
case activeTint
}
public let theme: PresentationTheme
public let background: Background
public let items: [Item]
public let minWidth: CGFloat
public init(
theme: PresentationTheme,
background: Background,
items: [Item],
minWidth: CGFloat
) {
self.theme = theme
self.background = background
self.items = items
self.minWidth = minWidth
}
public static func ==(lhs: GlassControlGroupComponent, rhs: GlassControlGroupComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.minWidth != rhs.minWidth {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: GlassBackgroundView
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var component: GlassControlGroupComponent?
private weak var state: EmptyComponentState?
override public init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func itemView(id: AnyHashable) -> UIView? {
return self.itemViews[id]?.view
}
func update(component: GlassControlGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
self.component = component
self.state = state
struct ItemId: Hashable {
var id: AnyHashable
var contentId: AnyHashable
init(id: AnyHashable, contentId: AnyHashable) {
self.id = id
self.contentId = contentId
}
}
var contentsWidth: CGFloat = 0.0
var validIds: [AnyHashable] = []
var isInteractive = false
for item in component.items {
let itemId = ItemId(id: item.id, contentId: item.content)
validIds.append(itemId)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemView = ComponentView()
self.itemViews[itemId] = itemView
itemTransition = itemTransition.withAnimation(.none)
}
if item.action != nil {
isInteractive = true
}
let content: AnyComponent<Empty>
var itemInsets = UIEdgeInsets()
switch item.content {
case let .icon(name):
content = AnyComponent(BundleIconComponent(
name: name,
tintColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor
))
case let .text(string):
content = AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: string, font: Font.semibold(15.0), textColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor))
))
itemInsets.left = 10.0
itemInsets.right = itemInsets.left
}
var minItemWidth: CGFloat = 40.0
if component.items.count == 1 {
minItemWidth = max(minItemWidth, component.minWidth)
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(PlainButtonComponent(
content: content,
minSize: CGSize(width: minItemWidth, height: 40.0),
contentInsets: itemInsets,
action: {
item.action?()
},
isEnabled: item.action != nil,
animateAlpha: false,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let itemFrame = CGRect(origin: CGPoint(x: contentsWidth, y: 0.0), size: itemSize)
if let itemComponentView = itemView.view {
var animateIn = false
if itemComponentView.superview == nil {
animateIn = true
self.backgroundView.contentView.addSubview(itemComponentView)
itemComponentView.alpha = 0.0
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
if animateIn {
alphaTransition.setAlpha(view: itemComponentView, alpha: 1.0)
alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 8.0, toRadius: 0.0)
}
}
contentsWidth += itemSize.width
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
alphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 0.0, toRadius: 8.0, removeOnCompletion: false)
}
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
let size = CGSize(width: contentsWidth, height: availableSize.height)
let tintColor: GlassBackgroundView.TintColor
switch component.background {
case .panel:
tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
case .activeTint:
tintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: component.theme.list.itemCheckColors.fillColor)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,280 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
public final class GlassControlPanelComponent: Component {
public final class Item: Equatable {
public let items: [GlassControlGroupComponent.Item]
public let background: GlassControlGroupComponent.Background
public init(items: [GlassControlGroupComponent.Item], background: GlassControlGroupComponent.Background) {
self.items = items
self.background = background
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.background != rhs.background {
return false
}
return true
}
}
public let theme: PresentationTheme
public let leftItem: Item?
public let rightItem: Item?
public let centralItem: Item?
public init(
theme: PresentationTheme,
leftItem: Item?,
centralItem: Item?,
rightItem: Item?
) {
self.theme = theme
self.leftItem = leftItem
self.centralItem = centralItem
self.rightItem = rightItem
}
public static func ==(lhs: GlassControlPanelComponent, rhs: GlassControlPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.leftItem != rhs.leftItem {
return false
}
if lhs.centralItem != rhs.centralItem {
return false
}
if lhs.rightItem != rhs.rightItem {
return false
}
return true
}
public final class View: UIView {
private let glassContainerView: GlassBackgroundContainerView
private var leftItemComponent: ComponentView<Empty>?
private var centralItemComponent: ComponentView<Empty>?
private var rightItemComponent: ComponentView<Empty>?
private var component: GlassControlPanelComponent?
private weak var state: EmptyComponentState?
public var leftItemView: GlassControlGroupComponent.View? {
return self.leftItemComponent?.view as? GlassControlGroupComponent.View
}
public var centerItemView: GlassControlGroupComponent.View? {
return self.centralItemComponent?.view as? GlassControlGroupComponent.View
}
public var rightItemView: GlassControlGroupComponent.View? {
return self.rightItemComponent?.view as? GlassControlGroupComponent.View
}
override public init(frame: CGRect) {
self.glassContainerView = GlassBackgroundContainerView()
super.init(frame: frame)
self.addSubview(self.glassContainerView)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GlassControlPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let minSpacing: CGFloat = 8.0
var leftItemFrame: CGRect?
if let leftItem = component.leftItem {
let leftItemComponent: ComponentView<Empty>
var leftItemTransition = transition
if let current = self.leftItemComponent {
leftItemComponent = current
} else {
leftItemComponent = ComponentView()
self.leftItemComponent = leftItemComponent
leftItemTransition = transition.withAnimation(.none)
}
let leftItemSize = leftItemComponent.update(
transition: leftItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: leftItem.background,
items: leftItem.items,
minWidth: 40.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let leftItemFrameValue = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: leftItemSize)
leftItemFrame = leftItemFrameValue
if let leftItemComponentView = leftItemComponent.view {
var animateIn = false
if leftItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(leftItemComponentView)
ComponentTransition.immediate.setScale(view: leftItemComponentView, scale: 0.001)
}
leftItemTransition.setPosition(view: leftItemComponentView, position: leftItemFrameValue.center)
leftItemTransition.setBounds(view: leftItemComponentView, bounds: CGRect(origin: CGPoint(), size: leftItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: leftItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: leftItemComponentView, scale: 1.0)
}
}
} else if let leftItemComponent = self.leftItemComponent {
self.leftItemComponent = nil
if let leftItemComponentView = leftItemComponent.view {
transition.setScale(view: leftItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: leftItemComponentView, alpha: 0.0, completion: { [weak leftItemComponentView] _ in
leftItemComponentView?.removeFromSuperview()
})
}
}
var rightItemFrame: CGRect?
if let rightItem = component.rightItem {
let rightItemComponent: ComponentView<Empty>
var rightItemTransition = transition
if let current = self.rightItemComponent {
rightItemComponent = current
} else {
rightItemComponent = ComponentView()
self.rightItemComponent = rightItemComponent
rightItemTransition = transition.withAnimation(.none)
}
let rightItemSize = rightItemComponent.update(
transition: rightItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: rightItem.background,
items: rightItem.items,
minWidth: 40.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
let rightItemFrameValue = CGRect(origin: CGPoint(x: availableSize.width - rightItemSize.width, y: 0.0), size: rightItemSize)
rightItemFrame = rightItemFrameValue
if let rightItemComponentView = rightItemComponent.view {
var animateIn = false
if rightItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(rightItemComponentView)
ComponentTransition.immediate.setScale(view: rightItemComponentView, scale: 0.001)
}
rightItemTransition.setPosition(view: rightItemComponentView, position: rightItemFrameValue.center)
rightItemTransition.setBounds(view: rightItemComponentView, bounds: CGRect(origin: CGPoint(), size: rightItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: rightItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: rightItemComponentView, scale: 1.0)
}
}
} else if let rightItemComponent = self.rightItemComponent {
self.rightItemComponent = nil
if let rightItemComponentView = rightItemComponent.view {
transition.setScale(view: rightItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: rightItemComponentView, alpha: 0.0, completion: { [weak rightItemComponentView] _ in
rightItemComponentView?.removeFromSuperview()
})
}
}
if let centralItem = component.centralItem {
let centralItemComponent: ComponentView<Empty>
var centralItemTransition = transition
if let current = self.centralItemComponent {
centralItemComponent = current
} else {
centralItemComponent = ComponentView()
self.centralItemComponent = centralItemComponent
centralItemTransition = transition.withAnimation(.none)
}
var maxCentralItemSize = CGSize(width: availableSize.width, height: availableSize.height)
var centralRightInset: CGFloat = 0.0
if let rightItemFrame {
centralRightInset = availableSize.width - rightItemFrame.minX + minSpacing
}
var centralLeftInset: CGFloat = 0.0
if let leftItemFrame {
centralLeftInset = leftItemFrame.maxX + minSpacing
}
if centralRightInset <= 48.0 && centralLeftInset <= 48.0 {
let maxInset = max(centralRightInset, centralLeftInset)
centralLeftInset = maxInset
centralRightInset = maxInset
}
maxCentralItemSize.width = max(1.0, availableSize.width - centralLeftInset - centralRightInset)
let centralItemSize = centralItemComponent.update(
transition: centralItemTransition,
component: AnyComponent(GlassControlGroupComponent(
theme: component.theme,
background: centralItem.background,
items: centralItem.items,
minWidth: 165.0
)),
environment: {},
containerSize: maxCentralItemSize
)
let centralItemFrameValue = CGRect(origin: CGPoint(x: centralLeftInset + floor((availableSize.width - centralLeftInset - centralRightInset - centralItemSize.width) * 0.5), y: 0.0), size: centralItemSize)
if let centralItemComponentView = centralItemComponent.view {
var animateIn = false
if centralItemComponentView.superview == nil {
animateIn = true
self.glassContainerView.contentView.addSubview(centralItemComponentView)
ComponentTransition.immediate.setScale(view: centralItemComponentView, scale: 0.001)
}
centralItemTransition.setPosition(view: centralItemComponentView, position: centralItemFrameValue.center)
centralItemTransition.setBounds(view: centralItemComponentView, bounds: CGRect(origin: CGPoint(), size: centralItemFrameValue.size))
if animateIn {
alphaTransition.animateAlpha(view: centralItemComponentView, from: 0.0, to: 1.0)
transition.setScale(view: centralItemComponentView, scale: 1.0)
}
}
} else if let centralItemComponent = self.centralItemComponent {
self.centralItemComponent = nil
if let centralItemComponentView = centralItemComponent.view {
transition.setScale(view: centralItemComponentView, scale: 0.001)
alphaTransition.setAlpha(view: centralItemComponentView, alpha: 0.0, completion: { [weak centralItemComponentView] _ in
centralItemComponentView?.removeFromSuperview()
})
}
}
transition.setFrame(view: self.glassContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.glassContainerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}