Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
24 changes: 24 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,25 @@ 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 {
renderedItem.isOn.toggle()
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 +1947,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
9 changes: 9 additions & 0 deletions Sources/SwiftCrossUI/Backend/ResolvedMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ 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 enabled 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 (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-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 (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'
case submenu(Menu)
}
12 changes: 12 additions & 0 deletions Sources/UIKitBackend/UIKitBackend+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ 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
action.state.isOn.toggle()
onChange(action.state.isOn)
}
case .submenu(let submenu):
buildMenu(content: submenu.content, label: submenu.label)
}
Expand Down Expand Up @@ -70,3 +75,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