Add breakpoint modifiers and condition DSL

This commit is contained in:
Brendan Szymanski 2026-08-11 18:17:01 -04:00
parent 3919f1fe34
commit 7c651e2064
13 changed files with 764 additions and 68 deletions

View file

@ -0,0 +1,46 @@
import Adw
import Portico
/// Demonstrates breakpoint conditions, typed setters, and apply handlers.
struct BreakpointDemoPage: View {
@State private var isCompact = false
@State private var appliedMessage = "No manual breakpoint applied"
@State private var currentBreakpoint: Breakpoint?
@WidgetRef private var carousel: Adw.Carousel?
var body: some View {
BreakpointBin {
VStack(spacing: 12) {
Label("Breakpoint demo")
.title1()
Label { isCompact ? "Compact layout" : "Wide layout" }
Label { appliedMessage }
.dimmed()
Carousel {
Label("Carousel content")
}
.ref(_carousel)
.hexpand(true)
.vexpand(true)
}
.margin(24)
.hexpand(true)
.vexpand(true)
}
.breakpoint("max-width: 700px", isActive: $isCompact)
.breakpoint(.maxWidth(500)) {
Setter(_carousel, "spacing", double: 0)
}
.breakpoint("max-width: 400px") { configuration in
configuration.onApply {
appliedMessage = "Manual breakpoint applied"
}
configuration.onUnapply {
appliedMessage = "Manual breakpoint unapplied"
}
}
.currentBreakpoint($currentBreakpoint)
.hexpand(true)
.vexpand(true)
}
}

View file

@ -31,6 +31,7 @@ struct ExampleApp: App {
settingsPage
AsyncDemoPage()
EnvironmentDemoPage()
BreakpointDemoPage()
navigationDemoPage
PresentationDemoPage()
conveniencePage
@ -42,7 +43,7 @@ struct ExampleApp: App {
.carousel($pager)
.halign(.center)
Label("Uptime: \(uptime)s, Page \(currentPage + 1) of 8")
Label("Uptime: \(uptime)s, Page \(currentPage + 1) of 9")
.dimmed()
.halign(.center)
.margin(8)

View file

@ -0,0 +1,268 @@
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()
}
}
}

View file

@ -349,6 +349,40 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.Button.init(withMnemonicLabel:)
/// Creates a new `GtkButton` containing a label.
///
/// If characters in `label` are preceded by an underscore, they are underlined.
/// If you need a literal underscore character in a label, use __ (two
/// underscores). The first underlined character represents a keyboard
/// accelerator called a mnemonic. Pressing <kbd>Alt</kbd> and that key
/// activates the button.
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withMnemonicLabel: The `withMnemonicLabel` value forwarded to `Gtk.Button`.
/// - Parameter canShrink: Whether the size of the button can be made smaller than the natural size of its contents.
/// - Parameter hasFrame: Whether the button has a frame.
/// - Parameter useUnderline: If set, an underline in the text indicates that the following character is to be used as mnemonic.
/// - Parameter child: A `ViewBuilder` closure whose first view is mounted into the `child` slot.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
/// - Parameter onClicked: Invoked when the widget emits the `clicked` signal.
public init(withMnemonicLabel: String, canShrink: Bool? = nil, hasFrame: Bool? = nil, useUnderline: Bool? = nil, @ViewBuilder child: () -> [AnyView] = { [] }, onActivate: (() -> Void)? = nil, onClicked: (() -> Void)? = nil) {
let childViews = child()
make = { _ in Gtk.Button(withMnemonicLabel: withMnemonicLabel) }
configure.append { w, ctx in
if let canShrink { w.setCanShrink(canShrink: canShrink) }
if let hasFrame { w.setHasFrame(hasFrame: hasFrame) }
if let useUnderline { w.setUseUnderline(useUnderline: useUnderline) }
if let v = childViews.first { w.setChild(child: v.makeWidget(ctx)) }
if let onActivate { ctx.registry.add(w.connectActivate { _ in onActivate() }) }
if let onClicked { ctx.registry.add(w.connectClicked { _ in onClicked() }) }
}
}
}
extension Button: WidgetView {

View file

@ -236,6 +236,34 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.CheckButton.init(withMnemonicLabel:)
/// Creates a new `GtkCheckButton` with the given text and a mnemonic.
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withMnemonicLabel: The `withMnemonicLabel` value forwarded to `Gtk.CheckButton`.
/// - Parameter active: If the check button is active.
/// - Parameter inconsistent: If the check button is in an in between state.
/// - Parameter useUnderline: If set, an underline in the text indicates that the following character is to be used as mnemonic.
/// - Parameter child: A `ViewBuilder` closure whose first view is mounted into the `child` slot.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
/// - Parameter onToggled: Invoked when the widget emits the `toggled` signal.
public init(withMnemonicLabel: String?, active: Bool? = nil, inconsistent: Bool? = nil, useUnderline: Bool? = nil, @ViewBuilder child: () -> [AnyView] = { [] }, onActivate: (() -> Void)? = nil, onToggled: (() -> Void)? = nil) {
let childViews = child()
make = { _ in Gtk.CheckButton(withMnemonicLabel: withMnemonicLabel) }
configure.append { w, ctx in
if let active { w.setActive(setting: active) }
if let inconsistent { w.setInconsistent(inconsistent: inconsistent) }
if let useUnderline { w.setUseUnderline(setting: useUnderline) }
if let v = childViews.first { w.setChild(child: v.makeWidget(ctx)) }
if let onActivate { ctx.registry.add(w.connectActivate { _ in onActivate() }) }
if let onToggled { ctx.registry.add(w.connectToggled { _ in onToggled() }) }
}
}
}
extension CheckButton: WidgetView {

View file

@ -160,6 +160,50 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.ComboBox.init(withModelAndEntryModel:)
/// Creates a new empty `GtkComboBox` with an entry and a model.
///
/// See also [ctor`Gtk`.ComboBox.new_with_entry].
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withModelAndEntryModel: The `withModelAndEntryModel` value forwarded to `Gtk.ComboBox`.
/// - Parameter active: The item which is currently active.
/// - Parameter activeId: The value of the ID column of the active row.
/// - Parameter buttonSensitivity: Whether the dropdown button is sensitive when the model is empty.
/// - Parameter entryTextColumn: The model column to associate with strings from the entry.
/// - Parameter idColumn: The model column that provides string IDs for the values in the model, if != -1.
/// - Parameter popupFixedWidth: Whether the popup's width should be a fixed width matching the allocated width of the combo box.
/// - Parameter child: A `ViewBuilder` closure whose first view is mounted into the `child` slot.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
/// - Parameter onChanged: Invoked when the widget emits the `changed` signal.
/// - Parameter onFormatEntryText: Invoked when the widget emits the `format-entry-text` signal. The closure receives the signal's arguments in order. Its return value is forwarded to GTK as the signal's result.
/// - Parameter onMoveActive: Invoked when the widget emits the `move-active` signal. The closure receives the signal's arguments in order.
/// - Parameter onPopdown: Invoked when the widget emits the `popdown` signal. Its return value is forwarded to GTK as the signal's result.
/// - Parameter onPopup: Invoked when the widget emits the `popup` signal.
public init(withModelAndEntryModel: Gtk.TreeModel, active: Int32? = nil, activeId: String? = nil, buttonSensitivity: Gtk.SensitivityType? = nil, entryTextColumn: Int32? = nil, idColumn: Int32? = nil, popupFixedWidth: Bool? = nil, @ViewBuilder child: () -> [AnyView] = { [] }, onActivate: (() -> Void)? = nil, onChanged: (() -> Void)? = nil, onFormatEntryText: ((String) -> String)? = nil, onMoveActive: ((Gtk.ScrollType) -> Void)? = nil, onPopdown: (() -> Bool)? = nil, onPopup: (() -> Void)? = nil) {
let childViews = child()
make = { _ in Gtk.ComboBox(withModelAndEntryModel: withModelAndEntryModel) }
configure.append { w, ctx in
if let active { w.setActive(index: active) }
if let activeId { w.setActiveId(activeId: activeId) }
if let buttonSensitivity { w.setButtonSensitivity(sensitivity: buttonSensitivity) }
if let entryTextColumn { w.setEntryTextColumn(textColumn: entryTextColumn) }
if let idColumn { w.setIdColumn(idColumn: idColumn) }
if let popupFixedWidth { w.setPopupFixedWidth(fixed: popupFixedWidth) }
if let v = childViews.first { w.setChild(child: v.makeWidget(ctx)) }
if let onActivate { ctx.registry.add(w.connectActivate { _ in onActivate() }) }
if let onChanged { ctx.registry.add(w.connectChanged { _ in onChanged() }) }
if let onFormatEntryText { ctx.registry.add(w.connectFormatEntryText { _, a0 in onFormatEntryText(a0) }) }
if let onMoveActive { ctx.registry.add(w.connectMoveActive { _, a0 in onMoveActive(a0) }) }
if let onPopdown { ctx.registry.add(w.connectPopdown { _ in onPopdown() }) }
if let onPopup { ctx.registry.add(w.connectPopup { _ in onPopup() }) }
}
}
}
extension ComboBox: WidgetView {

View file

@ -243,6 +243,44 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.Expander.init(withMnemonicLabel:)
/// Creates a new expander using `label` as the text of the label.
///
/// If characters in `label` are preceded by an underscore, they are
/// underlined. If you need a literal underscore character in a label,
/// use __ (two underscores). The first underlined character represents
/// a keyboard accelerator called a mnemonic.
///
/// Pressing Alt and that key activates the button.
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withMnemonicLabel: The `withMnemonicLabel` value forwarded to `Gtk.Expander`.
/// - Parameter expanded: Whether the expander has been opened to reveal the child.
/// - Parameter resizeToplevel: When this property is `true`, the expander will resize the toplevel widget containing the expander upon expanding and collapsing.
/// - Parameter useMarkup: Whether the text in the label is Pango markup.
/// - Parameter useUnderline: Whether an underline in the text indicates a mnemonic.
/// - Parameter child: A `ViewBuilder` closure whose first view is mounted into the `child` slot.
/// - Parameter labelWidget: A `ViewBuilder` closure whose first view is mounted into the `labelWidget` slot.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
public init(withMnemonicLabel: String?, expanded: Bool? = nil, resizeToplevel: Bool? = nil, useMarkup: Bool? = nil, useUnderline: Bool? = nil, @ViewBuilder child: () -> [AnyView] = { [] }, @ViewBuilder labelWidget: () -> [AnyView] = { [] }, onActivate: (() -> Void)? = nil) {
let childViews = child()
let labelWidgetViews = labelWidget()
make = { _ in Gtk.Expander(withMnemonicLabel: withMnemonicLabel) }
configure.append { w, ctx in
if let expanded { w.setExpanded(expanded: expanded) }
if let resizeToplevel { w.setResizeToplevel(resizeToplevel: resizeToplevel) }
if let useMarkup { w.setUseMarkup(useMarkup: useMarkup) }
if let useUnderline { w.setUseUnderline(useUnderline: useUnderline) }
if let v = childViews.first { w.setChild(child: v.makeWidget(ctx)) }
if let v = labelWidgetViews.first { w.setLabelWidget(labelWidget: v.makeWidget(ctx)) }
if let onActivate { ctx.registry.add(w.connectActivate { _ in onActivate() }) }
}
}
}
extension Expander: WidgetView {

View file

@ -46,17 +46,15 @@ import Gdk
/// - Parameter dialog: The `GtkFontDialog` that contains parameters for the font chooser dialog.
/// - Parameter fontDesc: The selected font.
/// - Parameter fontFeatures: The selected font features.
/// - Parameter language: The selected language for font features.
/// - Parameter level: The level of detail for the font chooser dialog.
/// - Parameter useFont: Whether the buttons label will be drawn in the selected font.
/// - Parameter useSize: Whether the buttons label will use the selected font size.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
public init(dialog: Gtk.FontDialog?, fontDesc: Gtk.FontDescription? = nil, fontFeatures: String? = nil, language: Gtk.Language? = nil, level: Gtk.FontLevel? = nil, useFont: Bool? = nil, useSize: Bool? = nil, onActivate: (() -> Void)? = nil) {
public init(dialog: Gtk.FontDialog?, fontDesc: Gtk.FontDescription? = nil, fontFeatures: String? = nil, level: Gtk.FontLevel? = nil, useFont: Bool? = nil, useSize: Bool? = nil, onActivate: (() -> Void)? = nil) {
make = { _ in Gtk.FontDialogButton(dialog: dialog) }
configure.append { w, ctx in
if let fontDesc { w.setFontDesc(fontDesc: fontDesc) }
if let fontFeatures { w.setFontFeatures(fontFeatures: fontFeatures) }
if let language { w.setLanguage(language: language) }
if let level { w.setLevel(level: level) }
if let useFont { w.setUseFont(useFont: useFont) }
if let useSize { w.setUseSize(useSize: useSize) }
@ -294,70 +292,6 @@ extension WidgetView where Target: Gtk.FontDialogButton {
}
}
// PorticoGen: generateModifierExtension -> generatePropertyModifiers(static) | source: Gtk.FontDialogButton.setLanguage(language:)
/// Sets the language to use for font features.
///
/// Applied once at mount; use the `Binding` or closure overload for a value that changes.
///
/// - Parameter language: The selected language for font features.
/// - Returns: A copy of this view with the modifier applied.
public func language(_ language: Gtk.Language?) -> Self {
appending { w, _ in
w.setLanguage(language: language)
}
}
// PorticoGen: generateModifierExtension -> generatePropertyModifiers -> bindingModifier(twoWay) | source: Gtk.FontDialogButton.setLanguage(language:), GObject.Object.connectNotify(detail:_:), Gtk.FontDialogButton.getLanguage()
/// Sets the language to use for font features.
///
/// Applied at mount and re-applied on every change the binding publishes.
/// When `Gtk.Language?` conforms to `Equatable` this binds in both directions: the widget's `notify` signal writes its current value back into the binding, so changes made in the UI propagate to the bound state. Each direction compares before writing, which terminates the echo after one hop. A value type that is not `Equatable` binds one way only, because the echo cannot be broken.
///
/// - Returns: A copy of this view with the modifier applied.
public func language(_ language: Portico.Binding<Gtk.Language?>) -> Self {
appending { w, ctx in
Portico.bindProperty(
w, language, registry: ctx.registry, notifyDetail: "language",
read: { [w] in w.getLanguage() },
write: { [w] v in w.setLanguage(language: v) }
)
}
}
// PorticoGen: generateModifierExtension -> generatePropertyModifiers -> bindingModifier(lifted,twoWay) | source: Gtk.FontDialogButton.setLanguage(language:), GObject.Object.connectNotify(detail:_:), Gtk.FontDialogButton.getLanguage()
/// Sets the language to use for font features.
///
/// Applied at mount and re-applied on every change the binding publishes.
/// `Binding` is invariant, so a `Binding<Gtk.Language>` is not accepted by the nullable overload; this one takes it and promotes each value. Pass a `Binding<Gtk.Language?>` to be able to clear the property.
/// When `Gtk.Language` conforms to `Equatable` this binds in both directions: the widget's `notify` signal writes its current value back into the binding, so changes made in the UI propagate to the bound state. Each direction compares before writing, which terminates the echo after one hop. A value type that is not `Equatable` binds one way only, because the echo cannot be broken.
/// A `nil` widget value is never written back into the binding.
///
/// - Returns: A copy of this view with the modifier applied.
public func language(_ language: Portico.Binding<Gtk.Language>) -> Self {
appending { w, ctx in
Portico.bindProperty(
w, language, registry: ctx.registry, notifyDetail: "language",
read: { [w] in w.getLanguage() },
write: { [w] v in w.setLanguage(language: v) }
)
}
}
// PorticoGen: generateModifierExtension -> generatePropertyModifiers(closure) | source: Gtk.FontDialogButton.setLanguage(language:)
/// Sets the language to use for font features.
///
/// The closure runs inside a `DependencyTracker`, so any `@State` it reads re-runs it and pushes the new value through `Gtk.FontDialogButton.setLanguage(language:)`.
///
/// - Parameter language: The selected language for font features.
/// - Returns: A copy of this view with the modifier applied.
public func language(_ language: @escaping () -> Gtk.Language?) -> Self {
appending { w, ctx in
let tracker = DependencyTracker { [w] in w.setLanguage(language: language()) }
tracker.run()
ctx.registry.add(tracker)
}
}
// PorticoGen: generateModifierExtension -> generatePropertyModifiers(static) | source: Gtk.FontDialogButton.setLevel(level:)
/// Sets the level of detail at which this dialog
/// lets the user select fonts.

View file

@ -294,6 +294,81 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.Label.init(withMnemonicStr:)
/// Creates a new label with the given text inside it, and a mnemonic.
///
/// If characters in `str` are preceded by an underscore, they are
/// underlined. If you need a literal underscore character in a label, use
/// '__' (two underscores). The first underlined character represents a
/// keyboard accelerator called a mnemonic. The mnemonic key can be used
/// to activate another widget, chosen automatically, or explicitly using
/// [method`Gtk`.Label.set_mnemonic_widget].
///
/// If [method`Gtk`.Label.set_mnemonic_widget] is not called, then the first
/// activatable ancestor of the label will be chosen as the mnemonic
/// widget. For instance, if the label is inside a button or menu item,
/// the button or menu item will automatically become the mnemonic widget
/// and be activated by the mnemonic.
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withMnemonicStr: The `withMnemonicStr` value forwarded to `Gtk.Label`.
/// - Parameter attributes: A list of style attributes to apply to the text of the label.
/// - Parameter ellipsize: The preferred place to ellipsize the string, if the label does not have enough room to display the entire string.
/// - Parameter extraMenu: A menu model whose contents will be appended to the context menu.
/// - Parameter justify: The alignment of the lines in the text of the label, relative to each other.
/// - Parameter label: The contents of the label.
/// - Parameter lines: The number of lines to which an ellipsized, wrapping label should display before it gets ellipsized. This both prevents the label from ellipsizing before this many lines are displayed, and limits the height request of the label to this many lines.
/// - Parameter maxWidthChars: The desired maximum width of the label, in characters.
/// - Parameter naturalWrapMode: Select the line wrapping for the natural size request.
/// - Parameter selectable: Whether the label text can be selected with the mouse.
/// - Parameter singleLineMode: Whether the label is in single line mode.
/// - Parameter tabs: Custom tabs for this label.
/// - Parameter useMarkup: True if the text of the label includes Pango markup.
/// - Parameter useUnderline: True if the text of the label indicates a mnemonic with an `_` before the mnemonic character.
/// - Parameter widthChars: The desired width of the label, in characters.
/// - Parameter wrap: True if the label text will wrap if it gets too wide.
/// - Parameter wrapMode: Controls how the line wrapping is done.
/// - Parameter xalign: The horizontal alignment of the label text inside its size allocation.
/// - Parameter yalign: The vertical alignment of the label text inside its size allocation.
/// - Parameter mnemonicWidget: A `ViewBuilder` closure whose first view is mounted into the `mnemonicWidget` slot.
/// - Parameter onActivateCurrentLink: Invoked when the widget emits the `activate-current-link` signal.
/// - Parameter onActivateLink: Invoked when the widget emits the `activate-link` signal. The closure receives the signal's arguments in order. Its return value is forwarded to GTK as the signal's result.
/// - Parameter onCopyClipboard: Invoked when the widget emits the `copy-clipboard` signal.
/// - Parameter onMoveCursor: Invoked when the widget emits the `move-cursor` signal. The closure receives the signal's arguments in order.
public init(withMnemonicStr: String?, attributes: Gtk.AttrList? = nil, ellipsize: Gtk.EllipsizeMode? = nil, extraMenu: Gtk.MenuModel? = nil, justify: Gtk.Justification? = nil, label: String? = nil, lines: Int32? = nil, maxWidthChars: Int32? = nil, naturalWrapMode: Gtk.NaturalWrapMode? = nil, selectable: Bool? = nil, singleLineMode: Bool? = nil, tabs: Gtk.TabArray? = nil, useMarkup: Bool? = nil, useUnderline: Bool? = nil, widthChars: Int32? = nil, wrap: Bool? = nil, wrapMode: Pango.WrapMode? = nil, xalign: Float? = nil, yalign: Float? = nil, @ViewBuilder mnemonicWidget: () -> [AnyView] = { [] }, onActivateCurrentLink: (() -> Void)? = nil, onActivateLink: ((String) -> Bool)? = nil, onCopyClipboard: (() -> Void)? = nil, onMoveCursor: ((Gtk.MovementStep, Int32, Bool) -> Void)? = nil) {
let mnemonicWidgetViews = mnemonicWidget()
make = { _ in Gtk.Label(withMnemonicStr: withMnemonicStr) }
configure.append { w, ctx in
if let attributes { w.setAttributes(attrs: attributes) }
if let ellipsize { w.setEllipsize(mode: ellipsize) }
if let extraMenu { w.setExtraMenu(model: extraMenu) }
if let justify { w.setJustify(jtype: justify) }
if let label { w.setLabel(str: label) }
if let lines { w.setLines(lines: lines) }
if let maxWidthChars { w.setMaxWidthChars(nChars: maxWidthChars) }
if let naturalWrapMode { w.setNaturalWrapMode(wrapMode: naturalWrapMode) }
if let selectable { w.setSelectable(setting: selectable) }
if let singleLineMode { w.setSingleLineMode(singleLineMode: singleLineMode) }
if let tabs { w.setTabs(tabs: tabs) }
if let useMarkup { w.setUseMarkup(setting: useMarkup) }
if let useUnderline { w.setUseUnderline(setting: useUnderline) }
if let widthChars { w.setWidthChars(nChars: widthChars) }
if let wrap { w.setWrap(wrap: wrap) }
if let wrapMode { w.setWrapMode(wrapMode: wrapMode) }
if let xalign { w.setXalign(xalign: xalign) }
if let yalign { w.setYalign(yalign: yalign) }
if let v = mnemonicWidgetViews.first { w.setMnemonicWidget(widget: v.makeWidget(ctx)) }
if let onActivateCurrentLink { ctx.registry.add(w.connectActivateCurrentLink { _ in onActivateCurrentLink() }) }
if let onActivateLink { ctx.registry.add(w.connectActivateLink { _, a0 in onActivateLink(a0) }) }
if let onCopyClipboard { ctx.registry.add(w.connectCopyClipboard { _ in onCopyClipboard() }) }
if let onMoveCursor { ctx.registry.add(w.connectMoveCursor { _, a0, a1, a2 in onMoveCursor(a0, a1, a2) }) }
}
}
}
extension Label: WidgetView {

View file

@ -279,6 +279,43 @@ import Gdk
}
}
// PorticoGen: generateInits(static) | source: Gtk.ToggleButton.init(withMnemonicLabel:)
/// Creates a new `GtkToggleButton` containing a label.
///
/// The label will be created using [ctor`Gtk`.Label.new_with_mnemonic],
/// so underscores in `label` indicate the mnemonic for the button.
///
/// Applied once at mount; use the `Binding` or closure overload for values that change.
/// Each closure is evaluated once; children are added in order and slot closures mount their first view.
/// An empty closure adds no children and leaves slots unset.
/// Optional value parameters are applied only when non-`nil`; a `nil` argument leaves the widget's own default in place and cannot clear a nullable property - use the matching modifier for that.
///
/// - Parameter withMnemonicLabel: The `withMnemonicLabel` value forwarded to `Gtk.ToggleButton`.
/// - Parameter active: If the toggle button should be pressed in.
/// - Parameter canShrink: Whether the size of the button can be made smaller than the natural size of its contents.
/// - Parameter hasFrame: Whether the button has a frame.
/// - Parameter iconName: The name of the icon used to automatically populate the button.
/// - Parameter useUnderline: If set, an underline in the text indicates that the following character is to be used as mnemonic.
/// - Parameter child: A `ViewBuilder` closure whose first view is mounted into the `child` slot.
/// - Parameter onToggled: Invoked when the widget emits the `toggled` signal.
/// - Parameter onActivate: Invoked when the widget emits the `activate` signal.
/// - Parameter onClicked: Invoked when the widget emits the `clicked` signal.
public init(withMnemonicLabel: String, active: Bool? = nil, canShrink: Bool? = nil, hasFrame: Bool? = nil, iconName: String? = nil, useUnderline: Bool? = nil, @ViewBuilder child: () -> [AnyView] = { [] }, onToggled: (() -> Void)? = nil, onActivate: (() -> Void)? = nil, onClicked: (() -> Void)? = nil) {
let childViews = child()
make = { _ in Gtk.ToggleButton(withMnemonicLabel: withMnemonicLabel) }
configure.append { w, ctx in
if let active { w.setActive(isActive: active) }
if let canShrink { w.setCanShrink(canShrink: canShrink) }
if let hasFrame { w.setHasFrame(hasFrame: hasFrame) }
if let iconName { w.setIconName(iconName: iconName) }
if let useUnderline { w.setUseUnderline(useUnderline: useUnderline) }
if let v = childViews.first { w.setChild(child: v.makeWidget(ctx)) }
if let onToggled { ctx.registry.add(w.connectToggled { _ in onToggled() }) }
if let onActivate { ctx.registry.add(w.connectActivate { _ in onActivate() }) }
if let onClicked { ctx.registry.add(w.connectClicked { _ in onClicked() }) }
}
}
}
extension ToggleButton: WidgetView {

View file

@ -32,6 +32,15 @@ public typealias ResponseAppearance = Adw.ResponseAppearance
/// Whether a dialog presents itself as a floating window or a bottom sheet.
public typealias DialogPresentationMode = Adw.DialogPresentationMode
/// A unit used by a breakpoint length condition.
public typealias LengthUnit = Adw.LengthUnit
/// A libadwaita breakpoint condition.
public typealias BreakpointCondition = Adw.BreakpointCondition
/// A libadwaita breakpoint.
public typealias Breakpoint = Adw.Breakpoint
/// The value/range model behind ``SpinRow`` and other range widgets.
///

View file

@ -0,0 +1,82 @@
import Adw
/// A type-safe builder for libadwaita breakpoint conditions.
///
/// Use a condition with ``WidgetView/breakpoint(_:isActive:)`` or one of the
/// typed setter breakpoint modifiers. String literals are parsed by libadwaita.
public enum Condition: ExpressibleByStringLiteral {
/// Matches when the width is at most the supplied value.
case maxWidth(Double, LengthUnit = .px)
/// Matches when the width is at least the supplied value.
case minWidth(Double, LengthUnit = .px)
/// Matches when the height is at most the supplied value.
case maxHeight(Double, LengthUnit = .px)
/// Matches when the height is at least the supplied value.
case minHeight(Double, LengthUnit = .px)
/// Matches when the aspect ratio is at most `width / height`.
case maxAspectRatio(width: Int, height: Int = 1)
/// Matches when the aspect ratio is at least `width / height`.
case minAspectRatio(width: Int, height: Int = 1)
/// Matches when both nested conditions match.
indirect case and(Condition, Condition)
/// Matches when either nested condition matches.
indirect case or(Condition, Condition)
/// Wraps an already constructed libadwaita condition.
case raw(BreakpointCondition)
/// Creates a condition from a libadwaita condition string.
public init(stringLiteral value: String) {
self = .raw(BreakpointCondition.parse(str: value))
}
/// The libadwaita condition represented by this value.
public var condition: BreakpointCondition {
switch self {
case let .maxWidth(value, unit):
return BreakpointCondition(
type: .maxWidth,
value: value,
unit: unit
)
case let .minWidth(value, unit):
return BreakpointCondition(
type: .minWidth,
value: value,
unit: unit
)
case let .maxHeight(value, unit):
return BreakpointCondition(
type: .maxHeight,
value: value,
unit: unit
)
case let .minHeight(value, unit):
return BreakpointCondition(
type: .minHeight,
value: value,
unit: unit
)
case let .maxAspectRatio(width, height):
return BreakpointCondition(
type: .maxAspectRatio,
width: Int32(width),
height: Int32(height)
)
case let .minAspectRatio(width, height):
return BreakpointCondition(
type: .minAspectRatio,
width: Int32(width),
height: Int32(height)
)
case let .and(first, second):
return BreakpointCondition(condition1: first.condition, condition2: second.condition)
case let .or(first, second):
return BreakpointCondition(
orCondition1: first.condition,
orCondition2: second.condition
)
case let .raw(condition):
return condition
}
}
}