Add an iPhone client sharing the macOS app's SwiftUI code
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user