Add support for toolbar buttons
Some checks failed
SwiftLint / SwiftLint (push) Failing after 4s

This commit is contained in:
david-swift 2024-11-06 11:51:02 +01:00
parent 3cee2ee2b2
commit e18797a8d9
7 changed files with 179 additions and 61 deletions

View File

@ -16,8 +16,6 @@ opt_in_rules:
- convenience_type
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
- discouraged_optional_collection
- empty_collection_literal
- empty_count
- empty_string

Binary file not shown.

Binary file not shown.

View File

@ -5,6 +5,7 @@
// Created by david-swift on 06.11.24.
//
import Core
import Foundation
/// Information about the app.
@ -27,4 +28,26 @@ struct AppInformation {
/// The settings label.
var settings: String
#if ADWAITA
/// The main menu for GNOME.
/// - Parameter showAbout: Whether to show the about dialog.
/// - Returns: The view.
func menu(showAbout: Binding<Bool>) -> AnyView {
AboutDialog(
visible: showAbout,
child: Menu(icon: .default(icon: .openMenu)) {
MenuButton(about) {
showAbout.wrappedValue.toggle()
}
},
appName: name,
developer: developer,
version: version,
icon: icon?.nativeIcon,
website: website,
issues: issues
)
}
#endif
}

View File

@ -24,6 +24,8 @@ public struct Window: AparokshaSceneElement {
var width: Binding<Int>?
/// The window's height.
var height: Binding<Int>?
/// The toolbar items.
var toolbarItems: [ToolbarItem] = []
/// Initialize a window.
/// - Parameters:
@ -98,6 +100,44 @@ public struct Window: AparokshaSceneElement {
return newSelf
}
/// Add a leading toolbar item.
/// - Parameters:
/// - title: The item's label.
/// - icon: The item's icon.
/// - disabled: Whether the item is disabled.
/// - action: The item's action.
public func leadingToolbarItem(
_ title: String,
icon: Icon,
disabled: Bool = false,
action: @escaping () -> Void
) -> Self {
var newSelf = self
newSelf.toolbarItems.append(
.init(title: title, icon: icon, action: action, leftAlign: true, active: !disabled)
)
return newSelf
}
/// Add a trailing toolbar item.
/// - Parameters:
/// - title: The item's label.
/// - icon: The item's icon.
/// - disabled: Whether the item is disabled.
/// - action: The item's action.
public func trailingToolbarItem(
_ title: String,
icon: Icon,
disabled: Bool = false,
action: @escaping () -> Void
) -> Self {
var newSelf = self
newSelf.toolbarItems.append(
.init(title: title, icon: icon, action: action, leftAlign: false, active: !disabled)
)
return newSelf
}
/// Set and observe the window's width and height.
/// - Parameters:
/// - width: The window's width.
@ -123,7 +163,7 @@ public struct Window: AparokshaSceneElement {
/// The native window.
func nativeWindow(app: Aparoksha) -> Core.Window {
.init(id: id, open: open) { _ in
contentView(app: app)
WindowMainView(content: content, defaultTitlebar: defaultTitlebar, app: app, toolbarItems: toolbarItems)
}
.title(title)
.size(width: width, height: height)
@ -132,16 +172,35 @@ public struct Window: AparokshaSceneElement {
}
#endif
/// The full content, including the correct environment data.
/// - Parameter app: The app.
/// - Returns: The content.
func fullContent(app: Aparoksha) -> AnyView {
content
.environment("info", data: app.appInformation)
}
}
/// The content view.
func contentView(app: Aparoksha) -> AnyView {
/// The window's content.
struct WindowMainView: View {
/// Whether to show the about dialog on GNOME.
@State private var showAbout = false
/// The window's content.
var content: Body
/// Whether to show the default title bar.
var defaultTitlebar: Bool
/// The app.
var app: Aparoksha
/// The toolbar items.
var toolbarItems: [ToolbarItem]
#if ADWAITA
/// The toolbar buttons.
var buttons: [(AnyView, left: Bool)] {
var body: [(AnyView, left: Bool)] = []
for button in toolbarItems {
body.append((button.button, left: button.leftAlign))
}
return body
}
#endif
/// The main view.
var view: Body {
#if WINUI
fullContent(app: app)
#else
@ -153,10 +212,51 @@ public struct Window: AparokshaSceneElement {
fullContent(app: app)
}
.top {
HeaderBar.empty()
HeaderBar {
buttons.filter { $0.left }.map { $0.0 }
} end: {
app.appInformation.menu(showAbout: $showAbout)
buttons.filter { !$0.left }.map { $0.0 }.reversed()
}
}
}
#endif
}
/// The full content, including the correct environment data.
/// - Parameter app: The app.
/// - Returns: The content.
func fullContent(app: Aparoksha) -> AnyView {
content
.environment("info", data: app.appInformation)
.environment("toolbar", data: toolbarItems)
}
}
/// The toolbar item.
struct ToolbarItem {
/// The item's title.
var title: String
/// The item's icon.
var icon: Icon
/// The item's action.
var action: () -> Void
/// Whether the item is left aligned.
var leftAlign: Bool
/// Whether the item is active.
var active: Bool
#if ADWAITA
/// The Adwaita button.
var button: AnyView {
ModifierWrapper(
content: Core.Button(icon: icon.nativeIcon, handler: action),
insensitive: !active,
tooltip: title
)
}
#endif
}

View File

@ -29,68 +29,34 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
var content: Body
/// Whether to force the sidebar layout.
var forceSidebar = false
// swiftlint:disable large_tuple
/// The primary action.
var primaryActionProperties: (label: String, icon: Icon, closure: () -> Void)?
// swiftlint:enable large_tuple
var primaryActionProperties: ToolbarItem?
/// Whether to use the view switcher layout.
var useViewSwitcher: Bool {
!forceSidebar && items.count <= 5
}
#if ADWAITA
/// The main menu for GNOME.
var menu: AnyView {
AboutDialog(
visible: $showAbout,
child: Menu(icon: .default(icon: .openMenu)) {
MenuButton(info?.about ?? "About") {
showAbout.toggle()
}
},
appName: info?.name,
developer: info?.developer,
version: info?.version,
icon: info?.icon?.nativeIcon,
website: info?.website,
issues: info?.issues
)
}
#endif
/// The view.
public var view: Body {
#if WINUI
winNavigationView
#else
let primaryActionView: Body = if let primaryActionProperties {
[
ModifierWrapper(
content: Core.Button(
icon: primaryActionProperties.icon.nativeIcon
) { primaryActionProperties.closure() },
tooltip: primaryActionProperties.label
)
]
} else {
[]
}
if useViewSwitcher {
AdwViewSwitcher(
selectedItem: $selectedItem,
content: content,
items: items,
menu: menu,
primaryAction: primaryActionView
menu: info?.menu(showAbout: $showAbout) ?? [],
primaryAction: primaryActionProperties?.button ?? []
)
} else {
AdwNavigationSplitView(
selectedItem: $selectedItem,
content: content,
items: items,
menu: menu,
primaryAction: primaryActionView
menu: info?.menu(showAbout: $showAbout) ?? [],
primaryAction: primaryActionProperties?.button ?? []
)
}
#endif
@ -158,14 +124,27 @@ public struct FlatNavigation<Item>: View where Item: FlatNavigationItem {
}
/// The primary action, displayed in the top-left corner.
/// - Parameter content: The view.
/// - Parameters:
/// - label: The button's label.
/// - icon: The button's icon.
/// - disabled: Whether the button is inactive.
/// - closure: The closure.
/// - Returns: The flat navigation view.
public func primaryAction(
_ label: String,
icon: Icon,
disabled: Bool = false,
closure: @escaping () -> Void
) -> Self {
modify { $0.primaryActionProperties = (label: label, icon: icon, closure: closure) }
modify { navigation in
navigation.primaryActionProperties = .init(
title: label,
icon: icon,
action: closure,
leftAlign: true,
active: !disabled
)
}
}
}
@ -208,7 +187,7 @@ struct AdwViewSwitcher<Item>: View where Item: FlatNavigationItem {
/// The main menu.
var menu: AnyView
/// The primary action view.
var primaryAction: Body
var primaryAction: AnyView
/// The view body.
var view: Body {
@ -273,6 +252,9 @@ struct AdwNavigationSplitView<Item>: View where Item: FlatNavigationItem {
@State private var width = 0
/// The currently selected item.
@Binding var selectedItem: Item
/// The toolbar items.
@Environment("toolbar")
var toolbarItems: [ToolbarItem]?
/// The content view.
var content: Body
/// The navigation items.
@ -280,21 +262,30 @@ struct AdwNavigationSplitView<Item>: View where Item: FlatNavigationItem {
/// The menu.
var menu: AnyView
/// The primary action.
var primaryAction: Body
var primaryAction: AnyView
/// The minimum width before toggling to the mobile layout.
var minWidth: Int {
width < 200 ? 450 : width + 250
}
#if ADWAITA
/// The Adwaita toolbar buttons.
var buttons: [(AnyView, left: Bool)] {
var body: [(AnyView, left: Bool)] = []
for button in toolbarItems ?? [] {
body.append((button.button, left: button.leftAlign))
}
return body
}
#endif
/// The view body.
var view: Body {
BreakpointBin(condition: .minWidth(minWidth), matches: $wide) {
NavigationSplitView {
ToolbarView()
.content {
sidebar
}
.content { sidebar }
.top {
HeaderBar {
primaryAction
@ -309,8 +300,11 @@ struct AdwNavigationSplitView<Item>: View where Item: FlatNavigationItem {
content
}
.top {
HeaderBar
.empty()
HeaderBar {
buttons.filter { $0.left }.map { $0.0 }
} end: {
buttons.filter { !$0.left }.map { $0.0 }.reversed()
}
}
.inspectOnAppear { storage in
inspectContent(storage: storage)

View File

@ -31,6 +31,9 @@ struct Demo: App {
ContentView()
}
.hideDefaultTitlebar()
.leadingToolbarItem("Airplane", icon: .airplane) {
print("Airplane")
}
.frame(width: $width, height: $height)
}