diff --git a/Sources/Core/View/Card.swift b/Sources/Core/View/Card.swift index c8b4318..8e92e3d 100644 --- a/Sources/Core/View/Card.swift +++ b/Sources/Core/View/Card.swift @@ -18,6 +18,8 @@ public struct Card: WinUIWidget, Wrapper { context: WinUIMainView.self ) var content: Body + /// Whether it is a settings card. + var settingsCard = false /// Initialize the wrapper. /// - Parameter content: The view content. @@ -29,7 +31,8 @@ public struct Card: WinUIWidget, Wrapper { /// - Returns: The widget. public func initializeWidget() -> Any { let stack = WinUI.Grid() - stack.cornerRadius = .init(topLeft: 8, topRight: 8, bottomRight: 8, bottomLeft: 8) + let radius: Double = settingsCard ? 4 : 8 + stack.cornerRadius = .init(topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius) stack.padding = .init(left: 12, top: 12, right: 12, bottom: 12) stack.borderThickness = .init(left: 1, top: 1, right: 1, bottom: 1) stack.borderBrush = Application.current.resources.lookup("CardStrokeColorDefaultBrush") as? Brush @@ -37,4 +40,10 @@ public struct Card: WinUIWidget, Wrapper { return stack } + /// Use the settings style. + /// - Returns: The card. + public func settings() -> Self { + modify { $0.settingsCard = true } + } + } diff --git a/Sources/Core/View/Controls/ComboBox.swift b/Sources/Core/View/Controls/ComboBox.swift new file mode 100644 index 0000000..747d0b1 --- /dev/null +++ b/Sources/Core/View/Controls/ComboBox.swift @@ -0,0 +1,66 @@ +// +// ToggleSwitch.swift +// WinUI +// +// Created by david-swift on 06.11.24. +// + +import LevenshteinTransformations +import WinUI + +/// The combo box widget. +public struct ComboBox: WinUIWidget where Element: CustomStringConvertible { + + /// The selected element. + @BindingProperty( + observe: { box, selection, storage in + box.selectionChanged.addHandler { _, _ in + selection.wrappedValue = (storage.previousState as? Self)?.elements[safe: .init(box.selectedIndex)] + } + }, + set: { box, value, _ in + if let value { + box.selectedItem = value.description + } + }, + pointer: WinUI.ComboBox.self + ) + var selection: Meta.Binding = .constant(nil) + /// The elements. + @Property( + set: { box, newValue, storage in + ((storage.previousState as? Self)?.elements ?? []).map { $0.description }.transform( + to: newValue.map { $0.description }, + functions: .init { index in + box.items.removeAt(.init(index)) + } insert: { index, element in + box.items.insertAt(.init(index), element) + } + ) + }, + pointer: WinUI.ComboBox.self + ) + var elements: [Element] = [] + + /// Initialize a combo box. + /// - Parameters: + /// - elements: The elements. + /// - selection: The selected element. + public init(_ elements: [Element], selection: Meta.Binding) { + self.selection = .init { + selection.wrappedValue + } set: { newValue in + if let newValue { + selection.wrappedValue = newValue + } + } + self.elements = elements + } + + /// Initialize the widget. + /// - Returns: The widget. + public func initializeWidget() -> Any { + WinUI.ComboBox() + } + +} \ No newline at end of file diff --git a/Sources/Core/View/Controls/ToggleSwitch.swift b/Sources/Core/View/Controls/ToggleSwitch.swift new file mode 100644 index 0000000..83ea122 --- /dev/null +++ b/Sources/Core/View/Controls/ToggleSwitch.swift @@ -0,0 +1,37 @@ +// +// ToggleSwitch.swift +// WinUI +// +// Created by david-swift on 06.11.24. +// + +import WinUI + +/// The toggle switch widget. +public struct ToggleSwitch: WinUIWidget { + + /// Whether the switch is active. + @BindingProperty( + observe: { view, binding in + view.toggled.addHandler { _, _ in + binding.wrappedValue = view.isOn + } + }, + set: { $0.isOn = $1 }, + pointer: WinUI.ToggleSwitch.self + ) + var isOn: Meta.Binding = .constant(false) + + /// Initialize the toggle switch. + /// - Parameter isOn: Whether the switch is active. + public init(isOn: Meta.Binding) { + self.isOn = isOn + } + + /// Initialize the widget. + /// - Returns: The widget. + public func initializeWidget() -> Any { + WinUI.ToggleSwitch() + } + +} diff --git a/Sources/Core/View/HyperlinkButton.swift b/Sources/Core/View/HyperlinkButton.swift new file mode 100644 index 0000000..3ae585e --- /dev/null +++ b/Sources/Core/View/HyperlinkButton.swift @@ -0,0 +1,42 @@ +// +// HyperlinkButton.swift +// WinUI +// +// Created by david-swift on 07.11.24. +// + +import Foundation +import WinUI + +/// A hyperlink as a button. +public struct HyperlinkButton: WinUIWidget { + + /// The button's label. + @Property( + set: { $0.content = $1 }, + pointer: WinUI.HyperlinkButton.self + ) + var label: String? + /// The hyperlink. + @Property( + set: { $0.navigateUri = .init($1) }, + pointer: WinUI.HyperlinkButton.self + ) + var url: String? + + /// Initialize the hyperlink button. + /// - Parameters: + /// - label: The label. + /// - url: The hyperlink. + public init(_ label: String, url: String) { + self.label = label + self.url = url + } + + /// Initialize the widget. + /// - Returns: + public func initializeWidget() -> Any { + WinUI.HyperlinkButton() + } + +} \ No newline at end of file diff --git a/Sources/Core/View/ModifierWrapper.swift b/Sources/Core/View/ModifierWrapper.swift index da0ac99..b73223d 100644 --- a/Sources/Core/View/ModifierWrapper.swift +++ b/Sources/Core/View/ModifierWrapper.swift @@ -28,6 +28,8 @@ public struct ModifierWrapper: WinUIWidget, CommandBarWidget { var gridColumn: Int? /// Whether the element is displayed. var visible: Bool? + /// The view's opacity. + var opacity: Double? // swiftlint:disable large_tuple /// Initialize the modifier wrapper. @@ -40,6 +42,7 @@ public struct ModifierWrapper: WinUIWidget, CommandBarWidget { /// - verticalAlignment: The vertical alignment. /// - gridColumn: The grid column. /// - visible: Whether the view is visible. + /// - opacity: The view's opacity. public init( view: AnyView, width: Double? = nil, @@ -48,7 +51,8 @@ public struct ModifierWrapper: WinUIWidget, CommandBarWidget { horizontalAlignment: HorizontalAlignment? = nil, verticalAlignment: VerticalAlignment? = nil, gridColumn: Int? = nil, - visible: Bool? = nil + visible: Bool? = nil, + opacity: Double? = nil ) { self.view = view self.width = width @@ -58,6 +62,7 @@ public struct ModifierWrapper: WinUIWidget, CommandBarWidget { self.verticalAlignment = verticalAlignment self.gridColumn = gridColumn self.visible = visible + self.opacity = opacity } // swiftlint:enable large_tuple @@ -114,6 +119,9 @@ public struct ModifierWrapper: WinUIWidget, CommandBarWidget { if let visible, previousState?.visible != visible { view.visibility = visible ? .visible : .collapsed } + if let opacity, previousState?.opacity != opacity { + view.opacity = opacity + } storage.previousState = self } diff --git a/Sources/Core/View/ScrollView.swift b/Sources/Core/View/ScrollView.swift new file mode 100644 index 0000000..2d7a0bb --- /dev/null +++ b/Sources/Core/View/ScrollView.swift @@ -0,0 +1,34 @@ +// +// ScrollView.swift +// WinUI +// +// Created by david-swift on 06.11.24. +// + +import WinUI + +/// A scroll view. +public struct ScrollView: WinUIWidget { + + /// The view content. + @ViewProperty( + set: { $0.content = $1 }, + pointer: WinUI.ScrollView.self, + subview: UIElement.self, + context: WinUIMainView.self + ) + var content + + /// Initialize a scroll view. + /// - Parameter content: The content. + public init(@ViewBuilder content: () -> Body) { + self.content = content() + } + + /// Initialize the widget. + /// - Returns: The widget. + public func initializeWidget() -> Any { + WinUI.ScrollView() + } + +} diff --git a/Sources/Core/View/Settings/SettingsControl.swift b/Sources/Core/View/Settings/SettingsControl.swift new file mode 100644 index 0000000..f0872ae --- /dev/null +++ b/Sources/Core/View/Settings/SettingsControl.swift @@ -0,0 +1,62 @@ +// +// SettingsControl.swift +// WinUI +// +// Created by david-swift on 06.11.24. +// + +/// A single settings item. +public struct SettingsControl: SimpleView { + + /// The settings item's title. + var title: String + /// The settings item's description. + var description: String? + /// The control view. + var control: Body + + /// Initialize a settings item. + /// - Parameters: + /// - title: The settings item's title. + /// - description: The settings item's description. + /// - control: The control view. + public init(_ title: String, description: String? = nil, @ViewBuilder control: () -> Body) { + self.title = title + self.description = description + self.control = control() + } + + /// The view. + public var view: Body { + ModifierWrapper( + view: Card { + ModifierWrapper( + view: Grid { + ModifierWrapper( + view: VStack { + Text(title) + ModifierWrapper( + view: Text(description ?? "") + .style(.caption), + visible: description != nil, + opacity: 0.8 + ) + }, + horizontalAlignment: .stretch, + verticalAlignment: .center + ) + ModifierWrapper( + view: control, + horizontalAlignment: .right + ) + }, + margin: (5, 5, 5, 5) + ) + } + .settings(), + margin: (2, 2, 2, 2), + verticalAlignment: .top + ) + } + +} diff --git a/Sources/Core/View/Settings/SettingsSection.swift b/Sources/Core/View/Settings/SettingsSection.swift new file mode 100644 index 0000000..31d5f4b --- /dev/null +++ b/Sources/Core/View/Settings/SettingsSection.swift @@ -0,0 +1,38 @@ +// +// SettingsSection.swift +// WinUI +// +// Created by david-swift on 06.11.24. +// + +/// A section in a settings page. +public struct SettingsSection: SimpleView { + + /// The section title. + var title: String + /// The section's content. + var content: Body + + /// Initialize the settings section. + /// - Parameters: + /// - title: The title. + /// - content: The content. + public init(_ title: String, @ViewBuilder content: () -> Body) { + self.title = title + self.content = content() + } + + /// The view. + public var view: Body { + ModifierWrapper( + view: Text(title) + .style(.bodyStrong), + margin: (5, 5, 5, 5) + ) + ModifierWrapper( + view: content, + margin: (0, 0, 0, 25) + ) + } + +} diff --git a/Sources/Demo/Demo.swift b/Sources/Demo/Demo.swift index 686a47d..8e428f4 100644 --- a/Sources/Demo/Demo.swift +++ b/Sources/Demo/Demo.swift @@ -30,18 +30,15 @@ struct Demo: App { struct ContentView: View { - @State private var selectedItem: NavigationItem = .shop + @State private var selectedItem: NavigationSelection = .custom(item: .shop) + @State private var comboBoxSelection: NavigationItem = .items + @State private var switchState = false var app: WinUIApp var view: Body { NavigationView(items: NavigationItem.allCases, selection: $selectedItem) { - if selectedItem != .shop { - Button("\(selectedItem)") { - selectedItem = .shop - } - .horizontalAlignment(.center) - .verticalAlignment(.center) - } else { + switch selectedItem { + case .custom(item: .shop): FlipView(NavigationItem.allCases) { element in Text(element.description) .style(.bodyStrong) @@ -57,13 +54,42 @@ struct ContentView: View { .margin(50) .verticalAlignment(.center) .verticalAlignment(.center) + case .settings: + ScrollView { + VStack { + SettingsSection("General") { + SettingsControl("Button Control") { + Button("Control") { } + } + SettingsControl("Combo Box", description: "Selected item: \(comboBoxSelection)") { + ComboBox(NavigationItem.allCases, selection: $comboBoxSelection) + } + SettingsControl("Toggle Switch", description: "Current state: \(switchState ? "on" : "off")") { + ToggleSwitch(isOn: $switchState) + } + } + SettingsSection("About") { + SettingsControl("WinUI for Swift Demo", description: "main") { + HyperlinkButton("Website", url: "https://aparoksha.dev/") + } + } + } + .margin(50, .horizontal) + .margin(20, .vertical) + } + case let .custom(item: item): + Button("\(item)") { + selectedItem = .custom(item: .shop) + } + .horizontalAlignment(.center) + .verticalAlignment(.center) } } .header(settingsLabel: "Settings") { AppBarButton("Delete", icon: .systemIcon(unicode: "\u{E74D}")) { print("Delete") } - .visible(selectedItem != .shop) + .visible(selectedItem != .settings) } } diff --git a/Sources/winui-swift/AnyView+.swift b/Sources/winui-swift/AnyView+.swift index 6abea63..9f1ffe0 100644 --- a/Sources/winui-swift/AnyView+.swift +++ b/Sources/winui-swift/AnyView+.swift @@ -29,7 +29,7 @@ extension AnyView { /// - edges: The affected edges. /// - Returns: The view. public func margin(_ value: Double, _ edges: [Edge] = .all) -> AnyView { - ModifierWrapper(view: self, margin: edges.set(value)) + ModifierWrapper(view: Grid { self }, margin: edges.set(value)) } /// Set the view's horizontal alignment. @@ -60,6 +60,13 @@ extension AnyView { ModifierWrapper(view: self, visible: visible) } + /// Set the view's opacity. + /// - Parameter opacity: The opacity. + /// - Returns: The view. + public func opacity(_ opacity: Double?) -> AnyView { + ModifierWrapper(view: self, opacity: opacity) + } + /// Make the view match the card style. /// - Returns: The card. public func card() -> AnyView {