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
5 changes: 4 additions & 1 deletion 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,13 +205,15 @@ struct WindowingApp: App {
CommandMenu("Demo menu") {
Button("Menu item") {}

Toggle("Toggle", active: $toggle)

Menu("Submenu") {
Button("Item 1") {}
Button("Item 2") {}
}
}
}
#if !os(iOS) && !os(tvOS)
#if !(os(iOS) || os(tvOS) || os(Windows))
WindowGroup("Secondary window") {
#hotReloadable {
Text("This a secondary window!")
Expand Down
23 changes: 23 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,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 @@ -1910,6 +1928,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
3 changes: 3 additions & 0 deletions Sources/Gtk3Backend/Gtk3Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,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
3 changes: 3 additions & 0 deletions Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
return 0
#else
if window.showMenuBar {
// TODO: Don't hardcode this (if possible), because some Gtk

Check warning on line 166 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 +179,7 @@
}

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

Check warning on line 182 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 +279,9 @@
}

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

Check warning on line 283 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 +386,14 @@
}

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

Check warning on line 389 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 396 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 +401,7 @@
of window: Window,
to action: @escaping () -> Void
) {
// TODO: Notify when window scale factor changes

Check warning on line 404 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 +686,7 @@

// private let tables = Tables()

// TODO: Implement tables

Check warning on line 689 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 @@ -1176,7 +1179,7 @@
}
},
window: window ?? windows[0]
) { result in

Check warning on line 1182 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 +1212,7 @@
configure(chooser)

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

Check warning on line 1215 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 (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 (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 (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'

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 (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'
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 }
}
}
9 changes: 9 additions & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ 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 { [weak widget] _, _ in
guard let widget else { return }
onChange(widget.isChecked)
}
return widget
case .submenu(let submenu):
let widget = MenuFlyoutSubItem()
widget.text = submenu.label
Expand Down
Loading