Add support for command bars
Some checks failed
Deploy Docs / publish (push) Waiting to run
SwiftLint / SwiftLint (push) Failing after 3s

This commit is contained in:
david-swift 2024-11-03 12:15:33 +01:00
parent 29715462f8
commit c7a0fccde0
13 changed files with 449 additions and 13 deletions

View File

@ -27,17 +27,19 @@ let package = Package(
url: "https://git.aparoksha.dev/aparoksha/levenshtein-transformations",
branch: "main"
),
.package(url: "https://github.com/thebrowsercompany/swift-winui", branch: "main"),
.package(url: "https://github.com/thebrowsercompany/swift-windowsappsdk", branch: "main"),
.package(url: "https://github.com/thebrowsercompany/swift-windowsfoundation", branch: "main")
.package(url: "https://github.com/thebrowsercompany/swift-windowsfoundation", branch: "main"),
.package(url: "https://github.com/thebrowsercompany/swift-cwinrt", branch: "main"),
.package(url: "https://github.com/AparokshaUI/winui", branch: "main")
],
targets: [
.target(
name: "Core",
dependencies: [
.product(name: "WinUI", package: "swift-winui"),
.product(name: "WinUI", package: "winui"),
.product(name: "WinAppSDK", package: "swift-windowsappsdk"),
.product(name: "WindowsFoundation", package: "swift-windowsfoundation"),
.product(name: "CWinRT", package: "swift-cwinrt"),
.product(name: "LevenshteinTransformations", package: "levenshtein-transformations"),
.product(name: "Meta", package: "meta")
]

View File

@ -0,0 +1,71 @@
//
// CommandBarCollection.swift
// WinUI
//
// Created by david-swift on 01.11.2024.
//
import Foundation
import WindowsFoundation
import WinUI
/// A collection of command bar elements.
public struct CommandBarCollection: CommandBarWidget, Wrapper {
/// The content of the collection.
var content: Body
/// Initialize a command bar collection.
/// - Parameter content: The content of the collection.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify the views before updating.
/// - type: The type of the views.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let storages = content.storages(data: data, type: type)
var items: [ICommandBarElement?] = []
getItems(items: &items, storages: storages)
return .init(items, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify the views before updating.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the views.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
/// Get the child items of the collection.
/// - Parameters:
/// - items: Pass the variable that will store the items.
/// - storages: The storages.
func getItems(items: inout [ICommandBarElement?], storages: [ViewStorage]) {
for item in storages {
if let item = item.pointer as? ICommandBarElement {
items.append(item)
} else {
items += item.pointer as? [ICommandBarElement] ?? []
}
}
}
}

View File

@ -0,0 +1,21 @@
//
// CommandBarContext.swift
// WinUI
//
// Created by david-swift on 01.11.24.
//
/// The command bar context.
public enum CommandBarContext: ViewRenderData {
/// The type of the widgets.
public typealias WidgetType = CommandBarWidget
/// The wrapper type.
public typealias WrapperType = CommandBarCollection
/// The either view type.
public typealias EitherViewType = CommandBarEitherView
}
/// The type of the widgets.
public protocol CommandBarWidget: Meta.Widget { }

View File

@ -0,0 +1,23 @@
//
// CommandBarView.swift
// WinUI
//
// Created by david-swift on 01.11.2024.
//
/// Show one of two views depending on a condition.
public struct CommandBarEitherView: Meta.EitherView, SimpleView {
/// The view.
public var view: Body
/// Initialize an either view.
/// - Parameters:
/// - condition: The condition.
/// - view1: The first view.
/// - view2: The second view.
public init(_ condition: Bool, view1: () -> Body, else view2: () -> Body) {
self.view = condition ? view1() : view2()
}
}

View File

@ -8,15 +8,22 @@
import WinUI
/// A button widget for menus.
public struct Separator: MenuWidget {
public struct Separator: MenuWidget, CommandBarWidget {
/// Initialize a separator menu item.
public init() { }
/// Initialize the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
MenuFlyoutSeparator()
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data : ViewRenderData {
if type == CommandBarContext.self {
.init(AppBarSeparator())
} else {
.init(MenuFlyoutSeparator())
}
}
}

View File

@ -0,0 +1,57 @@
//
// AppBarButton.swift
// WinUI
//
// Created by david-swift on 01.11.24.
//
import CWinRT
import WinUI
/// A button.
public struct AppBarButton: WinUIWidget, CommandBarWidget {
/// The action to run when the button gets clicked.
@Property(
set: { button, closure, storage in
if storage.fields["click"] == nil {
button.click.addHandler { _, _ in
(storage.fields["click"] as? () -> Void)?()
}
}
storage.fields["click"] = closure
},
pointer: WinUI.AppBarButton.self
)
var action: (() -> Void)?
/// The label.
@Property(
set: { $0.label = $1 },
pointer: WinUI.AppBarButton.self
)
var label: String?
/// The icon.
@Property(
set: { $0.icon = $1.winIcon },
pointer: WinUI.AppBarButton.self
)
var icon: Icon?
/// Initialize an app bar button.
/// - Parameters:
/// - label: The button's label.
/// - icon: An icon representing the action.
/// - action: The button's action.
public init(_ label: String, icon: Icon, action: @escaping () -> Void) {
self.action = action
self.label = label
self.icon = icon
}
/// Initialize the widget.
/// - Returns: The widget.
public func initializeWidget() -> Any {
WinUI.AppBarButton()
}
}

View File

@ -0,0 +1,105 @@
//
// CommandBar.swift
// WinUI
//
// Created by david-swift on 01.11.24.
//
import CWinRT
import WinUI
/// A command bar.
public struct CommandBar: WinUIWidget {
/// The primary commands.
var primaryCommands: Body
/// The secondary commands.
var secondaryCommands: Body
/// Additional content.
var content: Body = []
/// Whether the labels are positioned right to the icon.
var rightLabelPosition = false
/// Initialize a command bar.
/// - Parameters:
/// - primary: The primary commands.
/// - secondary: The secondary commands.
public init(@ViewBuilder primary: () -> Body, @ViewBuilder secondary: () -> Body = { [] }) {
primaryCommands = primary()
secondaryCommands = secondary()
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
let bar = WinUI.CommandBar()
let storage = ViewStorage(bar)
let primary = CommandBarCollection { primaryCommands }.container(data: data.noModifiers, type: CommandBarContext.self)
storage.content["primary"] = [primary]
if let primary = primary.pointer as? [ICommandBarElement?] {
primary.forEach { bar.primaryCommands.append($0) }
}
let secondary = CommandBarCollection { secondaryCommands }.container(data: data.noModifiers, type: CommandBarContext.self)
storage.content["secondary"] = [secondary]
if let secondary = secondary.pointer as? [ICommandBarElement?] {
secondary.forEach { bar.secondaryCommands.append($0) }
}
let content = content.storage(data: data, type: type)
storage.content[.mainContent] = [content]
bar.content = content.pointer as? UIElement
update(storage, data: data, updateProperties: true, type: type)
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if let content = storage.content["primary"]?.first {
CommandBarCollection { primaryCommands }.updateStorage(
content,
data: data.noModifiers,
updateProperties: updateProperties,
type: CommandBarContext.self
)
}
if let content = storage.content["secondary"]?.first {
CommandBarCollection { secondaryCommands }.updateStorage(
content,
data: data.noModifiers,
updateProperties: updateProperties,
type: CommandBarContext.self
)
}
if let content = storage.content[.mainContent]?.first {
self.content.updateStorage(content, data: data, updateProperties: updateProperties, type: type)
}
if updateProperties, (storage.previousState as? Self)?.rightLabelPosition != rightLabelPosition {
(storage.pointer as? WinUI.CommandBar)?.defaultLabelPosition = rightLabelPosition ? .right : .bottom
storage.previousState = self
}
}
public func content(@ViewBuilder content: () -> Body) -> Self {
modify { $0.content = content() }
}
public func rightLabelPosition(_ enabled: Bool = true) -> Self {
modify { $0.rightLabelPosition = enabled }
}
}

View File

@ -1,5 +1,5 @@
//
// VStack.swift
// Button.swift
// WinUI
//
// Created by david-swift on 11.10.24.

View File

@ -0,0 +1,64 @@
//
// VStack.swift
// WinUI
//
// Created by david-swift on 11.10.24.
//
import WinUI
/// A container arranging its children vertically.
public struct HStack: WinUIWidget {
/// The content.
var content: Body
/// Initialize the wrapper.
/// - Parameter content: The view content.
public init(@ViewBuilder content: () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - data: The widget data.
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(
data: WidgetData,
type: Data.Type
) -> ViewStorage where Data: ViewRenderData {
if content.count == 1, let view = content.first {
return view.storage(data: data, type: type)
}
let stack = StackPanel()
stack.orientation = .horizontal
let storages = content.storages(data: data, type: type)
for storage in storages {
stack.children.append(storage.pointer as? UIElement)
}
return .init(stack, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - data: The widget data.
/// - updateProperties: Whether to update the view's properties.
/// - type: The view render data type.
public func update<Data>(
_ storage: ViewStorage,
data: WidgetData,
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
if content.count == 1, let view = content.first {
view.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
return
}
if let storages = storage.content[.mainContent] {
content.update(storages, data: data, updateProperties: updateProperties, type: type)
}
}
}

View File

@ -8,7 +8,7 @@
import WinUI
/// A wrapper view for modifiers.
public struct ModifierWrapper: WinUIWidget {
public struct ModifierWrapper: WinUIWidget, CommandBarWidget {
/// The content view.
var view: AnyView
@ -26,6 +26,8 @@ public struct ModifierWrapper: WinUIWidget {
var verticalAlignment: VerticalAlignment?
/// The grid column.
var gridColumn: Int?
/// Whether the element is displayed.
var visible: Bool?
// swiftlint:disable large_tuple
/// Initialize the modifier wrapper.
@ -37,6 +39,7 @@ public struct ModifierWrapper: WinUIWidget {
/// - horizontalAlignment: The horizontal alignment.
/// - verticalAlignment: The vertical alignment.
/// - gridColumn: The grid column.
/// - visible: Whether the view is visible.
public init(
view: AnyView,
width: Double? = nil,
@ -44,7 +47,8 @@ public struct ModifierWrapper: WinUIWidget {
margin: (Double, Double, Double, Double)? = nil,
horizontalAlignment: HorizontalAlignment? = nil,
verticalAlignment: VerticalAlignment? = nil,
gridColumn: Int? = nil
gridColumn: Int? = nil,
visible: Bool? = nil
) {
self.view = view
self.width = width
@ -53,6 +57,7 @@ public struct ModifierWrapper: WinUIWidget {
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.gridColumn = gridColumn
self.visible = visible
}
// swiftlint:enable large_tuple
@ -62,7 +67,9 @@ public struct ModifierWrapper: WinUIWidget {
/// - type: The view render data type.
/// - Returns: The view storage.
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
let storage = view.storage(data: data, type: type)
let content = view.storage(data: data, type: type)
let storage = ViewStorage(content.pointer)
storage.content[.mainContent] = [content]
update(storage, data: data, updateProperties: true, type: type)
return storage
}
@ -79,7 +86,9 @@ public struct ModifierWrapper: WinUIWidget {
updateProperties: Bool,
type: Data.Type
) where Data: ViewRenderData {
view.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
if let storage = storage.content[.mainContent]?.first {
view.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
}
guard updateProperties, let view = storage.pointer as? WinUI.FrameworkElement else {
return
}
@ -102,6 +111,9 @@ public struct ModifierWrapper: WinUIWidget {
if let gridColumn, previousState?.gridColumn != gridColumn {
WinUI.Grid.setColumn(view, .init(gridColumn))
}
if let visible, previousState?.visible != visible {
view.visibility = visible ? .visible : .collapsed
}
storage.previousState = self
}

View File

@ -24,6 +24,32 @@ public struct NavigationView<Item>: WinUIWidget where Item: NavigationViewItem {
var paneCustomContent: Body?
/// The display mode of the navigation view.
var mode: NavigationViewMode = .sidebar
/// The primary commands in the header.
var primaryCommands: Body = []
/// The secondary commands in the header.
var secondaryCommands: Body = []
/// The label for the settings item.
var settingsLabel: String?
/// The view's header.
var header: AnyView? {
if let settingsLabel {
Grid {
Text(selectedItem.description ?? settingsLabel)
ModifierWrapper(
view: CommandBar {
primaryCommands
} secondary: {
secondaryCommands
},
margin: (0, 0, 50, 0),
horizontalAlignment: .right
)
}
} else {
nil
}
}
/// Initialize the navigation view without the settings item.
/// - Parameters:
@ -62,6 +88,16 @@ public struct NavigationView<Item>: WinUIWidget where Item: NavigationViewItem {
/// A custom item.
case custom(item: Item)
/// The item's description.
var description: String? {
switch self {
case .settings:
nil
case let .custom(item):
item.description
}
}
}
/// The view storage.
@ -86,6 +122,10 @@ public struct NavigationView<Item>: WinUIWidget where Item: NavigationViewItem {
storage.content["pane-custom-content"] = [customStorage]
view.paneCustomContent = customStorage.pointer as? UIElement
}
if let header = header?.storage(data: data, type: type) {
view.header = header.pointer
storage.content["header"] = [header]
}
if settings {
view.isSettingsVisible = true
} else {
@ -113,6 +153,9 @@ public struct NavigationView<Item>: WinUIWidget where Item: NavigationViewItem {
if let content = storage.content["pane-custom-content"]?.first {
paneCustomContent?.updateStorage(content, data: data, updateProperties: updateProperties, type: type)
}
if let content = storage.content["header"]?.first {
header?.updateStorage(content, data: data, updateProperties: updateProperties, type: type)
}
guard let navigationView = storage.pointer as? WinUI.NavigationView else {
return
}
@ -163,6 +206,24 @@ public struct NavigationView<Item>: WinUIWidget where Item: NavigationViewItem {
modify { $0.paneCustomContent = content() }
}
/// The view's header.
/// - Parameters:
/// - settingsLabel: The label for the settings item.
/// - primary: The primary command bar items.
/// - secondary: The secondary command bar items.
/// - Returns:
public func header(
settingsLabel: String,
@ViewBuilder primary: () -> Body,
@ViewBuilder secondary: () -> Body = { [] }
) -> Self {
modify { view in
view.settingsLabel = settingsLabel
view.primaryCommands = primary()
view.secondaryCommands = secondary()
}
}
}
/// The navigation selection type.

View File

@ -52,6 +52,12 @@ struct ContentView: View {
.verticalAlignment(.center)
}
}
.header(settingsLabel: "Settings") {
AppBarButton("Delete", icon: .systemIcon(unicode: "\u{E74D}")) {
print("Delete")
}
.visible(selectedItem != .shop)
}
}
@ViewBuilder var menu: Body {

View File

@ -53,4 +53,11 @@ extension AnyView {
ModifierWrapper(view: self, gridColumn: column)
}
/// Whether the view is visible.
/// - Parameter visible: Whether the view is visible.
/// - Returns: The view.
public func visible(_ visible: Bool? = true) -> AnyView {
ModifierWrapper(view: self, visible: visible)
}
}