From 8eb004c9ea766de8bebbfd8f814be87ed3c02267 Mon Sep 17 00:00:00 2001 From: david-swift Date: Mon, 1 Jan 2024 20:33:06 +0100 Subject: [PATCH] Add support for carousels Add support for any Libadwaita insertable container --- Documentation/Reference/README.md | 2 + Documentation/Reference/structs/Carousel.md | 26 +++++ Documentation/Reference/structs/Container.md | 60 +++++++++++ Sources/Adwaita/View/Carousel.swift | 37 +++++++ Sources/Adwaita/View/Container.swift | 101 +++++++++++++++++++ Tests/CarouselDemo.swift | 44 ++++++++ Tests/Page.swift | 5 + user-manual/Information/Widgets.md | 34 ++++--- 8 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 Documentation/Reference/structs/Carousel.md create mode 100644 Documentation/Reference/structs/Container.md create mode 100644 Sources/Adwaita/View/Carousel.swift create mode 100644 Sources/Adwaita/View/Container.swift create mode 100644 Tests/CarouselDemo.swift diff --git a/Documentation/Reference/README.md b/Documentation/Reference/README.md index 303940e..984ea0c 100644 --- a/Documentation/Reference/README.md +++ b/Documentation/Reference/README.md @@ -18,7 +18,9 @@ - [AppearObserver](structs/AppearObserver.md) - [Binding](structs/Binding.md) - [Button](structs/Button.md) +- [Carousel](structs/Carousel.md) - [Clamp](structs/Clamp.md) +- [Container](structs/Container.md) - [ContentModifier](structs/ContentModifier.md) - [FileDialog](structs/FileDialog.md) - [HStack](structs/HStack.md) diff --git a/Documentation/Reference/structs/Carousel.md b/Documentation/Reference/structs/Carousel.md new file mode 100644 index 0000000..db8c5d3 --- /dev/null +++ b/Documentation/Reference/structs/Carousel.md @@ -0,0 +1,26 @@ +**STRUCT** + +# `Carousel` + +A carousel view. + +## Properties +### `elements` + +The elements. + +### `content` + +The content. + +### `view` + +The view. + +## Methods +### `init(_:content:)` + +Initialize `Carousel`. +- Parameters: + - elements: The elements. + - content: The view for an element. diff --git a/Documentation/Reference/structs/Container.md b/Documentation/Reference/structs/Container.md new file mode 100644 index 0000000..95af77e --- /dev/null +++ b/Documentation/Reference/structs/Container.md @@ -0,0 +1,60 @@ +**STRUCT** + +# `Container` + +A container widget. + +## Properties +### `elements` + +The elements. + +### `content` + +The content. + +### `container` + +Get the container for initialization. + +### `elementsID` + +The identifier of the elements storage. + +## Methods +### `init(_:content:container:)` + +Initialize `Container`. +- Parameters: + - elements: The elements. + - content: The view for an element. + - container: Get the initial Libadwaita container widget. + +### `update(_:modifiers:)` + +Update a view storage. +- Parameters: + - storage: The view storage. + - modifiers: Modify views before being updated. + +### `container(modifiers:)` + +Get a view storage. +- Parameter modifiers: Modify views before being updated. +- Returns: The view storage. + +### `updateContainer(_:content:modifiers:)` + +Update the container's content. +- Parameters: + - container: The container. + - content: The content's view storage. + - modifiers: The view modifiers. + +### `getWidget(element:modifiers:)` + +Get the view storage of an element. +- Parameters: + - element: The element. + - modifiers: The modifiers. +- Returns: The view storage. diff --git a/Sources/Adwaita/View/Carousel.swift b/Sources/Adwaita/View/Carousel.swift new file mode 100644 index 0000000..bf23f3a --- /dev/null +++ b/Sources/Adwaita/View/Carousel.swift @@ -0,0 +1,37 @@ +// +// Carousel.swift +// Adwaita +// +// Created by david-swift on 01.01.24. +// + +import Libadwaita + +/// A carousel view. +public struct Carousel: View where Element: Identifiable { + + /// The elements. + var elements: [Element] + /// The content. + var content: (Element) -> Body + + /// The view. + public var view: Body { + Container(elements, content: content) { + Libadwaita.Carousel() + } + } + + /// Initialize `Carousel`. + /// - Parameters: + /// - elements: The elements. + /// - content: The view for an element. + public init( + _ elements: [Element], + @ViewBuilder content: @escaping (Element) -> Body + ) { + self.content = content + self.elements = elements + } + +} diff --git a/Sources/Adwaita/View/Container.swift b/Sources/Adwaita/View/Container.swift new file mode 100644 index 0000000..aceec96 --- /dev/null +++ b/Sources/Adwaita/View/Container.swift @@ -0,0 +1,101 @@ +// +// Container.swift +// Adwaita +// +// Created by david-swift on 01.01.24. +// + +import LevenshteinTransformations +import Libadwaita + +/// A container widget. +public struct Container: Widget +where Element: Identifiable, Type: InsertableContainer, Type: NativeWidgetPeer { + + /// The elements. + var elements: [Element] + /// The content. + var content: (Element) -> Body + /// Get the container for initialization. + var container: () -> Type + + /// The identifier of the elements storage. + let elementsID = "elements" + + /// Initialize `Container`. + /// - Parameters: + /// - elements: The elements. + /// - content: The view for an element. + /// - container: Get the initial Libadwaita container widget. + public init( + _ elements: [Element], + @ViewBuilder content: @escaping (Element) -> Body, + container: @escaping () -> Type + ) { + self.content = content + self.elements = elements + self.container = container + } + + /// Update a view storage. + /// - Parameters: + /// - storage: The view storage. + /// - modifiers: Modify views before being updated. + public func update(_ storage: ViewStorage, modifiers: [(View) -> View]) { + if let container = storage.view as? Type { + var content: [ViewStorage] = storage.content[.mainContent] ?? [] + updateContainer(container, content: .init { content } set: { content = $0 }, modifiers: modifiers) + storage.content[.mainContent] = content + for (index, element) in elements.enumerated() { + self.content(element).widget(modifiers: modifiers).update(content[index], modifiers: modifiers) + } + } + } + + /// Get a view storage. + /// - Parameter modifiers: Modify views before being updated. + /// - Returns: The view storage. + public func container(modifiers: [(View) -> View]) -> ViewStorage { + let container = self.container() + var content: [ViewStorage] = [] + updateContainer(container, content: .init { content } set: { content = $0 }, modifiers: modifiers) + return .init(container, content: [.mainContent: content]) + } + + /// Update the container's content. + /// - Parameters: + /// - container: The container. + /// - content: The content's view storage. + /// - modifiers: The view modifiers. + func updateContainer(_ container: Type, content: Binding<[ViewStorage]>, modifiers: [(View) -> View]) { + let old = container.fields[elementsID] as? [Element] ?? [] + old.identifiableTransform( + to: elements, + functions: .init { index, element in + let widget = getWidget(element: element, modifiers: modifiers) + _ = container.removeWidgets([content.wrappedValue[index].view]) + _ = container.insert(widget.view, at: index) + content.wrappedValue.remove(at: index) + content.wrappedValue.insert(widget, at: index) + } delete: { index in + _ = container.removeWidgets([content.wrappedValue[index].view]) + content.wrappedValue.remove(at: index) + } insert: { index, element in + let widget = getWidget(element: element, modifiers: modifiers) + _ = container.insert(widget.view, at: index) + content.wrappedValue.insert(widget, at: index) + } + ) + container.fields[elementsID] = elements + } + + /// Get the view storage of an element. + /// - Parameters: + /// - element: The element. + /// - modifiers: The modifiers. + /// - Returns: The view storage. + func getWidget(element: Element, modifiers: [(View) -> View]) -> ViewStorage { + self.content(element).widget(modifiers: modifiers).container(modifiers: modifiers) + } + +} diff --git a/Tests/CarouselDemo.swift b/Tests/CarouselDemo.swift new file mode 100644 index 0000000..79f9512 --- /dev/null +++ b/Tests/CarouselDemo.swift @@ -0,0 +1,44 @@ +// +// CarouselDemo.swift +// Adwaita +// +// Created by david-swift on 01.01.24. +// + +// swiftlint:disable missing_docs no_magic_numbers + +import Adwaita +import Foundation + +struct CarouselDemo: View { + + @State private var items: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")] + + var view: Body { + Button("Add Card") { + let element = ListDemo.Element(id: UUID().uuidString) + items.append(element) + } + .padding() + .halign(.center) + Carousel(items) { element in + VStack { + Text(element.id) + .vexpand() + Button("Delete") { + items = items.filter { $0.id != element.id } + } + .padding() + } + .vexpand() + .hexpand() + .style("card") + .padding(20) + .frame(minWidth: 300, minHeight: 200) + .frame(maxSize: 500) + } + } + +} + +// swiftlint:enable missing_docs no_magic_numbers diff --git a/Tests/Page.swift b/Tests/Page.swift index 3e8f2ba..ed81738 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -21,6 +21,7 @@ enum Page: String, Identifiable, CaseIterable { case overlayWindow case toast case list + case carousel var id: Self { self @@ -64,6 +65,8 @@ enum Page: String, Identifiable, CaseIterable { return "Show a notification inside of your app." case .list: return "Organize content in multiple rows." + case .carousel: + return "Scroll horizontally on a touchpad or touchscreen, or scroll down on your mouse wheel." } } @@ -88,6 +91,8 @@ enum Page: String, Identifiable, CaseIterable { ToastDemo(toast: toast) case .list: ListDemo() + case .carousel: + CarouselDemo() } } diff --git a/user-manual/Information/Widgets.md b/user-manual/Information/Widgets.md index a850108..7108130 100644 --- a/user-manual/Information/Widgets.md +++ b/user-manual/Information/Widgets.md @@ -2,22 +2,24 @@ This is an overview of the available widgets and other components in _Adwaita_. -| Name | Description | Widget | -| -------------------- | ----------------------------------------------------------------- | ---------------------- | -| Button | A widget that triggers a function when being clicked. | GtkButton | -| ViewStack | A widget that displays one of its child views based on an id. | GtkStack | -| HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar | -| Text | A widget for displaying a small amount of text. | GtkLabel | -| VStack | A widget which arranges child widgets into a single column. | GtkBox | -| HStack | A widget which arranges child widgets into a single row. | GtkBox | -| Toggle | A button with two possible states, on and off. | GtkToggleButton | -| List | A widget which arranges child widgets vertically into rows. | GtkListBox | -| Menu | A widget showing a button that toggles the appearance of a menu. | GtkMenuButton | -| NavigationSplitView | A widget presenting sidebar and content side by side. | AdwNavigationSplitView | -| OverlaySplitView | A widget presenting sidebar and content side by side. | AdwOverlaySplitView | -| ScrollView | A container that makes its child scrollable. | GtkScrolledWindow | -| StatusPage | A page with an icon, title, and optionally description and widget.| AdwStatusPage | -| StateWrapper | A wrapper not affecting the UI which stores state information. | - | +| Name | Description | Widget | +| -------------------- | ------------------------------------------------------------------- | ---------------------- | +| Button | A widget that triggers a function when being clicked. | GtkButton | +| ViewStack | A widget that displays one of its child views based on an id. | GtkStack | +| HeaderBar | A widget for creating custom title bars for windows. | GtkHeaderBar | +| Text | A widget for displaying a small amount of text. | GtkLabel | +| VStack | A widget which arranges child widgets into a single column. | GtkBox | +| HStack | A widget which arranges child widgets into a single row. | GtkBox | +| Toggle | A button with two possible states, on and off. | GtkToggleButton | +| List | A widget which arranges child widgets vertically into rows. | GtkListBox | +| Menu | A widget showing a button that toggles the appearance of a menu. | GtkMenuButton | +| NavigationSplitView | A widget presenting sidebar and content side by side. | AdwNavigationSplitView | +| OverlaySplitView | A widget presenting sidebar and content side by side. | AdwOverlaySplitView | +| ScrollView | A container that makes its child scrollable. | GtkScrolledWindow | +| 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 | +| StateWrapper | A wrapper not affecting the UI which stores state information. | - | ### View Modifiers