126 lines
5.3 KiB
Swift
126 lines
5.3 KiB
Swift
@_spi(SGTKInternal) import GObject
|
|
|
|
/// Applies `binding` to a widget property in one direction only: binding -> widget.
|
|
///
|
|
/// Selected by overload resolution when `Value` is not `Equatable`. Without `==`
|
|
/// there is no way to compare before writing, so a write-back would echo forever.
|
|
///
|
|
/// - Parameters:
|
|
/// - object: The widget the property lives on. Unused here; present so both
|
|
/// overloads share one signature.
|
|
/// - binding: The value source. Read once at mount, then subscribed to.
|
|
/// - registry: The mounting subtree's registry; the subscription is cancelled
|
|
/// when it tears down.
|
|
/// - notifyDetail: The candidate GObject property name. Unused in this
|
|
/// one-way overload; present so both overloads share one signature.
|
|
/// - read: Reads the widget's current value. Unused in this overload.
|
|
/// - write: Pushes a value into the widget.
|
|
@_spi(Portico) @MainActor
|
|
public func bindProperty<Value>(
|
|
_ object: GObject.Object,
|
|
_ binding: Binding<Value>,
|
|
registry: NodeRegistry,
|
|
notifyDetail: String,
|
|
read: @escaping () -> Value?,
|
|
write: @escaping (Value) -> Void
|
|
) {
|
|
write(binding.untrackedValue)
|
|
registry.add(binding.subscribe { v in write(v) })
|
|
}
|
|
|
|
/// Applies `binding` to a widget property in both directions.
|
|
///
|
|
/// Pushes the binding's value into the widget at mount, then keeps the two in
|
|
/// sync: the widget's `notify::<property>` signal writes the widget's current
|
|
/// value back into the binding, and the binding's subscription writes new
|
|
/// values into the widget. Each direction compares before writing, which
|
|
/// terminates the echo after one hop.
|
|
///
|
|
/// `notifyDetail` is resolved against the widget's GObject class. If the
|
|
/// candidate is not an installed property, the handler falls back to the
|
|
/// undetailed `notify` signal. This preserves write-back for wrapper accessors
|
|
/// whose Swift name differs from the underlying GObject property name.
|
|
///
|
|
/// - Parameters:
|
|
/// - object: The widget the property lives on; `notify` is connected here.
|
|
/// - binding: The value source and write-back destination.
|
|
/// - registry: The mounting subtree's registry; the signal handle and the
|
|
/// subscription are released when it tears down.
|
|
/// - notifyDetail: A candidate canonical GObject property name.
|
|
/// - read: Reads the widget's current value. `nil` suppresses the write-back
|
|
/// for that emission.
|
|
/// - write: Pushes a value into the widget.
|
|
///
|
|
@_spi(Portico) @MainActor
|
|
public func bindProperty<Value: Equatable>(
|
|
_ object: GObject.Object,
|
|
_ binding: Binding<Value>,
|
|
registry: NodeRegistry,
|
|
notifyDetail: String,
|
|
read: @escaping () -> Value?,
|
|
write: @escaping (Value) -> Void
|
|
) {
|
|
write(binding.untrackedValue)
|
|
registry.add(object.connectNotify(detail: resolvedNotifyDetail(object, notifyDetail)) { _, _ in
|
|
guard let current = read(), current != binding.untrackedValue else { return }
|
|
binding.wrappedValue = current
|
|
})
|
|
registry.add(binding.subscribe { v in
|
|
if let current = read(), current == v { return }
|
|
write(v)
|
|
})
|
|
}
|
|
|
|
/// Returns `candidate` when the object's class installs a GObject property
|
|
/// with that name, and `nil` otherwise.
|
|
///
|
|
/// The class pointer is the first word of every `GTypeInstance`, which is what
|
|
/// the `G_OBJECT_GET_CLASS` macro reads. Interface-installed properties are in
|
|
/// the class's param spec pool and resolve through the class lookup.
|
|
@MainActor
|
|
private func resolvedNotifyDetail(_ object: GObject.Object, _ candidate: String) -> String? {
|
|
let klass = object.pointer.load(as: UnsafeMutableRawPointer.self)
|
|
let found = candidate.withCString { portico_g_object_class_find_property(klass, $0) }
|
|
return found == nil ? nil : candidate
|
|
}
|
|
|
|
@_silgen_name("g_object_class_find_property")
|
|
private nonisolated func portico_g_object_class_find_property(
|
|
_ objectClass: UnsafeMutableRawPointer,
|
|
_ propertyName: UnsafePointer<CChar>
|
|
) -> UnsafeMutableRawPointer?
|
|
|
|
/// Writes `text` into a widget property once, and keeps it live when the text has
|
|
/// deferred segments.
|
|
///
|
|
/// A literal with no interpolation is written once and no tracker is retained. Otherwise a
|
|
/// ``DependencyTracker`` re-evaluates the text on every change to the state it reads and
|
|
/// pushes the result through `write`. The tracker is registered with `registry` only if it
|
|
/// actually captured a dependency, so interpolating a non-state value costs nothing after
|
|
/// mount.
|
|
@_spi(Portico) @MainActor
|
|
public func bindInterpolation(
|
|
_ text: InterpolatedText,
|
|
registry: NodeRegistry,
|
|
write: @escaping (String) -> Void
|
|
) {
|
|
if let staticText = text.staticText { write(staticText); return }
|
|
let tracker = DependencyTracker { write(text.evaluate()) }
|
|
tracker.run()
|
|
if tracker.hasDependencies || tracker.didArmObservation { registry.add(tracker) }
|
|
}
|
|
|
|
/// ``bindInterpolation(_:registry:write:)`` for a nullable property; a `nil` text writes
|
|
/// `nil` once.
|
|
@_spi(Portico) @MainActor
|
|
public func bindOptionalInterpolation(
|
|
_ text: InterpolatedText?,
|
|
registry: NodeRegistry,
|
|
write: @escaping (String?) -> Void
|
|
) {
|
|
guard let text else { write(nil); return }
|
|
if let staticText = text.staticText { write(staticText); return }
|
|
let tracker = DependencyTracker { write(text.evaluate()) }
|
|
tracker.run()
|
|
if tracker.hasDependencies || tracker.didArmObservation { registry.add(tracker) }
|
|
}
|