Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
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 (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-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'
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 }
}
}
8 changes: 8 additions & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ 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 { _, _ in
onChange(widget.isChecked)
}
return widget
case .submenu(let submenu):
let widget = MenuFlyoutSubItem()
widget.text = submenu.label
Expand Down
Loading