Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions Examples/Sources/WindowingExample/WindowingApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ struct SheetDemo: View {
struct WindowingApp: App {
@State var title = "My window"
@State var resizable = false
@State var toggle = false

var body: some Scene {
WindowGroup(title) {
Expand Down Expand Up @@ -204,6 +205,8 @@ struct WindowingApp: App {
CommandMenu("Demo menu") {
Button("Menu item") {}

Toggle("Toggle", active: $toggle)

Menu("Submenu") {
Button("Item 1") {}
Button("Item 2") {}
Expand Down
23 changes: 23 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,24 @@ public final class AppKitBackend: AppBackend {
renderedItem.action = #selector(wrappedAction.run)
renderedItem.target = wrappedAction
}
return renderedItem
case .toggle(let label, let value, let onChange):
// Custom subclass is used to keep strong reference to action
// wrapper.
let renderedItem = NSCustomMenuItem(
title: label,
action: nil,
keyEquivalent: ""
)
renderedItem.isOn = value

let wrappedAction = Action {
onChange(!renderedItem.isOn)
}
renderedItem.actionWrapper = wrappedAction
renderedItem.action = #selector(wrappedAction.run)
renderedItem.target = wrappedAction

return renderedItem
case .submenu(let submenu):
return renderSubmenu(submenu)
Expand Down Expand Up @@ -1928,6 +1946,11 @@ final class NSCustomMenuItem: NSMenuItem {
/// This property's only purpose is to keep a strong reference to the wrapped
/// action so that it sticks around for long enough to be useful.
var actionWrapper: Action?

var isOn: Bool {
get { state == .on }
set { state = newValue ? .on : .off }
}
}

// TODO: Update all controls to use this style of action passing, seems way nicer
Expand Down
23 changes: 22 additions & 1 deletion Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import CGtk3
import Foundation
import Gtk3
import Logging
import SwiftCrossUI

/// The storage behind `logger`.
///
/// `nil` if the logger hasn't been set yet.
///
/// > Safety: This is only set once, before it is ever read.
nonisolated(unsafe) private var _logger: Logger?

/// The global logger for this backend.
var logger: Logger {
guard let _logger else { fatalError("logger not yet initialized") }
return _logger
}

extension App {
public typealias Backend = Gtk3Backend

public var backend: Gtk3Backend {
Gtk3Backend(appIdentifier: Self.metadata?.identifier)
_logger = Logger(
label: "Gtk3Backend",
factory: Self.logHandler(label:metadataProvider:)
)
return Gtk3Backend(appIdentifier: Self.metadata?.identifier)
}
}

Expand Down Expand Up @@ -244,6 +262,9 @@ public final class Gtk3Backend: AppBackend {
}

model.appendItem(label: label, actionName: "\(actionNamespace).\(actionName)")
case .toggle(let label, let value, let onChange):
// FIXME: Implement
logger.warning("menu toggles not implemented")
case .submenu(let submenu):
model.appendSubmenu(
label: submenu.label,
Expand Down
23 changes: 22 additions & 1 deletion Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import CGtk
import Foundation
import Gtk
import Logging
import SwiftCrossUI

/// The storage behind `logger`.
///
/// `nil` if the logger hasn't been set yet.
///
/// > Safety: This is only set once, before it is ever read.
nonisolated(unsafe) private var _logger: Logger?

/// The global logger for this backend.
var logger: Logger {
guard let _logger else { fatalError("logger not yet initialized") }
return _logger
}

extension App {
public typealias Backend = GtkBackend

public var backend: GtkBackend {
GtkBackend(appIdentifier: Self.metadata?.identifier)
_logger = Logger(
label: "GtkBackend",
factory: Self.logHandler(label:metadataProvider:)
)
return GtkBackend(appIdentifier: Self.metadata?.identifier)
}
}

Expand Down Expand Up @@ -163,7 +181,7 @@
return 0
#else
if window.showMenuBar {
// TODO: Don't hardcode this (if possible), because some Gtk

Check warning on line 184 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Don't hardcode this (if possib...) (todo)
// themes may affect the height of the menu bar.
25
} else {
Expand All @@ -179,7 +197,7 @@
}

public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool {
// TODO: Detect whether window is fullscreen

Check warning on line 200 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Detect whether window is fulls...) (todo)
return true
}

Expand Down Expand Up @@ -279,6 +297,9 @@
}

model.appendItem(label: label, actionName: "\(actionNamespace).\(actionName)")
case .toggle(let label, let value, let onChange):
// FIXME: Implement

Check warning on line 301 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: FIXMEs should be resolved (Implement) (todo)
logger.warning("menu toggles not implemented")
case .submenu(let submenu):
model.appendSubmenu(
label: submenu.label,
Expand Down Expand Up @@ -383,14 +404,14 @@
}

public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) {
// TODO: React to theme changes

Check warning on line 407 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (React to theme changes) (todo)
}

public func computeWindowEnvironment(
window: Window,
rootEnvironment: EnvironmentValues
) -> EnvironmentValues {
// TODO: Record window scale factor in here

Check warning on line 414 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Record window scale factor in ...) (todo)
rootEnvironment
}

Expand All @@ -398,7 +419,7 @@
of window: Window,
to action: @escaping () -> Void
) {
// TODO: Notify when window scale factor changes

Check warning on line 422 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Notify when window scale facto...) (todo)
}

public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
Expand Down Expand Up @@ -683,7 +704,7 @@

// private let tables = Tables()

// TODO: Implement tables

Check warning on line 707 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Implement tables) (todo)
// public func createTable(rows: Int, columns: Int) -> Widget {
// let widget = Grid()

Expand Down Expand Up @@ -760,7 +781,7 @@
environment: EnvironmentValues,
action: @escaping () -> Void
) {
// TODO: Update button label color using environment

Check warning on line 784 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Todo Violation: TODOs should be resolved (Update button label color usin...) (todo)
let button = button as! Gtk.Button
button.sensitive = environment.isEnabled
button.label = label
Expand Down Expand Up @@ -1176,7 +1197,7 @@
}
},
window: window ?? windows[0]
) { result in

Check warning on line 1200 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
switch result {
case .success(let urls):
handleResult(.success(urls[0]))
Expand Down Expand Up @@ -1209,7 +1230,7 @@
configure(chooser)

chooser.registerSignals()
chooser.response = { (_: NativeDialog, response: Int) -> Void in

Check warning on line 1233 in Sources/GtkBackend/GtkBackend.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Redundant Void Return Violation: Returning Void in a function declaration is redundant (redundant_void_return)
// Release our intentional retain cycle which ironically only exists
// because of this line. The retain cycle keeps the file chooser
// around long enough for the user to respond (it gets released
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftCrossUI/Backend/ResolvedMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public struct ResolvedMenu {
public enum Item {
/// A button. A `nil` action means that the button is disabled.
case button(_ label: String, _ action: (@MainActor () -> Void)?)
/// A toggle that manages boolean state.
///
/// Usually appears as a checkbox.
/// - Parameters:
/// - label: The toggle's label.
/// - value: The toggle's current state.
/// - onChange: Called whenever the user changes the toggle's state.
case toggle(_ label: String, _ value: Bool, onChange: @MainActor (Bool) -> Void)
/// A named submenu.
case submenu(Submenu)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public struct MenuItemsBuilder {
[.text(first)]
}

public static func buildPartialBlock(first: Toggle) -> [MenuItem] {
[.toggle(first)]
}

public static func buildPartialBlock(first: Menu) -> [MenuItem] {
[.submenu(first)]
}
Expand Down Expand Up @@ -41,6 +45,13 @@ public struct MenuItemsBuilder {
accumulated + buildPartialBlock(first: next)
}

public static func buildPartialBlock(
accumulated: [MenuItem],
next: Toggle
) -> [MenuItem] {
accumulated + buildPartialBlock(first: next)
}

public static func buildPartialBlock(
accumulated: [MenuItem],
next: Menu
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftCrossUI/Scenes/CommandMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public struct CommandMenu: Sendable {
}

/// Resolves the menu to a representation used by backends.
@MainActor
func resolve() -> ResolvedMenu.Submenu {
ResolvedMenu.Submenu(
label: name,
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftCrossUI/Scenes/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public struct Commands: Sendable {
}

/// Resolves the menus to a representation used by backends.
@MainActor
func resolve() -> [ResolvedMenu.Submenu] {
menus.map { menu in
menu.resolve()
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftCrossUI/Views/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public struct Menu: Sendable {
}

/// Resolves the menu to a representation used by backends.
@MainActor
func resolve() -> ResolvedMenu.Submenu {
ResolvedMenu.Submenu(
label: label,
Expand All @@ -22,6 +23,7 @@ public struct Menu: Sendable {
}

/// Resolves the menu's items to a representation used by backends.
@MainActor
static func resolveItems(_ items: [MenuItem]) -> ResolvedMenu {
ResolvedMenu(
items: items.map { item in
Expand All @@ -30,6 +32,12 @@ public struct Menu: Sendable {
.button(button.label, button.action)
case .text(let text):
.button(text.string, nil)
case .toggle(let toggle):
.toggle(
toggle.label,
toggle.active.wrappedValue,
onChange: { toggle.active.wrappedValue = $0 }
)
case .submenu(let submenu):
.submenu(submenu.resolve())
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftCrossUI/Views/MenuItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
public enum MenuItem: Sendable {
case button(Button)
case text(Text)
case toggle(Toggle)

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit-catalyst

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit-catalyst

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (TV)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (TV)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (TV)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPad)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'

Check warning on line 5 in Sources/SwiftCrossUI/Views/MenuItem.swift

View workflow job for this annotation

GitHub Actions / uikit (iPad)

associated value 'toggle' of 'Sendable'-conforming enum 'MenuItem' has non-sendable type 'Toggle'
case submenu(Menu)
}
11 changes: 11 additions & 0 deletions Sources/UIKitBackend/UIKitBackend+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ extension UIKitBackend {
} else {
UIAction(title: label, attributes: .disabled) { _ in }
}
case .toggle(let label, let value, let onChange):
UIAction(title: label, state: value ? .on : .off) { action in
onChange(!action.state.isOn)
}
case .submenu(let submenu):
buildMenu(content: submenu.content, label: submenu.label)
}
Expand Down Expand Up @@ -70,3 +74,10 @@ extension UIKitBackend {
#endif
}
}

extension UIMenuElement.State {
var isOn: Bool {
get { self == .on }
set { self = newValue ? .on : .off }
}
}
12 changes: 12 additions & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ public final class WinUIBackend: AppBackend {
action?()
}
return widget
case .toggle(let label, let value, let onChange):
let widget = ToggleMenuFlyoutItem()
widget.text = label
widget.isChecked = value
widget.click.addHandler { widget, _ in
guard let widget = widget as? ToggleMenuFlyoutItem else {
return
}
widget.isChecked.toggle()
onChange(widget.isChecked)
}
return widget
case .submenu(let submenu):
let widget = MenuFlyoutSubItem()
widget.text = submenu.label
Expand Down
Loading