Add an iPhone client sharing the macOS app's SwiftUI code

This commit is contained in:
Ian Gulliver
2026-10-21 22:00:54 -07:00
parent 57d660c57f
commit 9e81e9107d
46 changed files with 1625 additions and 287 deletions
+170 -32
View File
@@ -206,17 +206,28 @@ struct InlineRenderer {
private func run(_ s: String, _ style: Style) -> AttributedString {
var a = AttributedString(s)
a.font = Fonts.font(style.small ? 11 : size, weight: style.bold ? 700 : weight, italic: style.italic, mono: style.mono)
a.mergeAttributes(try! AttributeContainer([.font: Fonts.ns(style.small ? 11 : size, weight: style.bold ? 700 : weight, italic: style.italic, mono: style.mono)], including: \.appKit))
let font = Fonts.native(style.small ? 11 : size, weight: style.bold ? 700 : weight, italic: style.italic, mono: style.mono)
#if os(macOS)
a.mergeAttributes(try! AttributeContainer([.font: font], including: \.appKit))
#else
a.uiKit.font = font
#endif
if let link = style.link {
a.link = link
}
if let color = style.color {
a.foregroundColor = color
#if os(macOS)
a.appKit.foregroundColor = NSColor(color)
#else
a.uiKit.foregroundColor = UIColor(color)
#endif
}
#if os(macOS)
if let tip = style.tip {
a.appKit.toolTip = tip
}
#endif
return a
}
@@ -244,7 +255,11 @@ struct InlineRenderer {
}
var a = run(c, s)
a.backgroundColor = Palette.surface
#if os(macOS)
a.appKit.backgroundColor = NSColor(Palette.surface)
#else
a.uiKit.backgroundColor = UIColor(Palette.surface)
#endif
append(a, &out)
case .strong(let inner):
var s = style
@@ -410,7 +425,15 @@ struct InlineRenderer {
}
}
private func flowColor(_ s: NSMutableAttributedString, _ color: NSColor) {
private func nativeString(_ a: AttributedString) -> NSAttributedString {
#if os(macOS)
try! NSAttributedString(a, including: \.appKit)
#else
try! NSAttributedString(a, including: \.uiKit)
#endif
}
private func flowColor(_ s: NSMutableAttributedString, _ color: PlatformColor) {
let all = NSRange(location: 0, length: s.length)
unsafe s.enumerateAttribute(.foregroundColor, in: all) { value, range, _ in
if value == nil {
@@ -543,7 +566,7 @@ private struct FlowBuilder {
gap += 4
para(inline([.strong(inlines)], level == 3 ? 14 : 13), indent, indent)
case .list(let ordered, let start, let items, _):
let font = Fonts.ns(ctx.size)
let font = Fonts.native(ctx.size)
for (n, item) in items.enumerated() {
if n > 0 {
gap = 2
@@ -563,14 +586,14 @@ private struct FlowBuilder {
private func inline(_ inlines: [MDInline], _ size: CGFloat) -> NSAttributedString {
let a = InlineRenderer(store: store, size: size, taskId: ctx.taskId, weight: ctx.weight, dir: ctx.dir).text(inlines)
return try! NSAttributedString(a, including: \.appKit)
return nativeString(a)
}
private mutating func para(_ a: NSAttributedString, _ first: CGFloat, _ head: CGFloat) {
let s = NSMutableAttributedString(attributedString: a)
s.append(NSAttributedString(string: "\n", attributes: [.font: Fonts.ns(ctx.size)]))
s.append(NSAttributedString(string: "\n", attributes: [.font: Fonts.native(ctx.size)]))
let all = NSRange(location: 0, length: s.length)
flowColor(s, NSColor(ctx.color))
flowColor(s, PlatformColor(ctx.color))
let lead = (s.string as NSString).range(of: "\n").location + 1
s.addAttribute(.paragraphStyle, value: style(first, head, 0), range: all)
s.addAttribute(.paragraphStyle, value: style(first, head, gap), range: NSRange(location: 0, length: lead))
@@ -589,6 +612,36 @@ private struct FlowBuilder {
}
}
private final class FlowMeasure {
private let storage = NSTextStorage()
private let layout = NSLayoutManager()
private let container = NSTextContainer()
private var measured: (text: NSAttributedString, width: CGFloat?, size: CGSize)?
init() {
container.lineFragmentPadding = 0
layout.addTextContainer(container)
storage.addLayoutManager(layout)
}
func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize {
let same = measured.map { $0.text === text || $0.text.isEqual(to: text) } ?? false
if same, let m = measured, m.width == width {
return m.size
}
if !same {
storage.setAttributedString(text)
}
container.size = CGSize(width: width ?? .greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
layout.ensureLayout(for: container)
let used = layout.usedRect(for: container)
let size = CGSize(width: width ?? ceil(used.width), height: ceil(used.height))
measured = (text, width, size)
return size
}
}
#if os(macOS)
private final class FlowTextView: NSTextView {
var onTap: (@MainActor @Sendable () -> Void)?
let storage: NSTextStorage
@@ -702,32 +755,10 @@ private struct FlowText: NSViewRepresentable {
final class Coordinator: NSObject, NSTextViewDelegate {
var openURL: OpenURLAction?
private let storage = NSTextStorage()
private let layout = NSLayoutManager()
private let container = NSTextContainer()
private var measured: (text: NSAttributedString, width: CGFloat?, size: CGSize)?
override init() {
super.init()
container.lineFragmentPadding = 0
layout.addTextContainer(container)
storage.addLayoutManager(layout)
}
private let measure = FlowMeasure()
func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize {
let same = measured.map { $0.text === text || $0.text.isEqual(to: text) } ?? false
if same, let m = measured, m.width == width {
return m.size
}
if !same {
storage.setAttributedString(text)
}
container.size = CGSize(width: width ?? .greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
layout.ensureLayout(for: container)
let used = layout.usedRect(for: container)
let size = CGSize(width: width ?? ceil(used.width), height: ceil(used.height))
measured = (text, width, size)
return size
measure.size(text, width)
}
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
@@ -739,6 +770,113 @@ private struct FlowText: NSViewRepresentable {
}
}
}
#else
private final class FlowTextView: UITextView {
var onTap: (@MainActor @Sendable () -> Void)?
let storage: NSTextStorage
init() {
let storage = NSTextStorage()
let layout = NSLayoutManager()
let container = NSTextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude))
container.lineFragmentPadding = 0
container.widthTracksTextView = true
storage.addLayoutManager(layout)
layout.addTextContainer(container)
self.storage = storage
super.init(frame: .zero, textContainer: container)
isEditable = false
isSelectable = true
isScrollEnabled = false
backgroundColor = .clear
textContainerInset = .zero
linkTextAttributes = [:]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
func link(at point: CGPoint) -> Bool {
guard storage.length > 0 else {
return false
}
let p = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)
let g = layoutManager.glyphIndex(for: p, in: textContainer)
if !layoutManager.boundingRect(forGlyphRange: NSRange(location: g, length: 1), in: textContainer).contains(p) {
return false
}
let i = layoutManager.characterIndexForGlyph(at: g)
if i >= storage.length {
return false
}
return unsafe storage.attribute(.link, at: i, effectiveRange: nil) != nil
}
}
private struct FlowText: UIViewRepresentable {
let text: NSAttributedString
func makeUIView(context: Context) -> FlowTextView {
let v = FlowTextView()
v.delegate = context.coordinator
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped(_:)))
tap.delegate = context.coordinator
v.addGestureRecognizer(tap)
return v
}
func updateUIView(_ v: FlowTextView, context: Context) {
context.coordinator.openURL = context.environment.openURL
v.onTap = context.environment.choiceTap
if v.storage.isEqual(to: text) {
return
}
let selected = v.selectedRange
v.storage.setAttributedString(text)
if NSMaxRange(selected) <= text.length {
v.selectedRange = selected
}
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView v: FlowTextView, context: Context) -> CGSize? {
context.coordinator.size(text, proposal.width.flatMap { $0.isFinite ? $0 : nil })
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UITextViewDelegate, UIGestureRecognizerDelegate {
var openURL: OpenURLAction?
private let measure = FlowMeasure()
func size(_ text: NSAttributedString, _ width: CGFloat?) -> CGSize {
measure.size(text, width)
}
@objc func tapped(_ g: UITapGestureRecognizer) {
guard let v = g.view as? FlowTextView, let onTap = v.onTap, v.selectedRange.length == 0, !v.link(at: g.location(in: v)) else {
return
}
onTap()
}
func gestureRecognizer(_ g: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
true
}
func textView(_ textView: UITextView, primaryActionFor textItem: UITextItem, defaultAction: UIAction) -> UIAction? {
guard case .link(let url) = textItem.content, let openURL else {
return defaultAction
}
return UIAction { _ in
openURL(url)
}
}
}
}
#endif
struct InlineView: View {
let inlines: [MDInline]
@@ -776,8 +914,8 @@ struct InlineView: View {
}
private func flowString(_ a: AttributedString) -> NSAttributedString {
let s = NSMutableAttributedString(attributedString: try! NSAttributedString(a, including: \.appKit))
flowColor(s, NSColor(ctx.color))
let s = NSMutableAttributedString(attributedString: nativeString(a))
flowColor(s, PlatformColor(ctx.color))
let p = NSMutableParagraphStyle()
p.lineSpacing = 3
s.addAttribute(.paragraphStyle, value: p, range: NSRange(location: 0, length: s.length))