Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

251 lines
11 KiB
Swift

import Foundation
import UIKit
import Display
import ContextUI
import TelegramPresentationData
import RichTextEditorCore
import RichTextEditorUIKit
import TelegramCore
import AsyncDisplayKit
import ComponentFlow
import PlainButtonComponent
import BundleIconComponent
import MultilineTextComponent
final class TableStructuralMenuAlignmentItem: ContextMenuCustomItem {
let initialHorizontal: TableHorizontalAlignment?
let initialVertical: TableVerticalAlignment?
let action: (TableHorizontalAlignment?, TableVerticalAlignment?) -> Void
init(initialHorizontal: TableHorizontalAlignment?, initialVertical: TableVerticalAlignment?,
action: @escaping (TableHorizontalAlignment?, TableVerticalAlignment?) -> Void) {
self.initialHorizontal = initialHorizontal
self.initialVertical = initialVertical
self.action = action
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return TableStructuralMenuAlignmentItemNode(presentationData: presentationData, getController: getController, actionSelected: actionSelected, item: self)
}
}
/// Renders the per-cell alignment control as two rows of three segmented buttons (H: Left/Center/Right,
/// V: Top/Middle/Bottom), modeled on `BrowserFontSizeContextMenuItemNode` (background + touch-highlight overlay
/// + separators per button, `updateTheme` re-applying colors, `updateLayout` returning a fixed-height control).
/// The current selection (seeded from `item.initialHorizontal`/`initialVertical`) is shown via accent color +
/// semibold weight; `nil` (mixed across the selected cells) shows no highlighted segment in that row. A tap on a
/// horizontal button reports only the horizontal axis (`item.action(h, nil)`); a vertical tap reports only the
/// vertical axis (`item.action(nil, v)`) — the other axis is left unchanged by the caller's `apply`.
private final class TableStructuralMenuAlignmentItemNode: ASDisplayNode, ContextMenuCustomNode {
private var presentationData: PresentationData
let item: TableStructuralMenuAlignmentItem
let needsPadding: Bool = false
private let title = ComponentView<Empty>()
private var currentHorizontal: TableHorizontalAlignment?
private var currentVertical: TableVerticalAlignment?
private let horizontalItems: [ComponentView<Empty>]
private let verticalItems: [ComponentView<Empty>]
private let horizontalSelection = UIImageView()
private let verticalSelection = UIImageView()
private var validLayout: (constrainedWidth: CGFloat, constrainedHeight: CGFloat, resultSize: CGSize)?
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void, item: TableStructuralMenuAlignmentItem) {
self.presentationData = presentationData
self.item = item
self.currentHorizontal = item.initialHorizontal
self.currentVertical = item.initialVertical
self.horizontalItems = (0 ..< 3).map { _ in ComponentView() }
self.verticalItems = (0 ..< 3).map { _ in ComponentView() }
super.init()
self.isUserInteractionEnabled = true
self.view.addSubview(self.horizontalSelection)
self.view.addSubview(self.verticalSelection)
}
override func didLoad() {
super.didLoad()
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
if let validLayout = self.validLayout {
let (_, apply) = self.updateLayout(constrainedWidth: validLayout.constrainedWidth, constrainedHeight: validLayout.constrainedHeight)
apply(validLayout.resultSize, .immediate)
}
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let size = CGSize(width: constrainedWidth, height: 68.0)
return (size, { [weak self] size, transition in
guard let self else {
return
}
self.validLayout = (constrainedWidth, constrainedHeight, size)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: self.presentationData.strings.RichText_TableMenu_Alignment, font: Font.regular(13.0), textColor: self.presentationData.theme.contextMenu.secondaryColor))
)),
environment: {},
containerSize: CGSize(width: 1000.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) * 0.5), y: 11.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.view.addSubview(titleView)
}
titleView.frame = titleFrame
}
let spacing: CGFloat = 2.0
let itemSize = CGSize(width: 34.0, height: 34.0)
enum ItemKind {
case left
case right
case hcenter
case top
case bottom
case vcenter
var icon: String {
switch self {
case .left:
return "Chat/Context Menu/AlignLeft"
case .right:
return "Chat/Context Menu/AlignRight"
case .hcenter:
return "Chat/Context Menu/AlignHCenter"
case .top:
return "Chat/Context Menu/AlignTop"
case .bottom:
return "Chat/Context Menu/AlignBottom"
case .vcenter:
return "Chat/Context Menu/AlignVCenter"
}
}
}
let hItems: [ItemKind] = [
.left, .hcenter, .right
]
let vItems: [ItemKind] = [
.top, .vcenter, .bottom
]
for itemSet in 0 ..< 2 {
let items = itemSet == 0 ? self.horizontalItems : self.verticalItems
let itemKinds = itemSet == 0 ? hItems : vItems
let baseX: CGFloat = itemSet == 0 ? 10.0 : (size.width - 10.0 - CGFloat(items.count) * itemSize.width - CGFloat(items.count - 1) * spacing)
let selectionView = itemSet == 0 ? self.horizontalSelection : self.verticalSelection
var selectionFrame: CGRect?
for i in 0 ..< items.count {
let item = items[i]
let itemKind = itemKinds[i]
let isSelected: Bool
switch itemKind {
case .left:
isSelected = self.currentHorizontal == .left
case .right:
isSelected = self.currentHorizontal == .right
case .hcenter:
isSelected = self.currentHorizontal == .center
case .top:
isSelected = self.currentVertical == .top
case .bottom:
isSelected = self.currentVertical == .bottom
case .vcenter:
isSelected = self.currentVertical == .middle
}
let _ = item.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: itemKind.icon,
tintColor: self.presentationData.theme.contextMenu.primaryColor,
)),
minSize: itemSize,
action: { [weak self] in
guard let self else {
return
}
switch itemKind {
case .left:
self.currentHorizontal = .left
self.item.action(.left, nil)
case .right:
self.currentHorizontal = .right
self.item.action(.right, nil)
case .hcenter:
self.currentHorizontal = .center
self.item.action(.center, nil)
case .top:
self.currentVertical = .top
self.item.action(nil, .top)
case .bottom:
self.currentVertical = .bottom
self.item.action(nil, .bottom)
case .vcenter:
self.currentVertical = .middle
self.item.action(nil, .middle)
}
if let validLayout = self.validLayout {
let (_, apply) = self.updateLayout(constrainedWidth: validLayout.constrainedWidth, constrainedHeight: validLayout.constrainedHeight)
apply(validLayout.resultSize, .animated(duration: 0.4, curve: .spring))
}
}
)),
environment: {},
containerSize: itemSize
)
let itemFrame = CGRect(origin: CGPoint(x: baseX + CGFloat(i) * (itemSize.width + spacing), y: 34.0), size: itemSize)
if let itemView = item.view {
if itemView.superview == nil {
self.view.addSubview(itemView)
}
itemView.frame = itemFrame
}
if selectionFrame == nil || isSelected {
selectionFrame = itemFrame
}
}
if let selectionFrame {
transition.updateFrame(view: selectionView, frame: selectionFrame)
if selectionView.image == nil {
selectionView.image = generateStretchableFilledCircleImage(radius: 8.0, color: .white)?.withRenderingMode(.alwaysTemplate)
}
selectionView.tintColor = self.presentationData.theme.contextMenu.itemHighlightedBackgroundColor
}
}
})
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func performAction() {
}
}