Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ private func luiOCamlTextChanged(_ node: Int64, _ text: UnsafePointer<CChar>?) -
private func luiOCamlSubmit(_ node: Int64) -> Int32
@_silgen_name("lui_ocaml_dismiss")
private func luiOCamlDismiss(_ node: Int64) -> Int32
@_silgen_name("lui_ocaml_picked")
private func luiOCamlPicked(_ node: Int64, _ payload: UnsafePointer<CChar>) -> Int32
@_silgen_name("lui_ocaml_double_press")
private func luiOCamlDoublePress(_ node: Int64) -> Int32
@_silgen_name("lui_ocaml_toggle_changed")
Expand Down Expand Up @@ -73,6 +75,8 @@ private final class GalleryHost {
text.withCString { _ = luiOCamlTextChanged(Int64(node), $0) }
case let .submit(node): _ = luiOCamlSubmit(Int64(node))
case let .dismiss(node): _ = luiOCamlDismiss(Int64(node))
case let .picked(node, payload):
payload.withCString { _ = luiOCamlPicked(Int64(node), $0) }
case let .doublePress(node): _ = luiOCamlDoublePress(Int64(node))
case let .toggleChanged(node, checked):
_ = luiOCamlToggleChanged(Int64(node), checked ? 1 : 0)
Expand Down
3 changes: 3 additions & 0 deletions examples/components/native/components_bridge.ml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ let submit node = dispatch (Submit node)

let dismiss node = dispatch (Dismiss node)

let picked node payload = dispatch (Picked (node, payload))

let double_press node = dispatch (DoublePress node)

let toggle_changed node checked = dispatch (ToggleChanged (node, checked))
Expand All @@ -104,6 +106,7 @@ let register prefix =
register (prefix ^ "_text_changed") text_changed;
register (prefix ^ "_submit") submit;
register (prefix ^ "_dismiss") dismiss;
register (prefix ^ "_picked") picked;
register (prefix ^ "_double_press") double_press;
register (prefix ^ "_toggle_changed") toggle_changed;
register (prefix ^ "_radio_changed") radio_changed;
Expand Down
12 changes: 12 additions & 0 deletions examples/markdown/macos-appkit/Sources/LUIMarkdown/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ private typealias StopFunction = @convention(c) () -> Int32
private typealias NodeFunction = @convention(c) (Int64) -> Int32
private typealias TextChangedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias PickedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias ToggleChangedFunction = @convention(c) (Int64, Int32) -> Int32
private typealias SliderChangedFunction = @convention(c) (Int64, Double) -> Int32
private typealias ExtensionEventFunction =
Expand Down Expand Up @@ -38,6 +40,7 @@ private final class NativeMarkdownRuntime {
private let textChangedFunction: TextChangedFunction
private let submitFunction: NodeFunction
private let dismissFunction: NodeFunction
private let pickedFunction: PickedFunction
private let doublePressFunction: NodeFunction
private let toggleChangedFunction: ToggleChangedFunction
private let radioChangedFunction: NodeFunction
Expand All @@ -57,6 +60,7 @@ private final class NativeMarkdownRuntime {
textChangedFunction = try Self.load("lui_ocaml_text_changed", from: handle)
submitFunction = try Self.load("lui_ocaml_submit", from: handle)
dismissFunction = try Self.load("lui_ocaml_dismiss", from: handle)
pickedFunction = try Self.load("lui_ocaml_picked", from: handle)
doublePressFunction = try Self.load("lui_ocaml_double_press", from: handle)
toggleChangedFunction = try Self.load("lui_ocaml_toggle_changed", from: handle)
radioChangedFunction = try Self.load("lui_ocaml_radio_changed", from: handle)
Expand Down Expand Up @@ -86,6 +90,12 @@ private final class NativeMarkdownRuntime {

func submit(node: Int) { _ = submitFunction(Int64(node)) }
func dismiss(node: Int) { _ = dismissFunction(Int64(node)) }

func picked(node: Int, payload: String) {
payload.withCString { source in
_ = pickedFunction(Int64(node), source)
}
}
func doublePress(node: Int) { _ = doublePressFunction(Int64(node)) }

func toggleChanged(node: Int, checked: Bool) {
Expand Down Expand Up @@ -166,6 +176,8 @@ private final class MarkdownHost: NSObject, NSApplicationDelegate, NSWindowDeleg
self?.runtime?.sliderChanged(node: node, value: value)
case let .dismiss(node):
self?.runtime?.dismiss(node: node)
case let .picked(node, payload):
self?.runtime?.picked(node: node, payload: payload)
case let .doublePress(node):
self?.runtime?.doublePress(node: node)
case let .appear(node):
Expand Down
3 changes: 3 additions & 0 deletions examples/markdown/native/markdown_bridge.ml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ let submit node = dispatch (Submit node)

let dismiss node = dispatch (Dismiss node)

let picked node payload = dispatch (Picked (node, payload))

let double_press node = dispatch (DoublePress node)

let toggle_changed node checked = dispatch (ToggleChanged (node, checked))
Expand Down Expand Up @@ -106,6 +108,7 @@ let () =
Callback.register "lui_ocaml_text_changed" text_changed;
Callback.register "lui_ocaml_submit" submit;
Callback.register "lui_ocaml_dismiss" dismiss;
Callback.register "lui_ocaml_picked" picked;
Callback.register "lui_ocaml_double_press" double_press;
Callback.register "lui_ocaml_toggle_changed" toggle_changed;
Callback.register "lui_ocaml_radio_changed" radio_changed;
Expand Down
12 changes: 12 additions & 0 deletions examples/split/macos-appkit/Sources/LUISplitDemo/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ private typealias TextChangedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias SubmitFunction = @convention(c) (Int64) -> Int32
private typealias DismissFunction = @convention(c) (Int64) -> Int32
private typealias PickedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias DoublePressFunction = @convention(c) (Int64) -> Int32
private typealias ToggleChangedFunction = @convention(c) (Int64, Int32) -> Int32
private typealias RadioChangedFunction = @convention(c) (Int64) -> Int32
Expand Down Expand Up @@ -83,6 +85,7 @@ private final class NativeSplitRuntime {
private let textChangedFunction: TextChangedFunction
private let submitFunction: SubmitFunction
private let dismissFunction: DismissFunction
private let pickedFunction: PickedFunction
private let doublePressFunction: DoublePressFunction
private let toggleChangedFunction: ToggleChangedFunction
private let radioChangedFunction: RadioChangedFunction
Expand All @@ -102,6 +105,7 @@ private final class NativeSplitRuntime {
textChangedFunction = try Self.load("lui_ocaml_text_changed", from: handle)
submitFunction = try Self.load("lui_ocaml_submit", from: handle)
dismissFunction = try Self.load("lui_ocaml_dismiss", from: handle)
pickedFunction = try Self.load("lui_ocaml_picked", from: handle)
doublePressFunction = try Self.load("lui_ocaml_double_press", from: handle)
toggleChangedFunction = try Self.load("lui_ocaml_toggle_changed", from: handle)
radioChangedFunction = try Self.load("lui_ocaml_radio_changed", from: handle)
Expand Down Expand Up @@ -135,6 +139,12 @@ private final class NativeSplitRuntime {

func dismiss(node: Int) { _ = dismissFunction(Int64(node)) }

func picked(node: Int, payload: String) {
payload.withCString { source in
_ = pickedFunction(Int64(node), source)
}
}

func doublePress(node: Int) { _ = doublePressFunction(Int64(node)) }

func toggleChanged(node: Int, checked: Bool) {
Expand Down Expand Up @@ -237,6 +247,8 @@ private final class SplitDemoHost: NSObject, NSApplicationDelegate, NSWindowDele
self?.runtime?.sliderChanged(node: node, value: value)
case let .dismiss(node):
self?.runtime?.dismiss(node: node)
case let .picked(node, payload):
self?.runtime?.picked(node: node, payload: payload)
case let .doublePress(node):
self?.runtime?.doublePress(node: node)
case let .`extension`(node, identifier, name, values):
Expand Down
3 changes: 3 additions & 0 deletions examples/split/native/split_bridge.ml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ let submit node = dispatch (Submit node)

let dismiss node = dispatch (Dismiss node)

let picked node payload = dispatch (Picked (node, payload))

let double_press node = dispatch (DoublePress node)

let toggle_changed node checked = dispatch (ToggleChanged (node, checked))
Expand Down Expand Up @@ -106,6 +108,7 @@ let () =
Callback.register "lui_ocaml_text_changed" text_changed;
Callback.register "lui_ocaml_submit" submit;
Callback.register "lui_ocaml_dismiss" dismiss;
Callback.register "lui_ocaml_picked" picked;
Callback.register "lui_ocaml_double_press" double_press;
Callback.register "lui_ocaml_toggle_changed" toggle_changed;
Callback.register "lui_ocaml_radio_changed" radio_changed;
Expand Down
12 changes: 12 additions & 0 deletions examples/todos/macos-appkit/Sources/LUITodos/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ private typealias TextChangedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias SubmitFunction = @convention(c) (Int64) -> Int32
private typealias DismissFunction = @convention(c) (Int64) -> Int32
private typealias PickedFunction =
@convention(c) (Int64, UnsafePointer<CChar>?) -> Int32
private typealias DoublePressFunction = @convention(c) (Int64) -> Int32
private typealias ToggleChangedFunction = @convention(c) (Int64, Int32) -> Int32
private typealias RadioChangedFunction = @convention(c) (Int64) -> Int32
Expand All @@ -39,6 +41,7 @@ private final class NativeTodosRuntime {
private let textChangedFunction: TextChangedFunction
private let submitFunction: SubmitFunction
private let dismissFunction: DismissFunction
private let pickedFunction: PickedFunction
private let doublePressFunction: DoublePressFunction
private let toggleChangedFunction: ToggleChangedFunction
private let radioChangedFunction: RadioChangedFunction
Expand All @@ -56,6 +59,7 @@ private final class NativeTodosRuntime {
textChangedFunction = try Self.load("lui_ocaml_text_changed", from: handle)
submitFunction = try Self.load("lui_ocaml_submit", from: handle)
dismissFunction = try Self.load("lui_ocaml_dismiss", from: handle)
pickedFunction = try Self.load("lui_ocaml_picked", from: handle)
doublePressFunction = try Self.load("lui_ocaml_double_press", from: handle)
toggleChangedFunction = try Self.load("lui_ocaml_toggle_changed", from: handle)
radioChangedFunction = try Self.load("lui_ocaml_radio_changed", from: handle)
Expand Down Expand Up @@ -94,6 +98,12 @@ private final class NativeTodosRuntime {
_ = dismissFunction(Int64(node))
}

func picked(node: Int, payload: String) {
payload.withCString { source in
_ = pickedFunction(Int64(node), source)
}
}

func doublePress(node: Int) {
_ = doublePressFunction(Int64(node))
}
Expand Down Expand Up @@ -161,6 +171,8 @@ private final class TodosHost: NSObject, NSApplicationDelegate, NSWindowDelegate
self?.runtime?.sliderChanged(node: node, value: value)
case let .dismiss(node):
self?.runtime?.dismiss(node: node)
case let .picked(node, payload):
self?.runtime?.picked(node: node, payload: payload)
case let .doublePress(node):
self?.runtime?.doublePress(node: node)
}
Expand Down
3 changes: 3 additions & 0 deletions examples/todos/native/todos_bridge.ml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ let submit node = dispatch (Submit node)

let dismiss node = dispatch (Dismiss node)

let picked node payload = dispatch (Picked (node, payload))

let double_press node = dispatch (DoublePress node)

let toggle_changed node checked = dispatch (ToggleChanged (node, checked))
Expand All @@ -94,6 +96,7 @@ let () =
Callback.register "lui_ocaml_text_changed" text_changed;
Callback.register "lui_ocaml_submit" submit;
Callback.register "lui_ocaml_dismiss" dismiss;
Callback.register "lui_ocaml_picked" picked;
Callback.register "lui_ocaml_double_press" double_press;
Callback.register "lui_ocaml_toggle_changed" toggle_changed;
Callback.register "lui_ocaml_radio_changed" radio_changed;
Expand Down
85 changes: 77 additions & 8 deletions platform/apple/Sources/LUIAppleBackend/LUIAppleBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ final class LUINodeModel: Identifiable {
properties[.doublePressEnabled]?.boolValue ?? false
}
var supportsAppear: Bool { properties[.appearEnabled]?.boolValue ?? false }
var rowKey: String? { properties[.key]?.stringValue }
var separatorVisibility: String? { properties[.separator]?.stringValue }
var listStyle: String? { properties[.style]?.stringValue }
var scrollTarget: String? { properties[.scrollTarget]?.stringValue }
var scrollAnchor: String? { properties[.scrollAnchor]?.stringValue }
var scrollToken: Int? { properties[.scrollToken]?.intValue }
var scrollAnimated: Bool { properties[.scrollAnimated]?.boolValue ?? true }
var tracksVisibleRange: Bool {
properties[.trackVisibleRange]?.boolValue ?? false
}
var containerRelativeFrame: String? {
properties[.containerRelativeFrame]?.stringValue
}
Expand Down Expand Up @@ -407,6 +417,40 @@ public final class LUIAppleBackend {
private var nodeFrames: [Int: CGRect] = [:]
private var framesReportScheduled = false

// MARK: - file-picker operations

/// In-flight and completed file-picker requests keyed by node id. Held
/// here rather than in `LUIFilePickerView`'s @State so a view teardown
/// (tab switch, re-layout) does not release security-scoped files or
/// lose the pending completion handshake for a still-mounted node.
private var filePickerOperations: [Int: LUIFilePickerOperation] = [:]

func filePickerOperation(node: Int) -> LUIFilePickerOperation? {
filePickerOperations[node]
}

func setFilePickerOperation(node: Int, _ operation: LUIFilePickerOperation) {
filePickerOperations[node] = operation
}

/// Drops the operation and releases every file it retained.
func clearFilePickerOperation(node: Int) {
guard let operation = filePickerOperations.removeValue(forKey: node)
else { return }
releaseFilePickerFiles(operation.files)
}

func releaseFilePickerFiles(_ files: [LUIRetainedFile]) {
for file in files {
if file.securityScoped {
file.url.stopAccessingSecurityScopedResource()
}
if file.temporary {
try? FileManager.default.removeItem(at: file.url)
}
}
}

func reportNodeFrame(_ nodeID: Int, _ rect: CGRect) {
if nodeFrames[nodeID] == rect { return }
nodeFrames[nodeID] = rect
Expand Down Expand Up @@ -653,7 +697,7 @@ public final class LUIAppleBackend {
(model.kind == .tableCell && model.supportsPress) ||
model.kind == .select ||
model.kind == .combobox || model.kind == .menuItem ||
model.kind == .listItem ||
model.kind == .listItem || model.kind == .swipeAction ||
(model.kind == .timelineItem && model.supportsPress) ||
(model.kind == .fileImage && model.supportsPress) ||
(model.isTreeItem && model.supportsPress),
Expand Down Expand Up @@ -707,6 +751,14 @@ public final class LUIAppleBackend {
emit(.appear(node: node))
}

func performScrollCompleted(node: Int, token: Int, outcome: String) {
emit(.scrollCompleted(node: node, token: token, outcome: outcome))
}

func performVisibleRange(node: Int, first: Int, last: Int) {
emit(.visibleRange(node: node, first: first, last: last))
}

public func performExtensionEvent(
node: Int,
name: String,
Expand Down Expand Up @@ -736,9 +788,11 @@ public final class LUIAppleBackend {
guard let model = models[node],
model.kind == .toggleButton || model.kind == .checkbox ||
model.kind == .switchControl || model.kind == .toggle ||
model.kind == .accordion || model.kind == .drawer || model.isTreeItem,
model.kind == .accordion || model.kind == .drawer ||
model.kind == .listItem || model.isTreeItem,
model.isEnabled,
(model.kind != .accordion && model.kind != .drawer && !model.isTreeItem) ||
(model.kind != .accordion && model.kind != .drawer &&
model.kind != .listItem && !model.isTreeItem) ||
model.supportsToggle else {
throw invalid("node \(node) is not an enabled toggle")
}
Expand Down Expand Up @@ -782,12 +836,20 @@ public final class LUIAppleBackend {
model.kind == .select || model.kind == .combobox ||
model.kind == .dropdownMenu || model.kind == .dialog ||
model.kind == .sheet || model.kind == .toast ||
model.kind == .filePreview else {
model.kind == .filePreview ||
model.kind == .filePicker else {
throw invalid("node \(node) is not dismissible")
}
emit(.dismiss(node: node))
}

func performPicked(node: Int, payload: String) throws {
guard let model = models[node], model.kind == .filePicker else {
throw invalid("node \(node) is not a file-picker")
}
emit(.picked(node: node, payload: payload))
}

func performAction(node: Int) throws {
guard let model = models[node] else { throw invalid("unknown node") }
if model.isTreeItem {
Expand All @@ -796,7 +858,7 @@ public final class LUIAppleBackend {
}
switch model.kind {
case .button, .select, .combobox, .menuItem, .listItem, .timelineItem,
.fileImage:
.swipeAction, .fileImage:
try performPress(node: node)
case .toggleButton:
try performToggle(node: node, checked: !model.isSelected)
Expand Down Expand Up @@ -920,13 +982,20 @@ public final class LUIAppleBackend {
for id in dropped {
models[id] = nil
extensionModels[id] = nil
clearFilePickerOperation(node: id)
}
for id in touched where !dropped.contains(id) {
if let state = tree.nodes[id] {
// Kinds are immutable, so context-menu membership in the
// Kinds are immutable, so auxiliary-slot membership in the
// child list is fixed until the list itself changes.
let visibleChildren = state.children.filter {
tree.nodes[$0]?.kind != .contextMenu
let visibleChildren = state.children.filter { childID in
switch tree.nodes[childID]?.kind {
case .contextMenu, .swipeActions, .listSectionHeader,
.listSectionFooter:
false
default:
true
}
}
if let model = models[id] {
model.apply(state: state, visibleChildren: visibleChildren)
Expand Down
Loading
Loading