diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index d6f0535..d82e793 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -7,6 +7,7 @@ - [MenuItemGroup](protocols/MenuItemGroup.md) - [StateProtocol](protocols/StateProtocol.md) - [View](protocols/View.md) +- [ViewSwitcherOption](protocols/ViewSwitcherOption.md) - [Widget](protocols/Widget.md) - [WindowScene](protocols/WindowScene.md) - [WindowSceneGroup](protocols/WindowSceneGroup.md) @@ -45,6 +46,7 @@ - [ToolbarView](structs/ToolbarView.md) - [VStack](structs/VStack.md) - [ViewStack](structs/ViewStack.md) +- [ViewSwitcher](structs/ViewSwitcher.md) - [Window](structs/Window.md) ## Classes diff --git a/Documentation/Reference/protocols/ViewSwitcherOption.md b/Documentation/Reference/protocols/ViewSwitcherOption.md new file mode 100644 index 0000000..44343b8 --- /dev/null +++ b/Documentation/Reference/protocols/ViewSwitcherOption.md @@ -0,0 +1,19 @@ +**PROTOCOL** + +# `ViewSwitcherOption` + +The protocol an element type for view switcher has to conform to. + +## Properties +### `title` + +The title displayed in the switcher and used for identification. + +### `icon` + +A symbolic representation in the view switcher. + +## Methods +### `init(title:)` + +Get the element from the title. diff --git a/Documentation/Reference/structs/ViewSwitcher.md b/Documentation/Reference/structs/ViewSwitcher.md new file mode 100644 index 0000000..795d0f6 --- /dev/null +++ b/Documentation/Reference/structs/ViewSwitcher.md @@ -0,0 +1,46 @@ +**STRUCT** + +# `ViewSwitcher` + +A picker used for indicating multiple views. + +It normally controls a `ViewStack` (e.g. via `switch` statements). + +## Properties +### `selection` + +The selected element. + +### `wide` + +Whether the wide style is used, that means the icons and titles are on the same line. + +## Methods +### `init(selection:)` + +Initialize a view switcher. +- Parameter selection: The selected element. + +### `container(modifiers:)` + +Get a view switcher's view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `update(_:modifiers:)` + +Update a view switcher's view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `updateSwitcher(switcher:)` + +Update a view switcher's style and selection. +- Parameter switcher: The view switcher. + +### `wideDesign(_:)` + +Set whether to use the wide design. +- Parameter wide: Whether to use the wide design. +- Returns: The view switcher. diff --git a/Sources/Adwaita/View/ViewSwitcher.swift b/Sources/Adwaita/View/ViewSwitcher.swift new file mode 100644 index 0000000..f7fd1e8 --- /dev/null +++ b/Sources/Adwaita/View/ViewSwitcher.swift @@ -0,0 +1,83 @@ +// +// ViewSwitcher.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +import Libadwaita + +/// A picker used for indicating multiple views. +/// +/// It normally controls a `ViewStack` (e.g. via `switch` statements). +public struct ViewSwitcher: Widget where Element: ViewSwitcherOption { + + /// The selected element. + @Binding var selection: Element + /// Whether the wide style is used, that means the icons and titles are on the same line. + var wide = false + + /// Initialize a view switcher. + /// - Parameter selection: The selected element. + public init(selection: Binding) { + self._selection = selection + } + + /// Get a view switcher's view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let switcher = Libadwaita.ViewSwitcher() + for option in Element.allCases { + _ = switcher.addOption(title: option.title, icon: option.icon) + } + _ = switcher.onSelect { + let selection = switcher.getSelection() + if let element = Element(title: selection) { + self.selection = element + } + } + updateSwitcher(switcher: switcher) + return .init(switcher) + } + + /// Update a view switcher's view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let switcher = storage.view as? Libadwaita.ViewSwitcher { + updateSwitcher(switcher: switcher) + } + } + + /// Update a view switcher's style and selection. + /// - Parameter switcher: The view switcher. + func updateSwitcher(switcher: Libadwaita.ViewSwitcher) { + _ = switcher.wideDesign(wide) + switcher.select(title: selection.title) + } + + /// Set whether to use the wide design. + /// - Parameter wide: Whether to use the wide design. + /// - Returns: The view switcher. + public func wideDesign(_ wide: Bool = true) -> Self { + var newSelf = self + newSelf.wide = wide + return newSelf + } + +} + +/// The protocol an element type for view switcher has to conform to. +public protocol ViewSwitcherOption: CaseIterable { + + /// The title displayed in the switcher and used for identification. + var title: String { get } + /// A symbolic representation in the view switcher. + var icon: Icon { get } + + /// Get the element from the title. + init?(title: String) + +} diff --git a/Tests/Demo.swift b/Tests/Demo.swift index 89e9321..0fee9e4 100644 --- a/Tests/Demo.swift +++ b/Tests/Demo.swift @@ -49,6 +49,13 @@ struct Demo: App { .closeShortcut() .defaultSize(width: 400, height: 250) .title("Toolbar Demo") + Window(id: "switcher-demo", open: 0) { _ in + ViewSwitcherDemo.WindowContent() + } + .closeShortcut() + .defaultSize(width: 600, height: 400) + .resizable(false) + .title("View Switcher Demo") } } diff --git a/Tests/Page.swift b/Tests/Page.swift index b5b73fb..622ef90 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -22,6 +22,7 @@ enum Page: String, Identifiable, CaseIterable, Codable { case toast case list case carousel + case viewSwitcher var id: Self { self @@ -31,6 +32,8 @@ enum Page: String, Identifiable, CaseIterable, Codable { switch self { case .overlayWindow: return "Overlay Window" + case .viewSwitcher: + return "View Switcher" default: return rawValue.capitalized } @@ -67,9 +70,12 @@ enum Page: String, Identifiable, CaseIterable, Codable { return "Organize content in multiple rows." case .carousel: return "Scroll horizontally on a touchpad or touchscreen, or scroll down on your mouse wheel." + case .viewSwitcher: + return "Switch the window's view." } } + // swiftlint:disable cyclomatic_complexity @ViewBuilder func view(app: GTUIApp!, window: GTUIApplicationWindow, toast: Signal) -> Body { switch self { @@ -93,8 +99,11 @@ enum Page: String, Identifiable, CaseIterable, Codable { ListDemo() case .carousel: CarouselDemo() + case .viewSwitcher: + ViewSwitcherDemo(app: app) } } + // swiftlint:enable cyclomatic_complexity } diff --git a/Tests/ViewSwitcherDemo.swift b/Tests/ViewSwitcherDemo.swift new file mode 100644 index 0000000..a6b5166 --- /dev/null +++ b/Tests/ViewSwitcherDemo.swift @@ -0,0 +1,101 @@ +// +// ToolbarDemo.swift +// Adwaita +// +// Created by david-swift on 03.01.24. +// + +// swiftlint:disable missing_docs + +import Adwaita +import Libadwaita + +struct ViewSwitcherDemo: View { + + var app: GTUIApp + + var view: Body { + VStack { + Button("View Demo") { + app.showWindow("switcher-demo") + } + .style("suggested-action") + .frame(maxSize: 100) + } + } + + struct WindowContent: View { + + @State private var selection: ViewSwitcherView = .albums + @State private var bottom = false + + var view: Body { + VStack { + Text(selection.title) + .padding() + HStack { + Button(bottom ? "Show Top Bar" : "Show Bottom Bar") { + bottom.toggle() + } + } + .halign(.center) + } + .valign(.center) + .topToolbar { + if bottom { + HeaderBar + .empty() + } else { + toolbar + } + } + .bottomToolbar(visible: bottom) { + toolbar + } + } + + var toolbar: View { + HeaderBar(titleButtons: !bottom) { } end: { } + .headerBarTitle { + ViewSwitcher(selection: $selection) + .wideDesign(!bottom) + } + } + + } + + enum ViewSwitcherView: String, ViewSwitcherOption { + + case albums + case artists + case songs + case playlists + + var title: String { + rawValue.capitalized + } + + var icon: Icon { + .default(icon: { + switch self { + case .albums: + return .mediaOpticalCdAudio + case .artists: + return .avatarDefault + case .songs: + return .emblemMusic + case .playlists: + return .viewList + } + }()) + } + + init?(title: String) { + self.init(rawValue: title.lowercased()) + } + + } + +} + +// swiftlint:enable missing_docs diff --git a/user-manual/Information/Widgets.md b/user-manual/Information/Widgets.md index 62e657c..a906cb0 100644 --- a/user-manual/Information/Widgets.md +++ b/user-manual/Information/Widgets.md @@ -19,6 +19,7 @@ This is an overview of the available widgets and other components in _Adwaita_. | StatusPage | A page with an icon, title, and optionally description and widget. | AdwStatusPage | | Container | Supports any widget conforming to `Libadwaita.InsertableContainer`. | Multiple widgets | | Carousel | A paginated scrolling widget. | AdwCarousel | +| ViewSwitcher | A control for switching between different views. | AdwViewSwitcher | | StateWrapper | A wrapper not affecting the UI which stores state information. | - | ### View Modifiers @@ -67,6 +68,11 @@ This is an overview of the available widgets and other components in _Adwaita_. | ---------------------------- | --------------------------------------------------------------------------------------- | | `trailingSidebar(_:)` | Whether the sidebar is trailing to the content view. | +### `ViewSwitcher` Modifiers +| Syntax | Description | +| ---------------------------- | --------------------------------------------------------------------------------------- | +| `wideDesign(_:)` | Whether the wide view switcher design is used. | + ### Window Types | Name | Description | Widget | | -------------------- | ----------------------------------------------------------------- | ---------------------- |