portico/Sources/Portico/Extensions/BreakpointBin+Extras.swift

268 lines
9.8 KiB
Swift

import Adw
import GObject
import Gtk
/// A typed property setter installed on an ``Adw.Breakpoint``.
///
/// The target is resolved from a mounted ``WidgetRef`` or its projected binding.
/// Libadwaita restores the target property when the breakpoint is unapplied.
public struct Setter {
fileprivate let target: () -> GLibObject?
fileprivate let property: String
fileprivate let addTo: (Adw.Breakpoint, GLibObject, String) -> Void
private init(
target: @escaping () -> GLibObject?,
property: String,
addTo: @escaping (Adw.Breakpoint, GLibObject, String) -> Void
) {
self.target = target
self.property = property
self.addTo = addTo
}
/// Creates a boolean setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, bool: Bool) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, bool: bool)
}
)
}
/// Creates a boolean setter targeting a widget binding.
public init<W: Gtk.Widget>(_ ref: Binding<W?>, _ property: String, bool: Bool) {
self.init(
target: { ref.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, bool: bool)
}
)
}
/// Creates a string setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, string: String) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, string: string)
}
)
}
/// Creates a string setter targeting a widget binding.
public init<W: Gtk.Widget>(_ ref: Binding<W?>, _ property: String, string: String) {
self.init(
target: { ref.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, string: string)
}
)
}
/// Creates a signed integer setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, int: Int32) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, int: int)
}
)
}
/// Creates an unsigned integer setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, uint: UInt32) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, uint: uint)
}
)
}
/// Creates a double setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, double: Double) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, double: double)
}
)
}
/// Creates an object setter targeting a widget reference.
public init<W: Gtk.Widget>(_ ref: WidgetRef<W>, _ property: String, widget: GLibObject?) {
self.init(
target: { ref.projectedValue.untrackedValue },
property: property,
addTo: { breakpoint, object, property in
breakpoint.addSetter(object: object, property: property, widget: widget)
}
)
}
}
/// Collects typed setters in a breakpoint modifier closure.
@resultBuilder
public struct BreakpointBuilder {
/// Combines the setters in a single builder block.
public static func buildBlock(_ components: [Setter]...) -> [Setter] {
components.flatMap { $0 }
}
/// Lifts a single setter into the builder's component type.
public static func buildExpression(_ setter: Setter) -> [Setter] {
[setter]
}
/// Combines setters produced by a loop.
public static func buildArray(_ components: [[Setter]]) -> [Setter] {
components.flatMap { $0 }
}
/// Handles an optional setter branch.
public static func buildOptional(_ component: [Setter]?) -> [Setter] {
component ?? []
}
/// Selects the first conditional branch.
public static func buildEither(first component: [Setter]) -> [Setter] {
component
}
/// Selects the second conditional branch.
public static func buildEither(second component: [Setter]) -> [Setter] {
component
}
}
/// Context for registering breakpoint apply and unapply handlers.
public struct BreakpointConfiguration {
/// The breakpoint being configured.
public let breakpoint: Adw.Breakpoint
let registry: NodeRegistry
init(breakpoint: Adw.Breakpoint, registry: NodeRegistry) {
self.breakpoint = breakpoint
self.registry = registry
}
/// Registers a handler that runs after breakpoint setters are applied.
public func onApply(_ handler: @escaping () -> Void) {
registry.add(breakpoint.connectApply { _ in handler() })
}
/// Registers a handler that runs before breakpoint setters are reset.
public func onUnapply(_ handler: @escaping () -> Void) {
registry.add(breakpoint.connectUnapply { _ in handler() })
}
}
extension WidgetView where Target: Adw.BreakpointBin {
/// Adds a breakpoint that writes `true` while its condition is active.
public func breakpoint(_ condition: Condition, isActive: Binding<Bool>) -> Self {
appending { widget, context in
let breakpoint = Adw.Breakpoint(condition: condition.condition)
context.registry.add(breakpoint.connectApply { _ in
isActive.wrappedValue = true
})
context.registry.add(breakpoint.connectUnapply { _ in
isActive.wrappedValue = false
})
widget.addBreakpoint(breakpoint: breakpoint)
}
}
/// Adds a breakpoint from a libadwaita condition string.
public func breakpoint(_ condition: String, isActive: Binding<Bool>) -> Self {
breakpoint(Condition(stringLiteral: condition), isActive: isActive)
}
/// Adds a breakpoint that identifies its matching layout value.
public func breakpoint<Layout: Equatable>(
_ condition: Condition,
isActive: Binding<Layout>,
matches: Layout
) -> Self {
appending { widget, context in
let breakpoint = Adw.Breakpoint(condition: condition.condition)
context.registry.add(breakpoint.connectApply { _ in
isActive.wrappedValue = matches
})
context.registry.add(breakpoint.connectUnapply { _ in
// A later breakpoint may already have replaced this value.
_ = isActive.untrackedValue == matches
})
widget.addBreakpoint(breakpoint: breakpoint)
}
}
/// Adds a breakpoint with typed widget-property setters.
public func breakpoint(
_ condition: Condition,
@BreakpointBuilder _ setters: () -> [Setter]
) -> Self {
let setterValues = setters()
return appending { widget, context in
let breakpoint = Adw.Breakpoint(condition: condition.condition)
for setter in setterValues {
guard let target = setter.target() else {
preconditionFailure(
"Setter target \(setter.property): WidgetRef is nil. "
+ "Ensure the widget is published with .ref() before the breakpoint mounts."
)
}
setter.addTo(breakpoint, target, setter.property)
}
widget.addBreakpoint(breakpoint: breakpoint)
}
}
/// Adds typed widget-property setters from a libadwaita condition string.
public func breakpoint(
_ condition: String,
@BreakpointBuilder _ setters: () -> [Setter]
) -> Self {
breakpoint(Condition(stringLiteral: condition), setters)
}
/// Adds a breakpoint with manually managed apply and unapply handlers.
public func breakpoint(
_ condition: Condition,
configure: @escaping (BreakpointConfiguration) -> Void
) -> Self {
appending { widget, context in
let breakpoint = Adw.Breakpoint(condition: condition.condition)
configure(BreakpointConfiguration(breakpoint: breakpoint, registry: context.registry))
widget.addBreakpoint(breakpoint: breakpoint)
}
}
/// Adds manually managed handlers from a libadwaita condition string.
public func breakpoint(
_ condition: String,
configure: @escaping (BreakpointConfiguration) -> Void
) -> Self {
breakpoint(Condition(stringLiteral: condition), configure: configure)
}
/// Binds the currently active breakpoint, or `nil` when none match.
public func currentBreakpoint(_ binding: Binding<Breakpoint?>) -> Self {
appending { widget, context in
context.registry.add(
widget.connectNotify(detail: "current-breakpoint") { _, _ in
binding.wrappedValue = widget.getCurrentBreakpoint()
}
)
binding.wrappedValue = widget.getCurrentBreakpoint()
}
}
}