├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── component_request.yml
│ └── feature_request.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── docs.yml
│ └── swiftlint.yml
├── .gitignore
├── .spi.yml
├── .swiftlint.yml
├── CONTRIBUTING.md
├── Icons
├── AdwaitaIcon.png
├── AdwaitaIcon.pxd
├── Counter.png
├── GitHubBanner.png
└── GitHubBanner.pxd
├── LICENSE.md
├── Package.swift
├── README.md
├── Sources
├── Adwaita
│ ├── Menu
│ │ ├── MenuButton.swift
│ │ ├── MenuCollection.swift
│ │ ├── MenuContext.swift
│ │ ├── MenuEitherView.swift
│ │ ├── MenuSection.swift
│ │ └── Submenu.swift
│ ├── Model
│ │ ├── AdwaitaApp.swift
│ │ ├── AdwaitaMainView.swift
│ │ ├── AdwaitaSceneElement.swift
│ │ ├── AdwaitaWidget.swift
│ │ ├── Enumerations
│ │ │ ├── Alignment.swift
│ │ │ ├── ContentFit.swift
│ │ │ ├── Edge.swift
│ │ │ ├── Icon.swift
│ │ │ └── Transition.swift
│ │ ├── Extensions
│ │ │ ├── Array.swift
│ │ │ ├── Bool.swift
│ │ │ ├── Int.swift
│ │ │ ├── OpaquePointer.swift
│ │ │ ├── Set.swift
│ │ │ ├── String.swift
│ │ │ ├── UInt.swift
│ │ │ ├── UnsafeMutablePointer.swift
│ │ │ ├── UnsafeMutableRawPointer.swift
│ │ │ └── ViewStorage.swift
│ │ ├── Idle.swift
│ │ ├── Signals
│ │ │ └── SignalData.swift
│ │ ├── Storage.swift
│ │ └── WindowView.swift
│ ├── View
│ │ ├── Banner+.swift
│ │ ├── Button+.swift
│ │ ├── Carousel+.swift
│ │ ├── CheckButton+.swift
│ │ ├── Dialogs
│ │ │ ├── AboutDialog.swift
│ │ │ ├── AlertDialog.swift
│ │ │ ├── Dialog.swift
│ │ │ └── FileDialog.swift
│ │ ├── EitherView.swift
│ │ ├── Fixed+.swift
│ │ ├── FlowBox+.swift
│ │ ├── ForEach.swift
│ │ ├── Forms
│ │ │ ├── ActionRow+.swift
│ │ │ ├── ComboRow+.swift
│ │ │ ├── EntryRow+.swift
│ │ │ ├── Form.swift
│ │ │ ├── FormSection+.swift
│ │ │ ├── PasswordEntryRow+.swift
│ │ │ ├── SpinRow+.swift
│ │ │ └── SwitchRow+.swift
│ │ ├── Generated
│ │ │ ├── ActionRow.swift
│ │ │ ├── AspectFrame.swift
│ │ │ ├── Avatar.swift
│ │ │ ├── Banner.swift
│ │ │ ├── Bin.swift
│ │ │ ├── Box.swift
│ │ │ ├── Button.swift
│ │ │ ├── ButtonContent.swift
│ │ │ ├── Carousel.swift
│ │ │ ├── CenterBox.swift
│ │ │ ├── CheckButton.swift
│ │ │ ├── Clamp.swift
│ │ │ ├── ComboRow.swift
│ │ │ ├── EntryRow.swift
│ │ │ ├── ExpanderRow.swift
│ │ │ ├── Fixed.swift
│ │ │ ├── FlowBox.swift
│ │ │ ├── HeaderBar.swift
│ │ │ ├── Label.swift
│ │ │ ├── LevelBar.swift
│ │ │ ├── LinkButton.swift
│ │ │ ├── ListBox.swift
│ │ │ ├── Menu.swift
│ │ │ ├── NavigationView.swift
│ │ │ ├── Overlay.swift
│ │ │ ├── OverlaySplitView.swift
│ │ │ ├── PasswordEntryRow.swift
│ │ │ ├── Picture.swift
│ │ │ ├── Popover.swift
│ │ │ ├── PreferencesGroup.swift
│ │ │ ├── PreferencesPage.swift
│ │ │ ├── PreferencesRow.swift
│ │ │ ├── ProgressBar.swift
│ │ │ ├── ScrolledWindow.swift
│ │ │ ├── SearchBar.swift
│ │ │ ├── SearchEntry.swift
│ │ │ ├── Separator.swift
│ │ │ ├── SpinRow.swift
│ │ │ ├── Spinner.swift
│ │ │ ├── SplitButton.swift
│ │ │ ├── StatusPage.swift
│ │ │ ├── SwitchRow.swift
│ │ │ ├── ToastOverlay.swift
│ │ │ ├── ToggleButton.swift
│ │ │ ├── ToolbarView.swift
│ │ │ └── WindowTitle.swift
│ │ ├── HStack.swift
│ │ ├── HeaderBar+.swift
│ │ ├── List.swift
│ │ ├── Menu+.swift
│ │ ├── Modifiers
│ │ │ ├── AnyView+.swift
│ │ │ ├── Clamp+.swift
│ │ │ ├── ModifierWrapper.swift
│ │ │ ├── Popover+.swift
│ │ │ └── ToastOverlay+.swift
│ │ ├── NavigationSplitView.swift
│ │ ├── NavigationView+.swift
│ │ ├── OverlaySplitView+.swift
│ │ ├── Picture+.swift
│ │ ├── ProgressBar+.swift
│ │ ├── ScrollView.swift
│ │ ├── StatusPage+.swift
│ │ ├── Text.swift
│ │ ├── Toggle.swift
│ │ ├── VStack.swift
│ │ ├── ViewStack.swift
│ │ └── ViewSwitcher.swift
│ └── Window
│ │ └── Window.swift
├── CAdw
│ ├── module.modulemap
│ └── shim.h
├── Demo
│ ├── AlertDialogDemo.swift
│ ├── CarouselDemo.swift
│ ├── CounterDemo.swift
│ ├── Demo.swift
│ ├── DialogDemo.swift
│ ├── DiceDemo.swift
│ ├── FixedDemo.swift
│ ├── FlowBoxDemo.swift
│ ├── FormDemo.swift
│ ├── IdleDemo.swift
│ ├── ListDemo.swift
│ ├── NavigationViewDemo.swift
│ ├── Page.swift
│ ├── PasswordCheckerDemo.swift
│ ├── PictureDemo.swift
│ ├── PopoverDemo.swift
│ ├── ToastDemo.swift
│ ├── ToolbarDemo.swift
│ ├── TransitionDemo.swift
│ ├── ViewSwitcherDemo.swift
│ └── WindowsDemo.swift
└── Generation
│ ├── Extensions
│ └── String.swift
│ ├── GIR
│ ├── Class+.swift
│ ├── Class.swift
│ ├── ClassLike.swift
│ ├── Conformance.swift
│ ├── Constructor.swift
│ ├── GIR.swift
│ ├── GIRType.swift
│ ├── Interface.swift
│ ├── Namespace.swift
│ ├── Parameter.swift
│ ├── Parameters.swift
│ ├── Property.swift
│ └── Signal.swift
│ ├── Generation.swift
│ ├── GenerationConfiguration.swift
│ └── WidgetConfiguration.swift
└── io.github.AparokshaUI.Demo.json
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Something is not working as expected.
3 | title: Description of the bug
4 | labels: bug
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Describe the bug
10 | description: >-
11 | A clear and concise description of what the bug is.
12 | validations:
13 | required: true
14 |
15 | - type: textarea
16 | attributes:
17 | label: To Reproduce
18 | description: >-
19 | Steps to reproduce the behavior.
20 | placeholder: |
21 | 1. Go to '...'
22 | 2. Click on '....'
23 | 3. Scroll down to '....'
24 | 4. See error
25 | validations:
26 | required: true
27 |
28 | - type: textarea
29 | attributes:
30 | label: Expected behavior
31 | description: >-
32 | A clear and concise description of what you expected to happen.
33 | validations:
34 | required: true
35 |
36 | - type: textarea
37 | attributes:
38 | label: Additional context
39 | description: >-
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/component_request.yml:
--------------------------------------------------------------------------------
1 | name: Components request
2 | description: Suggest an idea for a new component
3 | title: Description of the component request
4 | labels: enhancement
5 |
6 | body:
7 | - type: textarea
8 | attributes:
9 | label: Why would you like to add a new component?
10 | placeholder: >-
11 | A clear and concise description of why the component should be added.
12 | validations:
13 | required: false
14 |
15 | - type: textarea
16 | attributes:
17 | label: Describe your idea for the implementation.
18 | placeholder: >-
19 | What could the implementation be like in Adwaita?
20 | validations:
21 | required: false
22 |
23 | - type: textarea
24 | attributes:
25 | label: Additional context
26 | placeholder: >-
27 | Add any other context about the component request here.
28 | validations:
29 | required: false
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | title: Description of the feature request
4 | labels: enhancement
5 |
6 | body:
7 | - type: input
8 | attributes:
9 | label: Is your feature request related to a problem? Please describe.
10 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 | validations:
12 | required: false
13 |
14 | - type: textarea
15 | attributes:
16 | label: Describe the solution you'd like
17 | placeholder: >-
18 | A clear and concise description of what you want to happen.
19 | validations:
20 | required: true
21 |
22 | - type: textarea
23 | attributes:
24 | label: Describe alternatives you've considered
25 | placeholder: >-
26 | A clear and concise description of any alternative solutions or features you've considered.
27 | validations:
28 | required: true
29 |
30 | - type: textarea
31 | attributes:
32 | label: Additional context
33 | placeholder: >-
34 | Add any other context or screenshots about the feature request here.
35 | validations:
36 | required: true
37 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Steps
2 | - [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file.
3 | - [ ] Build the project on your machine. If it does not compile, fix the errors.
4 | - [ ] Describe the purpose and approach of your pull request below.
5 | - [ ] Submit the pull request. Thank you very much for your contribution!
6 |
7 | ## Purpose
8 | _Describe the problem or feature._
9 | _If there is a related issue, add the link._
10 |
11 | ## Approach
12 | _Describe how this pull request solves the problem or adds the feature._
13 |
14 | [1]: /Contributors.md
15 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | id-token: write
12 |
13 | concurrency:
14 | group: "pages"
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | Deploy:
19 | environment:
20 | name: github-pages
21 | url: ${{ steps.deployment.outputs.page_url }}
22 | runs-on: macos-15
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Install Libadwaita
26 | run: |
27 | brew update
28 | brew install libadwaita
29 | sed -i '' 's/-I..includedir.//g' $(brew --prefix)/Library/Homebrew/os/mac/pkgconfig/*/libffi.pc
30 | - name: Clone DocC Repo
31 | run: |
32 | git clone https://github.com/AparokshaUI/Adwaita.docc Sources/Adwaita/Adwaita.docc
33 | rm Sources/Adwaita/Adwaita.docc/LICENSE.md
34 | rm Sources/Adwaita/Adwaita.docc/README.md
35 | y | rm -r Sources/Adwaita/Adwaita.docc/.git
36 | - name: Build Docs
37 | run: |
38 | xcrun xcodebuild docbuild \
39 | -scheme Adwaita \
40 | -destination 'generic/platform=macOS' \
41 | -derivedDataPath "$PWD/.derivedData"
42 | xcrun docc process-archive transform-for-static-hosting \
43 | "$PWD/.derivedData/Build/Products/Debug/Adwaita.doccarchive" \
44 | --output-path "docs" \
45 | --hosting-base-path "adwaita-swift"
46 | - name: Modify Docs
47 | run: |
48 | echo "" > docs/index.html;
49 | sed -i '' 's/,2px/,10px/g' docs/css/index.*.css
50 | - name: Upload Artifact
51 | uses: actions/upload-pages-artifact@v3
52 | with:
53 | path: 'docs'
54 | - name: Deploy to GitHub Pages
55 | id: deployment
56 | uses: actions/deploy-pages@v4
57 |
--------------------------------------------------------------------------------
/.github/workflows/swiftlint.yml:
--------------------------------------------------------------------------------
1 | name: SwiftLint
2 |
3 | on:
4 | push:
5 | paths:
6 | - '.github/workflows/swiftlint.yml'
7 | - '.swiftlint.yml'
8 | - '**/*.swift'
9 | pull_request:
10 | paths:
11 | - '.github/workflows/swiftlint.yml'
12 | - '.swiftlint.yml'
13 | - '**/*.swift'
14 | workflow_dispatch:
15 | paths:
16 | - '.github/workflows/swiftlint.yml'
17 | - '.swiftlint.yml'
18 | - '**/*.swift'
19 |
20 | jobs:
21 | SwiftLint:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v1
25 | - name: SwiftLint
26 | uses: norio-nomura/action-swiftlint@3.2.1
27 | with:
28 | args: --strict
29 | env:
30 | WORKING_DIRECTORY: Source
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 | /Package.resolved
11 | .Ulysses-Group.plist
12 | /.docc-build
13 | /io.github.AparokshaUI.Generation.json
14 | /io.github.AparokshaUI.swiftlint.json
15 | /.vscode
16 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | external_links:
3 | documentation: "https://aparokshaui.github.io/adwaita-swift/"
4 | builder:
5 | configs:
6 | - platform: linux
7 | swift_version: '5.10'
8 | image: registry.gitlab.com/finestructure/spi-images:adwaita-5.10-latest
9 | - platform: linux
10 | swift_version: '5.9'
11 | image: registry.gitlab.com/finestructure/spi-images:adwaita-5.9-latest
12 | - platform: linux
13 | swift_version: '5.8'
14 | image: registry.gitlab.com/finestructure/spi-images:adwaita-5.8-latest
15 | - platform: linux
16 | swift_version: '5.7'
17 | image: registry.gitlab.com/finestructure/spi-images:adwaita-5.7-latest
18 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | # Opt-In Rules
2 | opt_in_rules:
3 | - anonymous_argument_in_multiline_closure
4 | - array_init
5 | - attributes
6 | - closure_body_length
7 | - closure_end_indentation
8 | - closure_spacing
9 | - collection_alignment
10 | - comma_inheritance
11 | - conditional_returns_on_newline
12 | - contains_over_filter_count
13 | - contains_over_filter_is_empty
14 | - contains_over_first_not_nil
15 | - contains_over_range_nil_comparison
16 | - convenience_type
17 | - discouraged_none_name
18 | - discouraged_object_literal
19 | - empty_collection_literal
20 | - empty_count
21 | - empty_string
22 | - enum_case_associated_values_count
23 | - explicit_init
24 | - fallthrough
25 | - file_header
26 | - file_name
27 | - file_name_no_space
28 | - first_where
29 | - flatmap_over_map_reduce
30 | - force_unwrapping
31 | - function_default_parameter_at_end
32 | - identical_operands
33 | - implicit_return
34 | - implicitly_unwrapped_optional
35 | - joined_default_parameter
36 | - last_where
37 | - legacy_multiple
38 | - let_var_whitespace
39 | - literal_expression_end_indentation
40 | - local_doc_comment
41 | - lower_acl_than_parent
42 | - missing_docs
43 | - modifier_order
44 | - multiline_arguments
45 | - multiline_arguments_brackets
46 | - multiline_function_chains
47 | - multiline_literal_brackets
48 | - multiline_parameters
49 | - multiline_parameters_brackets
50 | - no_extension_access_modifier
51 | - no_grouping_extension
52 | - number_separator
53 | - operator_usage_whitespace
54 | - optional_enum_case_matching
55 | - prefer_self_in_static_references
56 | - prefer_self_type_over_type_of_self
57 | - prefer_zero_over_explicit_init
58 | - prohibited_interface_builder
59 | - redundant_nil_coalescing
60 | - redundant_type_annotation
61 | - return_value_from_void_function
62 | - shorthand_optional_binding
63 | - sorted_first_last
64 | - sorted_imports
65 | - static_operator
66 | - strict_fileprivate
67 | - switch_case_on_newline
68 | - toggle_bool
69 | - trailing_closure
70 | - type_contents_order
71 | - unneeded_parentheses_in_closure_argument
72 | - yoda_condition
73 |
74 | # Disabled Rules
75 | disabled_rules:
76 | - block_based_kvo
77 | - class_delegate_protocol
78 | - dynamic_inline
79 | - is_disjoint
80 | - no_fallthrough_only
81 | - notification_center_detachment
82 | - ns_number_init_as_function_reference
83 | - nsobject_prefer_isequal
84 | - private_over_fileprivate
85 | - redundant_objc_attribute
86 | - self_in_property_initialization
87 | - todo
88 | - unavailable_condition
89 | - valid_ibinspectable
90 | - xctfail_message
91 |
92 | # Custom Rules
93 | custom_rules:
94 | github_issue:
95 | name: 'GitHub Issue'
96 | regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/AparokshaUI/Adwaita/issues/\d))'
97 | message: 'The related GitHub issue must be included in a TODO or FIXME.'
98 | severity: warning
99 |
100 | fatal_error:
101 | name: 'Fatal Error'
102 | regex: 'fatalError.*\(.*\)'
103 | message: 'Fatal error should not be used.'
104 | severity: error
105 |
106 | enum_case_parameter:
107 | name: 'Enum Case Parameter'
108 | regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)'
109 | message: 'The associated values of an enum case should have parameters.'
110 | severity: warning
111 |
112 | tab:
113 | name: 'Whitespaces Instead of Tab'
114 | regex: '\t'
115 | message: 'Spaces should be used instead of tabs.'
116 | severity: warning
117 |
118 | # Thanks to the creator of the SwiftLint rule
119 | # "empty_first_line"
120 | # https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml
121 | # in the GitHub repository
122 | # "CotEditor"
123 | # https://github.com/coteditor/CotEditor
124 | empty_first_line:
125 | name: 'Empty First Line'
126 | regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)'
127 | message: 'There should be an empty line after a declaration'
128 | severity: error
129 |
130 | # Analyzer Rules
131 | analyzer_rules:
132 | - unused_declaration
133 | - unused_import
134 |
135 | # Options
136 | file_header:
137 | required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// Adwaita\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n'
138 | missing_docs:
139 | warning: [internal, private]
140 | error: [open, public]
141 | excludes_extensions: true
142 | excludes_inherited_types: false
143 | type_contents_order:
144 | order:
145 | - case
146 | - type_alias
147 | - associated_type
148 | - type_property
149 | - instance_property
150 | - ib_inspectable
151 | - ib_outlet
152 | - subscript
153 | - initializer
154 | - deinitializer
155 | - subtype
156 | - type_method
157 | - view_life_cycle_method
158 | - ib_action
159 | - other_method
160 |
161 | excluded:
162 | - Sources/Adwaita/View/Generated/
163 | - Sources/Adwaita/Adwaita.docc/
164 | - .build/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you very much for taking the time for contributing to this project.
4 |
5 | ## Report a Bug
6 | Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
7 |
8 | ## Suggest a New Feature
9 | Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
10 |
11 | ### Suggest a New Component
12 | If you want to be able to access a component from Libadwaita or GTK that is currently not available in Adwaita, you can add it to [Libadwaita](https://github.com/AparokshaUI/Libadwaita).
13 | If you want to add an Adwaita interface for an already existing GTUI widget, open an issue.
14 |
15 | ## Pull Requests
16 | I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
17 |
18 | ### 1. Fork & Clone this Project
19 | Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
20 |
21 | ### 2. Open the Project
22 | Open the project folder in GNOME Builder, Xcode or another IDE.
23 |
24 | ### 3. Understand the Project Structure
25 | - The `README.md` file contains a description of the app or package.
26 | - The `Contributors.md` file contains the names or usernames of all the contributors with a link to their GitHub profile.
27 | - The `LICENSE.md` contains an GPL-3.0 license.
28 | - `CONTRIBUTING.md` is this file.
29 | - Directory `Icons` that contains PNG and PXD (Pixelmator Pro) files for the images used in the app and guides.
30 | - `Sources` contains the source code of the project.
31 | - `Adwaita` contains the source code of the project.
32 | - `Adwaita.docc` contains documentation.
33 | - `Model` is the directory with Adwaita's basis.
34 | - `Data Flow` contains property wrappers and protocols required for managing the updates of a view.
35 | - `Extensions` contains all the extensions of types that are not defined in this project.
36 | - `User Interface` contains protocols and structures that are the basis of presenting content to the user.
37 | - `View` contains structures that conform to the `View` protocol and provide an easier-to-use wrapper around a GTUI `NativeWidgetPeer` type.
38 | - `Window` contains structures that conform to the `Window` protocol and simplify the creation of different types of windows.
39 | - `CAdw` contains the reference to the C library.
40 | - `Generation` contains the code for the auto-generation of Adwaita and Gtk widgets.
41 | - `Tests` contains an example application for testing `Adwaita`.
42 |
43 | ### 4. Edit the Code
44 | Edit the code. If you add a new type, add documentation in the code.
45 |
46 | ### 5. Commit to the Fork
47 | Commit and push the fork.
48 |
49 | ### 6. Pull Request
50 | Open GitHub to submit a pull request. Thank you very much for your contribution!
51 |
--------------------------------------------------------------------------------
/Icons/AdwaitaIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AparokshaUI/adwaita-swift/c8376967ff573abf4e49ade5250883da6a632f30/Icons/AdwaitaIcon.png
--------------------------------------------------------------------------------
/Icons/AdwaitaIcon.pxd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AparokshaUI/adwaita-swift/c8376967ff573abf4e49ade5250883da6a632f30/Icons/AdwaitaIcon.pxd
--------------------------------------------------------------------------------
/Icons/Counter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AparokshaUI/adwaita-swift/c8376967ff573abf4e49ade5250883da6a632f30/Icons/Counter.png
--------------------------------------------------------------------------------
/Icons/GitHubBanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AparokshaUI/adwaita-swift/c8376967ff573abf4e49ade5250883da6a632f30/Icons/GitHubBanner.png
--------------------------------------------------------------------------------
/Icons/GitHubBanner.pxd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AparokshaUI/adwaita-swift/c8376967ff573abf4e49ade5250883da6a632f30/Icons/GitHubBanner.pxd
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 david-swift
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | //
3 | // Package.swift
4 | // Adwaita
5 | //
6 | // Created by david-swift on 08.06.23.
7 | //
8 |
9 | import PackageDescription
10 |
11 | /// The Adwaita package.
12 | let package = Package(
13 | name: "Adwaita",
14 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
15 | products: [
16 | .library(
17 | name: "Adwaita",
18 | targets: ["Adwaita"]
19 | ),
20 | .library(
21 | name: "CAdw",
22 | targets: ["CAdw"]
23 | )
24 | ],
25 | dependencies: [
26 | .package(url: "https://github.com/AparokshaUI/Meta", branch: "main"),
27 | .package(url: "https://github.com/AparokshaUI/meta-sqlite", branch: "main"),
28 | .package(
29 | url: "https://github.com/david-swift/LevenshteinTransformations",
30 | from: "0.1.1"
31 | ),
32 | .package(url: "https://github.com/CoreOffice/XMLCoder", from: "0.17.1")
33 | ],
34 | targets: [
35 | .systemLibrary(
36 | name: "CAdw",
37 | pkgConfig: "libadwaita-1"
38 | ),
39 | .target(
40 | name: "Adwaita",
41 | dependencies: [
42 | "CAdw",
43 | .product(name: "LevenshteinTransformations", package: "LevenshteinTransformations"),
44 | .product(name: "Meta", package: "Meta"),
45 | .product(name: "MetaSQLite", package: "meta-sqlite")
46 | ]
47 | ),
48 | .executableTarget(
49 | name: "Generation",
50 | dependencies: [
51 | .product(name: "XMLCoder", package: "XMLCoder")
52 | ]
53 | ),
54 | .executableTarget(
55 | name: "Demo",
56 | dependencies: ["Adwaita"]
57 | )
58 | ],
59 | swiftLanguageModes: [.v5]
60 | )
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!IMPORTANT]
2 | >
3 | > **This project has moved. You can find it [here](https://git.aparoksha.dev/aparoksha/adwaita-swift).**
4 | >
5 | > The decision is based on [this article](https://sfconservancy.org/GiveUpGitHub/).
6 | >
7 | > Thanks to [No GitHub](https://codeberg.org/NoGitHub) for the badge used below.
8 | >
9 | > [](https://sfconservancy.org/GiveUpGitHub/)
10 |
11 |
12 |
13 |
Adwaita for Swift
14 |
15 |
16 |
17 |
18 | Documentation
19 |
20 | ·
21 |
22 | GitHub
23 |
24 |
25 |
26 | _Adwaita_ is a framework for creating user interfaces for GNOME with an API similar to SwiftUI.
27 |
28 | The following code:
29 |
30 | ```swift
31 | struct Counter: View {
32 |
33 | @State private var count = 0
34 |
35 | var view: Body {
36 | HStack {
37 | Button(icon: .default(icon: .goPrevious)) {
38 | count -= 1
39 | }
40 | Text("\(count)")
41 | .title1()
42 | .frame(minWidth: 100)
43 | Button(icon: .default(icon: .goNext)) {
44 | count += 1
45 | }
46 | }
47 | }
48 |
49 | }
50 | ```
51 |
52 | Describes a simple counter view:
53 |
54 | ![Counter Example][image-1]
55 |
56 | More examples are available in the [demo app][1].
57 |
58 | ## Table of Contents
59 |
60 | - [Goals][2]
61 | - [Installation][4]
62 | - [Usage][5]
63 | - [Thanks][6]
64 |
65 | ## Goals
66 |
67 | _Adwaita_’s main goal is to provide an easy-to-use interface for creating GNOME apps. The backend should stay as simple as possible, while not limiting the possibilities there are with [Libadwaita][7] and [GTK][8].
68 |
69 | If you want to use _Adwaita_ in a project, but there are widgets missing, open an [issue on GitHub][9].
70 |
71 | Find more information about the project's motivation in [this blog post](https://www.swift.org/blog/adwaita-swift/).
72 |
73 | ## Installation
74 | ### Dependencies
75 | #### Flatpak
76 |
77 | It is recommended to develop apps inside of a Flatpak.
78 | That way, you don't have to install Swift or any of the dependencies on your system, and you always have access to the latest versions.
79 | Take a look at the [template repository](https://github.com/AparokshaUI/AdwaitaTemplate).
80 | This works on Linux only.
81 |
82 | #### Directly on System
83 |
84 | You can also run your apps directly on the system.
85 |
86 | If you are using a Linux distribution, install `libadwaita-devel` or `libadwaita` (or something similar, based on the package manager) as well as `gtk4-devel`, `gtk4` or similar.
87 |
88 | On macOS, follow these steps:
89 | 1. Install [Homebrew][11].
90 | 2. Install Libadwaita (and thereby GTK 4):
91 | ```
92 | brew install libadwaita
93 | ```
94 |
95 | ### Swift Package
96 | 1. Open your Swift package in GNOME Builder, Xcode, or any other IDE.
97 | 2. Open the `Package.swift` file.
98 | 3. Into the `Package` initializer, under `dependencies`, paste:
99 | ```swift
100 | .package(url: "https://github.com/AparokshaUI/Adwaita", from: "0.1.0")
101 | ```
102 |
103 | ## Usage
104 |
105 | I recommend using the [template repository](https://github.com/AparokshaUI/AdwaitaTemplate) as a starting point.
106 |
107 | Follow the [interactive tutorial](https://aparokshaui.github.io/adwaita-swift/tutorials/table-of-contents) or [read the docs](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita) in order to get to know _Adwaita for Swift_.
108 |
109 | ## Thanks
110 |
111 | ### Dependencies
112 | - [XMLCoder][18] licensed under the [MIT license][19]
113 | - [Levenshtein Transformations](https://github.com/david-swift/LevenshteinTransformations) licensed under the [MIT license](https://github.com/david-swift/LevenshteinTransformations/blob/main/LICENSE.md)
114 |
115 | ### Other Thanks
116 | - The [contributors][20]
117 | - The auto-generation of widgets is based on [Swift Cross UI](https://github.com/stackotter/swift-cross-ui)
118 | - [SwiftLint][21] for checking whether code style conventions are violated
119 | - The programming language [Swift][22]
120 |
121 | [1]: Tests/
122 | [2]: #goals
123 | [4]: #installation
124 | [5]: #usage
125 | [6]: #thanks
126 | [7]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html
127 | [8]: https://docs.gtk.org/gtk4/
128 | [9]: https://github.com/AparokshaUI/Adwaita/issues
129 | [10]: https://github.com/AparokshaUI/Libadwaita
130 | [11]: https://brew.sh
131 | [12]: user-manual/GettingStarted.md
132 | [13]: user-manual/Basics/HelloWorld.md
133 | [14]: user-manual/Basics/CreatingViews.md
134 | [15]: user-manual/Basics/Windows.md
135 | [16]: user-manual/Basics/KeyboardShortcuts.md
136 | [17]: user-manual/Advanced/CreatingWidgets.md
137 | [18]: https://github.com/CoreOffice/XMLCoder
138 | [19]: https://github.com/CoreOffice/XMLCoder/blob/main/LICENSE
139 | [20]: Contributors.md
140 | [21]: https://github.com/realm/SwiftLint
141 | [22]: https://github.com/apple/swift
142 | [23]: https://github.com/SourceDocs/SourceDocs
143 |
144 | [image-1]: Icons/Counter.png
145 | [image-2]: Icons/Demo.png
146 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/MenuButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuButton.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 22.10.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A button widget for menus.
11 | public struct MenuButton: MenuWidget {
12 |
13 | /// The button's label.
14 | var label: String
15 | /// The button's action handler.
16 | var handler: () -> Void
17 | /// The keyboard shortcut.
18 | var shortcut = ""
19 | /// Whether to prefer adding the action to the application window.
20 | var preferApplicationWindow: Bool
21 |
22 | /// The action label.
23 | var filteredLabel: String { label.filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "." } }
24 |
25 | /// Initialize a menu button.
26 | /// - Parameters:
27 | /// - label: The buttons label.
28 | /// - window: Whether to prefer adding the action to the application window.
29 | /// - handler: The button's action handler.
30 | public init(_ label: String, window: Bool = true, handler: @escaping () -> Void) {
31 | self.label = label
32 | preferApplicationWindow = window
33 | self.handler = handler
34 | }
35 |
36 | /// The view storage.
37 | /// - Parameters:
38 | /// - modifiers: Modify the views before updating.
39 | /// - type: The type of the views.
40 | /// - Returns: The view storage.
41 | public func container(
42 | data: WidgetData,
43 | type: Data.Type
44 | ) -> ViewStorage where Data: ViewRenderData {
45 | let storage = ViewStorage(nil)
46 | var label = filteredLabel
47 | guard let app = data.appStorage as? AdwaitaApp else {
48 | return .init(nil)
49 | }
50 | if let window = data.sceneStorage.pointer as? AdwaitaWindow, preferApplicationWindow {
51 | app.addKeyboardShortcut(shortcut, id: filteredLabel, window: window, handler: handler)
52 | label = "win." + label
53 | } else {
54 | app.addKeyboardShortcut(shortcut, id: filteredLabel, handler: handler)
55 | label = "app." + label
56 | }
57 | let pointer = g_menu_item_new(self.label, label)
58 | storage.pointer = pointer
59 | return storage
60 | }
61 |
62 | /// Update the stored content.
63 | /// - Parameters:
64 | /// - storage: The storage to update.
65 | /// - modifiers: Modify the views before updating.
66 | /// - updateProperties: Whether to update the properties.
67 | /// - type: The type of the views.
68 | public func update(
69 | _ storage: ViewStorage,
70 | data: WidgetData,
71 | updateProperties: Bool,
72 | type: Data.Type
73 | ) where Data: ViewRenderData {
74 | guard let app = data.appStorage as? AdwaitaApp else {
75 | return
76 | }
77 | if let window = data.sceneStorage.pointer as? AdwaitaWindow, preferApplicationWindow {
78 | app.addKeyboardShortcut(shortcut, id: filteredLabel, window: window, handler: handler)
79 | } else {
80 | app.addKeyboardShortcut(shortcut, id: filteredLabel, handler: handler)
81 | }
82 | }
83 |
84 | /// Create a keyboard shortcut for an application from a button.
85 | ///
86 | /// Note that the keyboard shortcut is available after the view has been visible for the first time.
87 | /// - Parameter shortcut: The keyboard shortcut.
88 | /// - Returns: The button.
89 | public func keyboardShortcut(_ shortcut: String) -> Self {
90 | modify { $0.shortcut = shortcut }
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/MenuCollection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuCollection.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 02.08.2024.
6 | //
7 |
8 | import CAdw
9 | import Foundation
10 |
11 | /// A collection of menus.
12 | public struct MenuCollection: MenuWidget, Wrapper {
13 |
14 | /// The content of the collection.
15 | var content: Body
16 |
17 | /// Initialize a menu.
18 | /// - Parameter content: The content of the collection.
19 | public init(@ViewBuilder content: @escaping () -> Body) {
20 | self.content = content()
21 | }
22 |
23 | /// The view storage.
24 | /// - Parameters:
25 | /// - modifiers: Modify the views before updating.
26 | /// - type: The type of the views.
27 | /// - Returns: The view storage.
28 | public func container(
29 | data: WidgetData,
30 | type: Data.Type
31 | ) -> ViewStorage where Data: ViewRenderData {
32 | let storages = content.storages(data: data, type: type)
33 | return .init(nil, content: [.mainContent: storages])
34 | }
35 |
36 | /// Update the stored content.
37 | /// - Parameters:
38 | /// - storage: The storage to update.
39 | /// - modifiers: Modify the views before updating.
40 | /// - updateProperties: Whether to update the properties.
41 | /// - type: The type of the views.
42 | public func update(
43 | _ storage: ViewStorage,
44 | data: WidgetData,
45 | updateProperties: Bool,
46 | type: Data.Type
47 | ) where Data: ViewRenderData {
48 | guard let storages = storage.content[.mainContent] else {
49 | return
50 | }
51 | content.update(storages, data: data, updateProperties: updateProperties, type: type)
52 | }
53 |
54 | /// Render the collection as a menu.
55 | /// - Parameter data: The widget data.
56 | /// - Returns: The view storage with the GMenu as the pointer.
57 | public func getMenu(data: WidgetData) -> ViewStorage {
58 | let menu = g_menu_new()
59 | let storage = container(data: data.noModifiers, type: MenuContext.self)
60 | if let app = data.appStorage as? AdwaitaApp, let window = data.sceneStorage.pointer as? AdwaitaWindow {
61 | initializeMenu(menu: menu, storage: storage, app: app, window: window)
62 | }
63 | storage.pointer = menu
64 | return storage
65 | }
66 |
67 | /// Initialize a menu.
68 | /// - Parameters:
69 | /// - menu: The pointer to the GMenu.
70 | /// - storage: The storage for the menu's content.
71 | /// - app: The app object.
72 | /// - window: The window object.
73 | func initializeMenu(menu: OpaquePointer?, storage: ViewStorage, app: AdwaitaApp, window: AdwaitaWindow?) {
74 | if let item = storage.opaquePointer {
75 | g_menu_append_item(menu, item)
76 | storage.pointer = item
77 | } else {
78 | for element in storage.content[.mainContent] ?? [] {
79 | initializeMenu(menu: menu, storage: element, app: app, window: window)
80 | }
81 | }
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/MenuContext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuContext.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 01.08.24.
6 | //
7 |
8 | /// The menu items view context.
9 | public enum MenuContext: ViewRenderData {
10 |
11 | /// The type of the widgets.
12 | public typealias WidgetType = MenuWidget
13 | /// The wrapper type.
14 | public typealias WrapperType = MenuCollection
15 | /// The either view type.
16 | public typealias EitherViewType = MenuEitherView
17 |
18 | }
19 |
20 | /// The type of the widgets.
21 | public protocol MenuWidget: Meta.Widget { }
22 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/MenuEitherView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuEitherView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 06.08.2024.
6 | //
7 |
8 | /// Show one of two views depending on a condition.
9 | public struct MenuEitherView: Meta.EitherView, SimpleView {
10 |
11 | /// The view.
12 | public var view: Body
13 |
14 | /// Initialize an either view.
15 | /// - Parameters:
16 | /// - condition: The condition.
17 | /// - view1: The first view.
18 | /// - view2: The second view.
19 | public init(_ condition: Bool, view1: () -> Body, else view2: () -> Body) {
20 | self.view = condition ? view1() : view2()
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/MenuSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuButton.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 22.10.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A button widget for menus.
11 | public struct MenuSection: MenuWidget {
12 |
13 | /// The content of the section.
14 | var sectionContent: Body
15 |
16 | /// Initialize a section for menus.
17 | /// - Parameter content: The content of the section.
18 | public init(@ViewBuilder content: () -> Body) {
19 | self.sectionContent = content()
20 | }
21 |
22 | /// The view storage.
23 | /// - Parameters:
24 | /// - modifiers: Modify the views before updating.
25 | /// - type: The type of the views.
26 | /// - Returns: The view storage.
27 | public func container(
28 | data: WidgetData,
29 | type: Data.Type
30 | ) -> ViewStorage where Data: ViewRenderData {
31 | let storage = ViewStorage(nil)
32 | let childStorage = MenuCollection { sectionContent }.getMenu(data: data)
33 | storage.content[.mainContent] = [childStorage]
34 | let pointer = g_menu_item_new_section(nil, childStorage.opaquePointer?.cast())
35 | storage.pointer = pointer
36 | return storage
37 | }
38 |
39 | /// Update the stored content.
40 | /// - Parameters:
41 | /// - storage: The storage to update.
42 | /// - modifiers: Modify the views before updating.
43 | /// - updateProperties: Whether to update the properties.
44 | /// - type: The type of the views.
45 | public func update(
46 | _ storage: ViewStorage,
47 | data: WidgetData,
48 | updateProperties: Bool,
49 | type: Data.Type
50 | ) where Data: ViewRenderData {
51 | guard let content = storage.content[.mainContent]?.first else {
52 | return
53 | }
54 | MenuCollection { sectionContent }
55 | .updateStorage(content, data: data, updateProperties: updateProperties, type: MenuContext.self)
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Menu/Submenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Submenu.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 22.10.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A submenu widget.
11 | public struct Submenu: MenuWidget {
12 |
13 | /// The label of the submenu.
14 | var label: String
15 | /// The content of the submenu.
16 | var content: Body
17 |
18 | /// Initialize a submenu.
19 | /// - Parameters:
20 | /// - label: The submenu's label.
21 | /// - content: The content of the section.
22 | public init(_ label: String, @ViewBuilder content: () -> Body) {
23 | self.label = label
24 | self.content = content()
25 | }
26 |
27 | /// The view storage.
28 | /// - Parameters:
29 | /// - modifiers: Modify the views before updating.
30 | /// - type: The type of the views.
31 | /// - Returns: The view storage.
32 | public func container(
33 | data: WidgetData,
34 | type: Data.Type
35 | ) -> ViewStorage where Data: ViewRenderData {
36 | let storage = ViewStorage(nil)
37 | let childStorage = MenuCollection { content }.getMenu(data: data)
38 | storage.content[.mainContent] = [childStorage]
39 | let pointer = g_menu_item_new_submenu(label, childStorage.opaquePointer?.cast())
40 | storage.pointer = pointer
41 | return storage
42 | }
43 |
44 | /// Update the stored content.
45 | /// - Parameters:
46 | /// - storage: The storage to update.
47 | /// - modifiers: Modify the views before updating.
48 | /// - updateProperties: Whether to update the properties.
49 | /// - type: The type of the views.
50 | public func update(
51 | _ storage: ViewStorage,
52 | data: WidgetData,
53 | updateProperties: Bool,
54 | type: Data.Type
55 | ) where Data: ViewRenderData {
56 | guard let content = storage.content[.mainContent]?.first else {
57 | return
58 | }
59 | MenuCollection { self.content }
60 | .updateStorage(content, data: data, updateProperties: updateProperties, type: MenuContext.self)
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/AdwaitaApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdwaitaApp.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 31.07.2024.
6 | //
7 |
8 | import CAdw
9 | import Foundation
10 | @_exported import Meta
11 | @_exported import MetaSQLite
12 |
13 | /// The Meta app storage for the Adwaita backend.
14 | public class AdwaitaApp: AppStorage {
15 |
16 | /// The scene element type of the Adwaita backend.
17 | public typealias SceneElementType = AdwaitaSceneElement
18 | /// The widget type of the Adwaita backend.
19 | public typealias WidgetType = AdwaitaWidget
20 | /// The wrapper type of the Adwaita backend.
21 | public typealias WrapperType = VStack
22 |
23 | /// The app storage.
24 | public var storage: StandardAppStorage = .init()
25 | /// The signal data for running.
26 | var runSignal: SignalData?
27 | /// The application's pointer.
28 | var pointer: UnsafeMutablePointer?
29 | /// Fields for storing closure data.
30 | var signals: [String: SignalData] = [:]
31 |
32 | /// Initialize the app storage.
33 | /// - Parameter id: The identifier.
34 | public required init(id: String) {
35 | pointer = adw_application_new(id, G_APPLICATION_DEFAULT_FLAGS)?.cast()
36 | DatabaseInformation.setPath(Self.userDataDir().appendingPathComponent("data.sqlite").path)
37 | }
38 |
39 | /// Copy a string to the clipboard.
40 | public static func copy(_ text: String) {
41 | let clipboard = gdk_display_get_clipboard(gdk_display_get_default())
42 | gdk_clipboard_set_text(clipboard, text)
43 | }
44 |
45 | /// The directory used for storing user data.
46 | /// - Returns: The URL.
47 | public static func userDataDir() -> URL {
48 | .init(fileURLWithPath: .init(cString: g_get_user_data_dir()))
49 | }
50 |
51 | /// Execute the app.
52 | /// - Parameter setup: Set the scene elements up.
53 | public func run(setup: @escaping () -> Void) {
54 | let data = SignalData {
55 | setup()
56 | }
57 | runSignal = data
58 | data.connect(pointer: pointer, signal: "activate")
59 | g_application_run(pointer?.cast(), 0, nil)
60 | }
61 |
62 | /// Quit the app.
63 | public func quit() {
64 | g_application_quit(pointer?.cast())
65 | }
66 |
67 | /// Add a keyboard shortcut to the application.
68 | /// - Parameters:
69 | /// - shortcut: The keyboard shortcut.
70 | /// - id: The action's id.
71 | /// - window: Optionally an application window.
72 | /// - handler: The action's handler.
73 | func addKeyboardShortcut(
74 | _ shortcut: String,
75 | id: String,
76 | window: AdwaitaWindow? = nil,
77 | handler: @escaping () -> Void
78 | ) {
79 | if window == nil, let data = signals[id] {
80 | data.closure = { _ in handler() }
81 | return
82 | } else if let data = window?.signals[id] {
83 | data.closure = { _ in handler() }
84 | return
85 | }
86 | let action = g_simple_action_new(id, nil)
87 | let data = SignalData(closure: handler)
88 | g_signal_connect_data(
89 | action?.cast(),
90 | "activate",
91 | unsafeBitCast(data.threeParamsHandler, to: GCallback.self),
92 | Unmanaged.passUnretained(data).toOpaque().cast(),
93 | nil,
94 | G_CONNECT_AFTER
95 | )
96 | if let window {
97 | g_action_map_add_action(.init(window.pointer), action)
98 | window.signals[id] = data
99 | } else {
100 | g_action_map_add_action(.init(pointer), action)
101 | signals[id] = data
102 | }
103 | gtk_application_set_accels_for_action(pointer, (window == nil ? "app." : "win.") + id, [shortcut].cArray)
104 | }
105 |
106 | /// Remove a keyboard shortcut from the application.
107 | /// - Parameters:
108 | /// - id: The keyboard shortcut's id.
109 | /// - window: Optionally an application window.
110 | func removeKeyboardShortcut(
111 | id: String,
112 | window: AdwaitaWindow? = nil
113 | ) {
114 | if let window {
115 | g_action_map_remove_action(.init(window.pointer), id)
116 | window.signals.removeValue(forKey: id)
117 | } else {
118 | g_action_map_remove_action(.init(pointer), id)
119 | signals.removeValue(forKey: id)
120 | }
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/AdwaitaMainView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdwaitaMainView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 31.07.2024.
6 | //
7 |
8 | /// The type of widgets of the Adwaita backend.
9 | public enum AdwaitaMainView: ViewRenderData {
10 |
11 | /// The type of the widgets.
12 | public typealias WidgetType = AdwaitaWidget
13 | /// The wrapper type.
14 | public typealias WrapperType = VStackWrapper
15 | /// The either view type.
16 | public typealias EitherViewType = EitherView
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/AdwaitaSceneElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdwaitaSceneElement.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 31.07.2024.
6 | //
7 |
8 | /// The type of scene elements of the Adwaita backend.
9 | public protocol AdwaitaSceneElement: SceneElement { }
10 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/AdwaitaWidget.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdwaitaWidget.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 31.07.2024.
6 | //
7 |
8 | /// The type of widgets of the Adwaita backend.
9 | public protocol AdwaitaWidget: Widget { }
10 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Enumerations/Alignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alignment.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// The alignment for a widget.
11 | public enum Alignment: Int {
12 |
13 | /// The widget will fill the available space.
14 | case fill
15 | /// The widget will start at the beginning of the available space.
16 | case start
17 | /// The widget will end at the end of the available space.
18 | case end
19 | /// The widget will be centered in the available space.
20 | case center
21 | /// The widget will be baseline aligned in the available space.
22 | case baselineFill
23 | /// The widget will be baseline aligned at the start of the available space.
24 | case baselineCenter
25 |
26 | /// Get the GtkAlign alignment.
27 | public var cAlign: GtkAlign {
28 | switch self {
29 | case .fill:
30 | return GTK_ALIGN_FILL
31 | case .start:
32 | return GTK_ALIGN_START
33 | case .end:
34 | return GTK_ALIGN_END
35 | case .center:
36 | return GTK_ALIGN_CENTER
37 | case .baselineFill:
38 | return GTK_ALIGN_BASELINE_FILL
39 | case .baselineCenter:
40 | return GTK_ALIGN_BASELINE_CENTER
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Enumerations/ContentFit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentFit.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 19.07.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// Control how a content should be made to fit inside an allocation.
11 | public enum ContentFit: UInt32 {
12 |
13 | /// Make the content fill the entire allocation,
14 | /// without taking its aspect ratio in consideration.
15 | case fill
16 | /// Scale the content to fit the allocation,
17 | /// while taking its aspect ratio in consideration.
18 | case contain
19 | /// Cover the entire allocation,
20 | /// while taking the content aspect ratio in consideration.
21 | case cover
22 | /// The content is scaled down to fit the allocation, if needed,
23 | /// otherwise its original size is used.
24 | case scaleDown
25 |
26 | /// The ContentFit value as a GtkContentFit value.
27 | var gtkValue: GtkContentFit {
28 | .init(rawValue)
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Enumerations/Edge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Edge.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.24.
6 | //
7 |
8 | /// The edges for a widget.
9 | public enum Edge {
10 |
11 | /// The leading (start) edge.
12 | case leading
13 | /// The trailing (end) edge.
14 | case trailing
15 | /// The top edge.
16 | case top
17 | /// The bottom edge.
18 | case bottom
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Enumerations/Transition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transition.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A transition for a stack.
11 | public enum Transition: Int {
12 |
13 | // swiftlint:disable missing_docs discouraged_none_name
14 | case none
15 | case crossfade
16 | case slideRight, slideLeft, slideUp, slideDown, slideLeftRight, slideUpDown
17 | case coverUp, coverDown, coverLeft, coverRight
18 | case uncoverUp, uncoverDown, uncoverLeft, uncoverRight
19 | case coverUpDown, coverDownUp, coverLeftRight, coverRightLeft
20 | case rotateLeft, rotateRight, rotateLeftRight
21 | // swiftlint:enable missing_docs discouraged_none_name
22 |
23 | /// Get the GtkStackTransitionType transition.
24 | public var cTransition: GtkStackTransitionType {
25 | switch self {
26 | case .none:
27 | return GTK_STACK_TRANSITION_TYPE_NONE
28 | case .crossfade:
29 | return GTK_STACK_TRANSITION_TYPE_CROSSFADE
30 | case .slideRight:
31 | return GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT
32 | case .slideLeft:
33 | return GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT
34 | case .slideUp:
35 | return GTK_STACK_TRANSITION_TYPE_SLIDE_UP
36 | case .slideDown:
37 | return GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN
38 | case .slideLeftRight:
39 | return GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT
40 | case .slideUpDown:
41 | return GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN
42 | case .coverUp:
43 | return GTK_STACK_TRANSITION_TYPE_OVER_UP
44 | case .coverDown:
45 | return GTK_STACK_TRANSITION_TYPE_OVER_DOWN
46 | case .coverLeft:
47 | return GTK_STACK_TRANSITION_TYPE_OVER_LEFT
48 | case .coverRight:
49 | return GTK_STACK_TRANSITION_TYPE_OVER_RIGHT
50 | case .uncoverUp:
51 | return GTK_STACK_TRANSITION_TYPE_UNDER_UP
52 | case .uncoverDown:
53 | return GTK_STACK_TRANSITION_TYPE_UNDER_DOWN
54 | case .uncoverLeft:
55 | return GTK_STACK_TRANSITION_TYPE_UNDER_LEFT
56 | case .uncoverRight:
57 | return GTK_STACK_TRANSITION_TYPE_UNDER_RIGHT
58 | case .coverUpDown:
59 | return GTK_STACK_TRANSITION_TYPE_OVER_UP_DOWN
60 | case .coverDownUp:
61 | return GTK_STACK_TRANSITION_TYPE_OVER_DOWN_UP
62 | case .coverLeftRight:
63 | return GTK_STACK_TRANSITION_TYPE_OVER_LEFT_RIGHT
64 | case .coverRightLeft:
65 | return GTK_STACK_TRANSITION_TYPE_OVER_RIGHT_LEFT
66 | case .rotateLeft:
67 | return GTK_STACK_TRANSITION_TYPE_ROTATE_LEFT
68 | case .rotateRight:
69 | return GTK_STACK_TRANSITION_TYPE_ROTATE_RIGHT
70 | case .rotateLeftRight:
71 | return GTK_STACK_TRANSITION_TYPE_ROTATE_LEFT_RIGHT
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/Array.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 06.08.23.
6 | //
7 |
8 | extension Array where Element == String {
9 |
10 | /// Get the C version of the array.
11 | var cArray: UnsafePointer?>? {
12 | let cStrings = self.map { $0.utf8CString }
13 | let cStringPointers = cStrings.map { $0.withUnsafeBufferPointer { $0.baseAddress } }
14 | let optionalCStringPointers = cStringPointers + [nil]
15 | var optionalCStringPointersCopy = optionalCStringPointers
16 | optionalCStringPointersCopy.withUnsafeMutableBufferPointer { bufferPointer in
17 | bufferPointer.baseAddress?.advanced(by: cStrings.count).pointee = nil
18 | }
19 | let flatArray = optionalCStringPointersCopy.compactMap { $0 }
20 | let pointer = UnsafeMutablePointer?>.allocate(capacity: flatArray.count + 1)
21 | for (index, element) in flatArray.enumerated() {
22 | pointer.advanced(by: index).pointee = element
23 | }
24 | pointer.advanced(by: flatArray.count).pointee = nil
25 | return UnsafePointer(pointer)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/Bool.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bool.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | extension Bool {
9 |
10 | /// Get the gboolean for C.
11 | public var cBool: Int32 {
12 | self ? 1 : 0
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/Int.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | extension Int: @retroactive Identifiable {
9 |
10 | /// Get the integer itself as the identifier.
11 | public var id: Int { self }
12 | /// The C integer.
13 | public var cInt: Int32 {
14 | .init(truncatingIfNeeded: self)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/OpaquePointer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpaquePointer.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.23.
6 | //
7 |
8 | extension OpaquePointer {
9 |
10 | /// Convert an opaque pointer into an unsafe mutable pointer with a defined type.
11 | /// - Returns: The unsafe mutable pointer.
12 | public func cast() -> UnsafeMutablePointer {
13 | .init(self)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/Set.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Set.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.24.
6 | //
7 |
8 | extension Set where Element == Edge {
9 |
10 | /// Horizontal and vertical edges.
11 | public static var all: Self { vertical.union(horizontal) }
12 |
13 | /// Top and bottom edges.
14 | public static var vertical: Self { top.union(bottom) }
15 |
16 | /// Leading and trailing edges.
17 | public static var horizontal: Self { leading.union(trailing) }
18 |
19 | /// Top edge.
20 | public static var top: Self { [.top] }
21 |
22 | /// Bottom edge.
23 | public static var bottom: Self { [.bottom] }
24 |
25 | /// Leading edge.
26 | public static var leading: Self { [.leading] }
27 |
28 | /// Trailing edge.
29 | public static var trailing: Self { [.trailing] }
30 |
31 | /// Add a collection of edges to a collection of edges.
32 | /// - Parameter edges: The collection of edges.
33 | /// - Returns: Both collections combined.
34 | public func add(_ edges: Self) -> Self { union(edges) }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 09.09.23.
6 | //
7 |
8 | extension String {
9 |
10 | /// A label for the transition data in a widget's fields.
11 | static var transition: Self { "transition" }
12 | /// A label for the navigation label in a widget's fields.
13 | static var navigationLabel: Self { "navigation-label" }
14 |
15 | /// Add the Ctrl key to a shortcut.
16 | /// - Returns: The shortcut.
17 | public func ctrl() -> String { "\(self)" }
18 |
19 | /// Add the Shift key to a shortcut.
20 | /// - Returns: The shortcut.
21 | public func shift() -> String { "\(self)" }
22 |
23 | /// Add the Alt key to a shortcut.
24 | /// - Returns: The shortcut.
25 | public func alt() -> String { "\(self)" }
26 |
27 | /// Add the Meta key to a shortcut.
28 | /// - Returns: The shortcut.
29 | public func meta() -> String { "\(self)" }
30 |
31 | /// Add the Super key to a shortcut.
32 | /// - Returns: The shortcut.
33 | public func `super`() -> String { "\(self)" }
34 |
35 | /// Add the Hyper key to a shortcut.
36 | /// - Returns: The shortcut.
37 | public func hyper() -> String { "\(self)" }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/UInt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UInt.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 19.01.24.
6 | //
7 |
8 | extension UInt {
9 |
10 | /// Convert an unsigned integer into the C form.
11 | public var cInt: UInt32 {
12 | .init(truncatingIfNeeded: self)
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/UnsafeMutablePointer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnsafeMutablePointer.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | extension UnsafeMutablePointer {
9 |
10 | /// Convert into an opaque pointer.
11 | /// - Returns: The opaque pointer.
12 | public func opaque() -> OpaquePointer {
13 | .init(self)
14 | }
15 |
16 | /// Convert into an unsafe mutable pointer of another type.
17 | /// - Returns: The unsafe mutable pointer.
18 | public func cast() -> UnsafeMutablePointer {
19 | let pointer = UnsafeMutableRawPointer(self).bindMemory(to: T.self, capacity: 1)
20 | return UnsafeMutablePointer(mutating: pointer)
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/UnsafeMutableRawPointer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnsafeMutableRawPointer.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | extension UnsafeMutableRawPointer {
9 |
10 | /// Convert into an unsafe mutable pointer of a certain type.
11 | /// - Returns: The unsafe mutable pointer.
12 | public func cast() -> UnsafeMutablePointer {
13 | let pointer = UnsafeMutableRawPointer(self).bindMemory(to: T.self, capacity: 1)
14 | return UnsafeMutablePointer(mutating: pointer)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Extensions/ViewStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewStorage.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 01.08.24.
6 | //
7 |
8 | extension ViewStorage {
9 |
10 | /// Modify the view.
11 | /// - Parameter modify: The modification function.
12 | public func modify(_ modify: (OpaquePointer?) -> Void) {
13 | modify(opaquePointer)
14 | }
15 |
16 | /// Convert the pointer to a pointer of a certain type and modify the view.
17 | /// - Parameters:
18 | /// - type: The pointer's type.
19 | /// - modify: The modification function.
20 | public func modify(_ type: T.Type, _ modify: (UnsafeMutablePointer?) -> Void) {
21 | modify(opaquePointer?.cast())
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Idle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Idle.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 12.08.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// Add a task to GLib's idle.
11 | public struct Idle {
12 |
13 | /// The idle handler.
14 | static let handler = IdleHandler()
15 |
16 | /// Run a function whenever there are no higher priority events pending to the default main loop.
17 | /// - Parameters:
18 | /// - priority: The task's priority, default priority by default.
19 | /// - closure: The closure to run.
20 | @discardableResult
21 | public init(
22 | priority: Priority = .defaultIdle,
23 | closure: @escaping () -> Void
24 | ) {
25 | Self.handler.add({ closure(); return false }, priority: .init(priority.rawValue))
26 | }
27 |
28 | /// Repeat a function with a certain delay.
29 | /// - Parameters:
30 | /// - delay: The delay between the repetitions.
31 | /// - priority: The task's priority, default priority by default.
32 | /// - closure: The closure to run. Return if you want to exit the loop.
33 | @discardableResult
34 | @available(macOS, introduced: 13)
35 | public init(
36 | delay: Duration,
37 | priority: Priority = .defaultIdle,
38 | closure: @escaping () -> Bool
39 | ) {
40 | let secondsToMilliseconds: Int64 = 1_000
41 | let attosecondsToMilliseconds: Int64 = 1_000_000_000_000_000
42 | let milliseconds = delay.components.seconds * secondsToMilliseconds
43 | + (delay.components.attoseconds / attosecondsToMilliseconds)
44 | self.init(delay: milliseconds, priority: priority, closure: closure)
45 | }
46 |
47 | /// Repeat a function with a certain delay.
48 | /// - Parameters:
49 | /// - delay: The delay between the repetitions in milliseconds.
50 | /// - priority: The task's priority, default priority by default.
51 | /// - closure: The closure to run. Return if you want to exit the loop.
52 | @discardableResult
53 | @available(macOS, deprecated: 13)
54 | public init(
55 | delay: Int64,
56 | priority: Priority = .defaultIdle,
57 | closure: @escaping () -> Bool
58 | ) {
59 | Self.handler.add(closure, priority: .init(priority.rawValue), delay: delay)
60 | }
61 |
62 | /// The priority of an idle task.
63 | public enum Priority: Int {
64 |
65 | /// A very low priority background task.
66 | case low = 300
67 | /// A high priority event source.
68 | case high = -100
69 | /// A default priority event source.
70 | case `default` = 0
71 | /// A high priority idle function.
72 | case highIdle = 100
73 | /// A default priority idle function.
74 | case defaultIdle = 200
75 |
76 | }
77 |
78 | /// An idle handler.
79 | class IdleHandler {
80 |
81 | /// Add a function to be called whenever there are no higher priority events pending to the default main loop.
82 | /// - Parameter closure: The function.
83 | func add(_ closure: @escaping () -> Bool, priority: Int32, delay: Int64? = nil) {
84 | let context = UnsafeMutableRawPointer(Unmanaged.passRetained(ClosureContainer(closure: closure)).toOpaque())
85 | if let delay {
86 | // swiftlint:disable prefer_self_in_static_references
87 | g_timeout_add_full(priority, .init(delay), { IdleHandler.run(pointer: $0) }, context, nil)
88 | // swiftlint:enable prefer_self_in_static_references
89 | } else {
90 | // swiftlint:disable prefer_self_in_static_references
91 | g_idle_add_full(priority, { IdleHandler.run(pointer: $0) }, context, nil)
92 | // swiftlint:enable prefer_self_in_static_references
93 | }
94 | }
95 |
96 | /// Execute the function.
97 | /// - Parameter pointer: The closure wrapper's pointer.
98 | static func run(pointer: gpointer?) -> Int32 {
99 | if let pointer {
100 | let container = Unmanaged.fromOpaque(pointer).takeUnretainedValue()
101 | let result = container.closure()
102 | if !result {
103 | Unmanaged.fromOpaque(pointer).release()
104 | }
105 | return result.cBool
106 | }
107 | return G_SOURCE_REMOVE
108 | }
109 |
110 | }
111 |
112 | /// A reference type holding a closure.
113 | class ClosureContainer {
114 |
115 | /// The closure.
116 | var closure: () -> Bool
117 |
118 | /// Initialize an object.
119 | /// - Parameter closure: The closure.
120 | init(closure: @escaping () -> Bool) {
121 | self.closure = closure
122 | }
123 |
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Signals/SignalData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignalData.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 31.07.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// Data to pass to signal handlers.
11 | public class SignalData {
12 |
13 | /// The closure.
14 | public var closure: ([Any?]) -> Void
15 |
16 | /// The closure as a C handler.
17 | var handler: @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void {
18 | { _, data in
19 | let data = unsafeBitCast(data, to: SignalData.self)
20 | data.closure([])
21 | }
22 | }
23 |
24 | /// The closure as a C handler with three parameters.
25 | var threeParamsHandler: @convention(c) (
26 | UnsafeMutableRawPointer,
27 | UnsafeRawPointer?,
28 | UnsafeMutableRawPointer
29 | ) -> Void {
30 | { _, arg1, data in
31 | let data = unsafeBitCast(data, to: SignalData.self)
32 | data.closure([arg1])
33 | }
34 | }
35 |
36 | /// The closure as a C handler with four parameters.
37 | var fourParamsHandler: @convention(c) (
38 | UnsafeMutableRawPointer,
39 | UnsafeRawPointer?,
40 | UnsafeRawPointer?,
41 | UnsafeMutableRawPointer
42 | ) -> Void {
43 | { _, arg1, arg2, data in
44 | let data = unsafeBitCast(data, to: SignalData.self)
45 | data.closure([arg1, arg2])
46 | }
47 | }
48 |
49 | /// The closure as a C handler with five parameters.
50 | var fiveParamsHandler: @convention(c) (
51 | UnsafeMutableRawPointer,
52 | UnsafeRawPointer?,
53 | Double,
54 | Double,
55 | UnsafeMutableRawPointer
56 | ) -> Void {
57 | { _, arg1, arg2, arg3, data in
58 | let data = unsafeBitCast(data, to: SignalData.self)
59 | data.closure([arg1, arg2, arg3])
60 | }
61 | }
62 |
63 | /// Initialize the signal data.
64 | /// - Parameter closure: The signal's closure.
65 | public convenience init(closure: @escaping () -> Void) {
66 | self.init { _ in closure() }
67 | }
68 |
69 | /// Initialize the signal data.
70 | /// - Parameter closure: The signal's closure.
71 | public init(closure: @escaping ([Any]) -> Void) {
72 | self.closure = closure
73 | }
74 |
75 | /// Connect the signal data to a signal.
76 | /// - Parameters:
77 | /// - pointer: The pointer to the object which holds the signal.
78 | /// - signal: The signal's name.
79 | /// - argCount: The number of arguments.
80 | public func connect(pointer: UnsafeMutableRawPointer?, signal: String, argCount: Int = 0) {
81 | let callback: GCallback
82 | if argCount >= 3 {
83 | callback = unsafeBitCast(fiveParamsHandler, to: GCallback.self)
84 | } else if argCount == 2 {
85 | callback = unsafeBitCast(fourParamsHandler, to: GCallback.self)
86 | } else if argCount == 1 {
87 | callback = unsafeBitCast(threeParamsHandler, to: GCallback.self)
88 | } else {
89 | callback = unsafeBitCast(handler, to: GCallback.self)
90 | }
91 | g_signal_connect_data(
92 | pointer,
93 | signal,
94 | callback,
95 | Unmanaged.passUnretained(self).toOpaque().cast(),
96 | nil,
97 | G_CONNECT_AFTER
98 | )
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/Storage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 06.08.24.
6 | //
7 |
8 | /// A storage type is a view storage or a scene storage.
9 | public protocol Storage: AnyObject {
10 |
11 | /// The pointer.
12 | var opaquePointer: OpaquePointer? { get }
13 | /// Additional fields.
14 | var fields: [String: Any] { get set }
15 |
16 | }
17 |
18 | extension Storage {
19 |
20 | /// Connect a handler to the observer of a property.
21 | /// - Parameters:
22 | /// - name: The property's name.
23 | /// - id: The handlers id to separate form others connecting to the signal.
24 | /// - pointer: A custom pointer instead of the stored one.
25 | /// - handler: The signal's handler.
26 | public func notify(
27 | name: String,
28 | id: String = "",
29 | pointer: OpaquePointer? = nil,
30 | handler: @escaping () -> Void
31 | ) {
32 | let name = "notify::" + name
33 | connectSignal(name: name, id: id, argCount: 1, pointer: pointer, handler: handler)
34 | }
35 |
36 | /// Connect a handler to a signal.
37 | /// - Parameters:
38 | /// - name: The signal's name.
39 | /// - id: The handlers id to separate form others connecting to the signal.
40 | /// - connectFlags: The GConnectFlags.
41 | /// - argCount: The number of additional arguments (without the first and the last one).
42 | /// - pointer: A custom pointer instead of the stored one.
43 | /// - handler: The signal's handler.
44 | public func connectSignal(
45 | name: String,
46 | id: String = "",
47 | argCount: Int = 0,
48 | pointer: OpaquePointer? = nil,
49 | handler: @escaping () -> Void
50 | ) {
51 | connectSignal(name: name, id: id, argCount: argCount, pointer: pointer) { _ in
52 | handler()
53 | }
54 | }
55 |
56 | /// Connect a handler to a signal.
57 | /// - Parameters:
58 | /// - name: The signal's name.
59 | /// - id: The handlers id to separate form others connecting to the signal.
60 | /// - argCount: The number of additional arguments (without the first and the last one).
61 | /// - pointer: A custom pointer instead of the stored one.
62 | /// - handler: The signal's handler.
63 | public func connectSignal(
64 | name: String,
65 | id: String = "",
66 | argCount: Int = 0,
67 | pointer: OpaquePointer? = nil,
68 | handler: @escaping ([Any]) -> Void
69 | ) {
70 | if let data = fields[name + id] as? SignalData {
71 | data.closure = handler
72 | } else {
73 | let data = SignalData(closure: handler)
74 | fields[name + id] = data
75 | data.connect(pointer: (pointer ?? opaquePointer)?.cast(), signal: name, argCount: argCount)
76 | }
77 | }
78 |
79 | }
80 |
81 | extension ViewStorage: Storage { }
82 | extension SceneStorage: Storage { }
83 |
--------------------------------------------------------------------------------
/Sources/Adwaita/Model/WindowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 06.08.24.
6 | //
7 |
8 | /// A special view that can access the window of the current instance
9 | /// if located as the first view directly inside a window.
10 | public protocol WindowView: View {
11 |
12 | /// Modify the window.
13 | /// - Parameter window: The window.
14 | /// - Returns: The window.
15 | func window(_ window: Window) -> Window
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Banner+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Banner+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 17.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension Banner {
11 |
12 | /// Initialize a text widget.
13 | /// - Parameters:
14 | /// - title: The content.
15 | /// - visible: Whether the banner is visible.
16 | public init(_ title: String, visible: Bool) {
17 | self.init(title: title)
18 | self = self.revealed(visible)
19 | }
20 |
21 | /// Configure the banner's button.
22 | /// - Parameters:
23 | /// - label: The button's title.
24 | /// - handler: The button's handler.
25 | /// - Returns: The banner.
26 | public func button(_ label: String, handler: @escaping () -> Void) -> Self {
27 | buttonLabel(label)
28 | .buttonClicked {
29 | handler()
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Button+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Button+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A button widget.
11 | extension Button {
12 |
13 | // swiftlint:disable function_default_parameter_at_end
14 | /// Initialize a button.
15 | /// - Parameters:
16 | /// - label: The button's label.
17 | /// - icon: The button's icon.
18 | /// - handler: The button's action handler.
19 | public init(_ label: String? = nil, icon: Icon, handler: @escaping () -> Void) {
20 | self.init()
21 | self = self.child {
22 | ButtonContent()
23 | .label(label)
24 | .iconName(icon.string)
25 | }
26 | self = self.clicked(handler)
27 | }
28 | // swiftlint:enable function_default_parameter_at_end
29 |
30 | /// Initialize a button.
31 | /// - Parameters:
32 | /// - label: The buttons label.
33 | /// - handler: The button's action handler.
34 | public init(_ label: String, handler: @escaping () -> Void) {
35 | self.init()
36 | self = self.label(label)
37 | self = self.clicked(handler)
38 | }
39 |
40 | /// Create a keyboard shortcut for an application window from a button.
41 | ///
42 | /// Note that the keyboard shortcut is available after the view has been visible for the first time.
43 | /// - Parameters:
44 | /// - shortcut: The keyboard shortcut.
45 | /// - window: The application window.
46 | /// - active: Whether the keyboard shortcut is active.
47 | /// - Returns: The button.
48 | public func keyboardShortcut(_ shortcut: String, window: AdwaitaWindow, active: Bool = true) -> AnyView {
49 | onUpdate {
50 | if active {
51 | window.app.addKeyboardShortcut(shortcut, id: shortcut, window: window) { self.clicked?() }
52 | } else {
53 | window.app.removeKeyboardShortcut(id: shortcut, window: window)
54 | }
55 | }
56 | }
57 |
58 | /// Create a keyboard shortcut for an application from a button.
59 | ///
60 | /// Note that the keyboard shortcut is available after the view has been visible for the first time.
61 | /// - Parameters:
62 | /// - shortcut: The keyboard shortcut.
63 | /// - window: The application.
64 | /// - active: Whether the keyboard shortcut is active.
65 | /// - Returns: The button.
66 | public func keyboardShortcut(_ shortcut: String, app: AdwaitaApp, active: Bool = true) -> AnyView {
67 | onUpdate {
68 | if active {
69 | app.addKeyboardShortcut(shortcut, id: shortcut) { self.clicked?() }
70 | } else {
71 | app.removeKeyboardShortcut(id: shortcut)
72 | }
73 | }
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Carousel+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Carousel+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 18.01.24.
6 | //
7 |
8 | extension Carousel {
9 |
10 | /// Set whether long swipes are allowed or not.
11 | /// - Parameter longSwipes: Whether long swipes are allowed.
12 | /// - Returns: The carousel.
13 | public func longSwipes(_ longSwipes: Bool = true) -> Self {
14 | allowLongSwipes(longSwipes)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/CheckButton+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckButton+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 18.05.24.
6 | //
7 |
8 | /// A button widget.
9 | extension CheckButton {
10 |
11 | /// Apply the selection mode style class.
12 | /// - Parameter active: Whether it is applied.
13 | /// - Returns: A view.
14 | public func selectionMode(_ active: Bool = true) -> AnyView {
15 | style("selection-mode", active: active)
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Dialogs/AboutDialog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AboutDialog.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.03.24.
6 | //
7 |
8 | import CAdw
9 | import Foundation
10 |
11 | /// The about dialog widget.
12 | struct AboutDialog: AdwaitaWidget {
13 |
14 | /// Whether the dialog is visible.
15 | @Binding var visible: Bool
16 | /// The wrapped view.
17 | var child: AnyView
18 |
19 | /// The app's name.
20 | var appName: String?
21 | /// The developer's name.
22 | var developer: String?
23 | /// The app version.
24 | var version: String?
25 | /// The app icon.
26 | var icon: Icon?
27 | /// The app's website.
28 | var website: URL?
29 | /// The link for opening issues.
30 | var issues: URL?
31 |
32 | /// The ID for the dialog's storage.
33 | let dialogID = "dialog"
34 |
35 | /// The view storage.
36 | /// - Parameters:
37 | /// - modifiers: Modify views before being updated.
38 | /// - type: The view render data type.
39 | /// - Returns: The view storage.
40 | func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
41 | let storage = child.storage(data: data, type: type)
42 | update(storage, data: data, updateProperties: true, type: type)
43 | return storage
44 | }
45 |
46 | /// Update the stored content.
47 | /// - Parameters:
48 | /// - storage: The storage to update.
49 | /// - modifiers: Modify views before being updated
50 | /// - updateProperties: Whether to update the view's properties.
51 | /// - type: The view render data type.
52 | func update(
53 | _ storage: ViewStorage,
54 | data: WidgetData,
55 | updateProperties: Bool,
56 | type: Data.Type
57 | ) where Data: ViewRenderData {
58 | child.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
59 | guard updateProperties, (storage.previousState as? Self)?.visible != visible else {
60 | return
61 | }
62 | if visible {
63 | if storage.content[dialogID]?.first == nil {
64 | createDialog(storage: storage)
65 | adw_dialog_present(
66 | storage.content[dialogID]?.first?.opaquePointer?.cast(),
67 | storage.opaquePointer?.cast()
68 | )
69 | }
70 | let dialog = storage.content[dialogID]?.first?.opaquePointer
71 | if let appName {
72 | adw_about_dialog_set_application_name(dialog, appName)
73 | }
74 | if let developer {
75 | adw_about_dialog_set_developer_name(dialog, developer)
76 | }
77 | if let version {
78 | adw_about_dialog_set_version(dialog, version)
79 | }
80 | if let icon {
81 | adw_about_dialog_set_application_icon(dialog, icon.string)
82 | }
83 | if let website {
84 | adw_about_dialog_set_website(dialog, website.absoluteString)
85 | }
86 | if let issues {
87 | adw_about_dialog_set_issue_url(dialog, issues.absoluteString)
88 | }
89 | adw_dialog_set_content_height(dialog?.cast(), -1)
90 | } else {
91 | if storage.content[dialogID]?.first != nil {
92 | adw_dialog_close(storage.content[dialogID]?.first?.opaquePointer?.cast())
93 | }
94 | }
95 | storage.previousState = self
96 | }
97 |
98 | /// Create a new instance of the dialog.
99 | /// - Parameter storage: The wrapped view's storage.
100 | func createDialog(storage: ViewStorage) {
101 | let pointer = adw_about_dialog_new()
102 | let dialog = ViewStorage(pointer?.opaque())
103 | storage.content[dialogID] = [dialog]
104 | dialog.connectSignal(name: "closed") {
105 | storage.content[dialogID] = []
106 | if visible {
107 | visible = false
108 | }
109 | }
110 | }
111 |
112 | }
113 |
114 | extension AnyView {
115 |
116 | /// Add an about dialog to the parent window.
117 | /// - Parameters:
118 | /// - visible: Whether the dialog is presented.
119 | /// - app: The app's name.
120 | /// - developer: The developer's name.
121 | /// - version: The version string.
122 | /// - icon: The app icon.
123 | /// - website: The app's website.
124 | /// - issues: Website for reporting issues.
125 | public func aboutDialog(
126 | visible: Binding,
127 | app: String? = nil,
128 | developer: String? = nil,
129 | version: String? = nil,
130 | icon: Icon? = nil,
131 | website: URL? = nil,
132 | issues: URL? = nil
133 | ) -> AnyView {
134 | AboutDialog(
135 | visible: visible,
136 | child: self,
137 | appName: app,
138 | developer: developer,
139 | version: version,
140 | icon: icon,
141 | website: website,
142 | issues: issues
143 | )
144 | }
145 |
146 | }
147 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Dialogs/Dialog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dialog.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.03.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// The dialog widget.
11 | struct Dialog: AdwaitaWidget {
12 |
13 | /// Whether the dialog is visible.
14 | @Binding var visible: Bool
15 | /// An identifier used if multiple dialogs are on one view.
16 | var id: String
17 | /// The dialog's title.
18 | var title: String?
19 | /// The wrapped view.
20 | var child: AnyView
21 | /// The content of the dialog.
22 | var content: Body
23 | /// The dialog's width.
24 | var width: Int?
25 | /// The dialog's height.
26 | var height: Int?
27 |
28 | /// The ID for the dialog's storage.
29 | let dialogID = "dialog"
30 | /// The ID for the content's storage.
31 | let contentID = "content"
32 |
33 | /// The view storage.
34 | /// - Parameters:
35 | /// - modifiers: Modify views before being updated.
36 | /// - type: The view render data type.
37 | /// - Returns: The view storage.
38 | func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
39 | let child = child.storage(data: data, type: type)
40 | let storage = ViewStorage(child.opaquePointer, content: [.mainContent: [child]])
41 | update(storage, data: data, updateProperties: true, type: type)
42 | return storage
43 | }
44 |
45 | /// Update the stored content.
46 | /// - Parameters:
47 | /// - storage: The storage to update.
48 | /// - modifiers: Modify views before being updated
49 | /// - updateProperties: Whether to update the view's properties.
50 | /// - type: The view render data type.
51 | func update(
52 | _ storage: ViewStorage,
53 | data: WidgetData,
54 | updateProperties: Bool,
55 | type: Data.Type
56 | ) where Data: ViewRenderData {
57 | if let storage = storage.content[.mainContent]?.first {
58 | child.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
59 | }
60 | if let storage = storage.content[contentID + id]?.first {
61 | content
62 | .updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
63 | }
64 | guard updateProperties else {
65 | return
66 | }
67 | if visible {
68 | if storage.content[dialogID + id]?.first == nil {
69 | createDialog(storage: storage, data: data, type: type)
70 | adw_dialog_present(
71 | storage.content[dialogID + id]?.first?.opaquePointer?.cast(),
72 | storage.opaquePointer?.cast()
73 | )
74 | }
75 | let pointer = storage.content[dialogID + id]?.first?.opaquePointer
76 | if let title {
77 | adw_dialog_set_title(pointer?.cast(), title)
78 | }
79 | if let width {
80 | adw_dialog_set_content_width(pointer?.cast(), width.cInt)
81 | }
82 | if let height {
83 | adw_dialog_set_content_height(pointer?.cast(), height.cInt)
84 | }
85 | } else {
86 | if storage.content[dialogID + id]?.first != nil {
87 | adw_dialog_close(storage.content[dialogID + id]?.first?.opaquePointer?.cast())
88 | }
89 | }
90 | }
91 |
92 | /// Create a new instance of the dialog.
93 | /// - Parameters:
94 | /// - storage: The wrapped view's storage.
95 | /// - modifiers: The view modifiers.
96 | /// - type: The view render data type.
97 | func createDialog(
98 | storage: ViewStorage,
99 | data: WidgetData,
100 | type: Data.Type
101 | ) where Data: ViewRenderData {
102 | let pointer = adw_dialog_new()
103 | let dialog = ViewStorage(pointer?.opaque())
104 | storage.content[dialogID + id] = [dialog]
105 | let contentStorage = content.storage(data: data, type: type)
106 | adw_dialog_set_child(pointer, contentStorage.opaquePointer?.cast())
107 | storage.content[contentID + id] = [contentStorage]
108 | dialog.connectSignal(name: "closed") {
109 | storage.content[dialogID + id] = []
110 | storage.content[contentID + id] = []
111 | if visible {
112 | visible = false
113 | }
114 | }
115 | }
116 |
117 | }
118 |
119 | extension AnyView {
120 |
121 | /// Add a dialog to the parent window.
122 | /// - Parameters:
123 | /// - visible: Whether the dialog is presented.
124 | /// - title: The dialog's title.
125 | /// - width: The dialog's width.
126 | /// - height: The dialog's height.
127 | /// - content: The dialog's content.
128 | public func dialog(
129 | visible: Binding,
130 | title: String? = nil,
131 | id: String? = nil,
132 | width: Int? = nil,
133 | height: Int? = nil,
134 | @ViewBuilder content: () -> Body
135 | ) -> AnyView {
136 | Dialog(
137 | visible: visible,
138 | id: id ?? "",
139 | title: title,
140 | child: self,
141 | content: content(),
142 | width: width,
143 | height: height
144 | )
145 | }
146 |
147 | }
148 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/EitherView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewStack.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 30.12.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A widget showing one of two widgets based on a condition.
11 | public struct EitherView: SimpleView, Meta.EitherView {
12 |
13 | /// Whether the first view is visible.
14 | var condition: Bool
15 | /// The first view.
16 | var view1: Body
17 | /// The second view.
18 | var view2: Body
19 |
20 | /// The view's content.
21 | public var view: Body {
22 | if condition {
23 | return [ViewStack(id: true) { _ in view1 }.homogeneous(false)]
24 | } else {
25 | return [ViewStack(id: false) { _ in view2 }.homogeneous(false)]
26 | }
27 | }
28 |
29 | /// Initialize the either view.
30 | /// - Parameters:
31 | /// - condition: Whether the first view is visible-
32 | /// - view1: The first view, visible if true.
33 | /// - view2: The second view, visible if false.
34 | public init(
35 | _ condition: Bool,
36 | @ViewBuilder view1: () -> Body,
37 | @ViewBuilder else view2: () -> Body
38 | ) {
39 | self.condition = condition
40 | self.view1 = view1()
41 | self.view2 = view2()
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Fixed+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fixed+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.07.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension Fixed {
11 |
12 | /// Place an element in the coordinate system.
13 | /// - Parameters:
14 | /// - xCoordinate: The x coordinate.
15 | /// - yCoordinate: The y coordinate.
16 | /// - id: A unique identifier.
17 | /// - view: The element.
18 | /// - Returns: The coordinate system with the element.
19 | public func element(
20 | x xCoordinate: Double,
21 | y yCoordinate: Double,
22 | id: String,
23 | @ViewBuilder view: @escaping () -> Body
24 | ) -> Self {
25 | var newSelf = self
26 | newSelf.appearFunctions.append { storage, data in
27 | let view = view().storage(data: data, type: AdwaitaMainView.self)
28 | gtk_fixed_put(
29 | storage.opaquePointer?.cast(),
30 | view.opaquePointer?.cast(),
31 | xCoordinate,
32 | yCoordinate
33 | )
34 | storage.content[id] = [view]
35 | }
36 | newSelf.updateFunctions.append { storage, data, updateProperties in
37 | guard let content = storage.content[id]?.first else {
38 | return
39 | }
40 | view()
41 | .updateStorage(
42 | content,
43 | data: data,
44 | updateProperties: updateProperties,
45 | type: AdwaitaMainView.self
46 | )
47 | if updateProperties {
48 | gtk_fixed_move(
49 | storage.opaquePointer?.cast(),
50 | content.opaquePointer?.cast(),
51 | xCoordinate,
52 | yCoordinate
53 | )
54 | }
55 | }
56 | return newSelf
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/FlowBox+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlowBox+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 12.02.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension FlowBox {
11 |
12 | /// The ID for the field storing the selection value.
13 | static var selectionField: String { "selection" }
14 | /// The ID for the field storing the elements.
15 | static var elementsField: String { "element" }
16 |
17 | /// Initialize `FlowBox`.
18 | /// - Parameters:
19 | /// - elements: The elements.
20 | /// - selection: The identifier of the selected element. Selection disabled if `nil`.
21 | /// - content: The view for an element.
22 | public init(
23 | _ elements: [Element],
24 | selection: Binding?,
25 | @ViewBuilder content: @escaping (Element) -> Body
26 | ) {
27 | self.init(elements, content: content)
28 | let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in
29 | if let child = g_list_nth_data(gtk_flow_box_get_selected_children(storage.opaquePointer), 0) {
30 | let element = gtk_flow_box_child_get_child(child.cast())
31 | return elements[safe: storage.content[.mainContent]?
32 | .firstIndex { $0.opaquePointer?.cast() == element }]?.id
33 | }
34 | return nil
35 | }
36 | if let selection {
37 | updateFunctions.append { storage, _, _ in
38 | storage.connectSignal(name: "selected_children_changed", id: Self.selectionField) {
39 | if let elements = storage.fields[Self.elementsField] as? [Element],
40 | let id = id(storage, elements) {
41 | selection.wrappedValue = id
42 | }
43 | }
44 | if selection.wrappedValue != id(storage, elements),
45 | let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt {
46 | gtk_flow_box_select_child(
47 | storage.opaquePointer,
48 | gtk_flow_box_get_child_at_index(storage.opaquePointer, index)
49 | )
50 | }
51 | }
52 | } else {
53 | appearFunctions.append { storage, _ in
54 | gtk_flow_box_set_selection_mode(storage.opaquePointer, GTK_SELECTION_NONE)
55 | }
56 | }
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/ForEach.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEach.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 30.01.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A dynamic list but without a list design in the user interface.
12 | public struct ForEach: AdwaitaWidget where Element: Identifiable {
13 |
14 | /// The dynamic widget elements.
15 | var elements: [Element]
16 | /// The dynamic widget content.
17 | var content: (Element) -> Body
18 | /// Whether the list is horizontal.
19 | var horizontal: Bool
20 |
21 | /// Initialize `ForEach`.
22 | public init(_ elements: [Element], horizontal: Bool = false, @ViewBuilder content: @escaping (Element) -> Body) {
23 | self.elements = elements
24 | self.content = content
25 | self.horizontal = horizontal
26 | }
27 |
28 | /// The view storage.
29 | /// - Parameters:
30 | /// - modifiers: Modify views before being updated.
31 | /// - type: The view render data type.
32 | /// - Returns: The view storage.
33 | public func container(
34 | data: WidgetData,
35 | type: Data.Type
36 | ) -> ViewStorage where Data: ViewRenderData {
37 | let storage = ViewStorage(
38 | gtk_box_new(horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL, 0)?.opaque()
39 | )
40 | update(storage, data: data, updateProperties: true, type: type)
41 | return storage
42 | }
43 |
44 | /// Update the stored content.
45 | /// - Parameters:
46 | /// - storage: The storage to update.
47 | /// - modifiers: Modify views before being updated
48 | /// - updateProperties: Whether to update the view's properties.
49 | /// - type: The view render data type.
50 | public func update(
51 | _ storage: ViewStorage,
52 | data: WidgetData,
53 | updateProperties: Bool,
54 | type: Data.Type
55 | ) where Data: ViewRenderData {
56 | var contentStorage: [ViewStorage] = storage.content[.mainContent] ?? []
57 | let old = storage.fields["element"] as? [Element] ?? []
58 | let widget: UnsafeMutablePointer? = storage.opaquePointer?.cast()
59 | old.identifiableTransform(
60 | to: elements,
61 | functions: .init { index, element in
62 | let child = content(element).storage(data: data, type: type)
63 | gtk_box_remove(widget, contentStorage[safe: index]?.opaquePointer?.cast())
64 | gtk_box_insert_child_after(
65 | widget,
66 | child.opaquePointer?.cast(),
67 | contentStorage[safe: index - 1]?.opaquePointer?.cast()
68 | )
69 | contentStorage.remove(at: index)
70 | contentStorage.insert(child, at: index)
71 | } delete: { index in
72 | gtk_box_remove(widget, contentStorage[safe: index]?.opaquePointer?.cast())
73 | contentStorage.remove(at: index)
74 | } insert: { index, element in
75 | let child = content(element).storage(data: data, type: type)
76 | gtk_box_insert_child_after(
77 | widget,
78 | child.opaquePointer?.cast(),
79 | contentStorage[safe: index - 1]?.opaquePointer?.cast()
80 | )
81 | contentStorage.insert(child, at: index)
82 | }
83 | )
84 | if updateProperties {
85 | gtk_orientable_set_orientation(
86 | widget?.opaque(),
87 | horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL
88 | )
89 | }
90 | storage.fields["element"] = elements
91 | storage.content[.mainContent] = contentStorage
92 | for (index, element) in elements.enumerated() {
93 | content(element)
94 | .updateStorage(
95 | contentStorage[index],
96 | data: data,
97 | updateProperties: updateProperties,
98 | type: type
99 | )
100 | }
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/ActionRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActionRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | /// A form content row showing a title and optionally a subtitle and widgets.
9 | extension ActionRow {
10 |
11 | /// Initialize an action row.
12 | /// - Parameter title: The row's title.
13 | public init(_ title: String) {
14 | self = self.title(title)
15 | }
16 |
17 | /// Deemphasize the row title and emphasize the subtitle.
18 | /// - Parameter active: Whether the style is currently applied.
19 | /// - Returns: A view.
20 | public func property(_ active: Bool = true) -> AnyView {
21 | style("property", active: active)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/ComboRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComboRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A row for selecting an element out of a list of elements.
12 | extension ComboRow {
13 |
14 | /// The identifier for the values.
15 | static var values: String { "values" }
16 | /// The identifier for the string list.
17 | static var stringList: String { "string-list" }
18 |
19 | /// Initialize a combo row.
20 | /// - Parameters:
21 | /// - title: The row's title.
22 | /// - selection: The selected value.
23 | /// - values: The available values.
24 | public init(
25 | _ title: String,
26 | selection: Binding,
27 | values: [Element]
28 | ) where Element: Identifiable, Element: CustomStringConvertible {
29 | self = self.title(title)
30 | self = self.selected(.init {
31 | .init(values.firstIndex { $0.id == selection.wrappedValue } ?? 0)
32 | } set: { index in
33 | if let id = values[safe: .init(index)]?.id {
34 | selection.wrappedValue = id
35 | }
36 | })
37 | appearFunctions.append { storage, _ in
38 | let list = gtk_string_list_new(nil)
39 | storage.fields[Self.stringList] = list
40 | adw_combo_row_set_model(storage.opaquePointer?.cast(), list)
41 | Self.updateContent(storage: storage, values: values, element: Element.self)
42 | }
43 | updateFunctions.append { storage, _, _ in
44 | Self.updateContent(storage: storage, values: values, element: Element.self)
45 | }
46 | }
47 |
48 | /// Update the combo row's content.
49 | /// - Parameters:
50 | /// - storage: The view storage.
51 | /// - values: The elements.
52 | /// - element: The type of the elements.
53 | static func updateContent(
54 | storage: ViewStorage,
55 | values: [Element],
56 | element: Element.Type
57 | ) where Element: Identifiable, Element: CustomStringConvertible {
58 | if let list = storage.fields[Self.stringList] as? OpaquePointer {
59 | let old = storage.fields[Self.values] as? [Element] ?? []
60 | old.identifiableTransform(
61 | to: values,
62 | functions: .init { index, element in
63 | gtk_string_list_remove(list, .init(index))
64 | gtk_string_list_append(list, element.description)
65 | } delete: { index in
66 | gtk_string_list_remove(list, .init(index))
67 | } insert: { _, element in
68 | gtk_string_list_append(list, element.description)
69 | }
70 | )
71 | storage.fields[Self.values] = values
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/EntryRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EntryRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension EntryRow {
11 |
12 | /// Initialize an entry row.
13 | /// - Parameters:
14 | /// - title: The row's title.
15 | /// - text: The text.
16 | public init(_ title: String, text: Binding) {
17 | self.init()
18 | self = self.title(title)
19 | updateFunctions.append { storage, _, _ in
20 | storage.notify(name: "text") {
21 | let newValue = String(cString: gtk_editable_get_text(storage.opaquePointer))
22 | if text.wrappedValue != newValue {
23 | text.wrappedValue = newValue
24 | }
25 | }
26 | if text.wrappedValue != .init(cString: gtk_editable_get_text(storage.opaquePointer)) {
27 | gtk_editable_set_text(storage.opaquePointer, text.wrappedValue)
28 | }
29 | }
30 | }
31 |
32 | /// Set the entry row's subtitle.
33 | /// - Parameter subtitle: The subtitle.
34 | /// - Returns: The entry row.
35 | public func onSubmit(_ onSubmit: @escaping () -> Void) -> Self {
36 | showApplyButton()
37 | .apply(onSubmit)
38 | }
39 |
40 | /// Let the user securely enter private text.
41 | /// - Parameter: The text.
42 | /// - Returns: The entry row.
43 | public func secure(text: Binding? = nil) -> PasswordEntryRow {
44 | .init(title ?? "", text: text ?? .constant(""))
45 | .activatesDefault(activatesDefault)
46 | .enableEmojiCompletion(enableEmojiCompletion)
47 | .showApplyButton(showApplyButton)
48 | .titleSelectable(titleSelectable)
49 | .useMarkup(useMarkup)
50 | .useUnderline(useUnderline)
51 | .apply(apply ?? { })
52 | .entryActivated(entryActivated ?? { })
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/Form.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 03.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A list with no dynamic content styled as a boxed list.
11 | public struct Form: SimpleView {
12 |
13 | /// The content.
14 | var content: Body
15 |
16 | /// The view's body.
17 | public var view: Body {
18 | List([Int](content.indices), selection: nil) { index in content[index] }
19 | .style("boxed-list")
20 | }
21 |
22 | /// Initialize a `Form`.
23 | /// - Parameter content: The view content, usually different kind of rows.
24 | public init(@ViewBuilder content: @escaping () -> Body) {
25 | self.content = content()
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/FormSection+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormSection+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | /// A section usually groups forms.
9 | public typealias FormSection = PreferencesGroup
10 |
11 | extension FormSection {
12 |
13 | /// Initialize a form section.
14 | /// - Parameters:
15 | /// - title: The title.
16 | /// - content: The content, usually one or more forms.
17 | public init(_ title: String, @ViewBuilder content: @escaping () -> Body) {
18 | self.init()
19 | self = self.title(title)
20 | self = self.child(content)
21 | }
22 |
23 | /// Set the form section's suffix view.
24 | /// - Parameter suffix: The suffix.
25 | /// - Returns: The form section.
26 | public func suffix(@ViewBuilder _ suffix: @escaping () -> Body) -> Self {
27 | headerSuffix(suffix)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/PasswordEntryRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasswordEntryRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension PasswordEntryRow {
11 |
12 | /// Initialize an entry row.
13 | /// - Parameters:
14 | /// - title: The row's title.
15 | /// - text: The text.
16 | public init(_ title: String, text: Binding) {
17 | self.init()
18 | self = self.title(title)
19 | updateFunctions.append { storage, _, _ in
20 | storage.notify(name: "text") {
21 | let newValue = String(cString: gtk_editable_get_text(storage.opaquePointer))
22 | if text.wrappedValue != newValue {
23 | text.wrappedValue = newValue
24 | }
25 | }
26 | if text.wrappedValue != .init(cString: gtk_editable_get_text(storage.opaquePointer)) {
27 | gtk_editable_set_text(storage.opaquePointer, text.wrappedValue)
28 | }
29 | }
30 | }
31 |
32 | /// Set the entry row's subtitle.
33 | /// - Parameter subtitle: The subtitle.
34 | /// - Returns: The entry row.
35 | public func onSubmit(_ onSubmit: @escaping () -> Void) -> Self {
36 | apply(onSubmit)
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/SpinRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension SpinRow {
11 |
12 | /// Initialize a spin row.
13 | /// - Parameters:
14 | /// - title: The row's title.
15 | /// - value: The selected value.
16 | /// - min: The minimum value.
17 | /// - max: The maximum value.
18 | public init(_ title: String, value: Binding, min: Int, max: Int) {
19 | self.init(
20 | title,
21 | value: .init { .init(value.wrappedValue) } set: { value.wrappedValue = .init($0) },
22 | min: .init(min),
23 | max: .init(max)
24 | )
25 | }
26 |
27 | /// Initialize a spin row.
28 | /// - Parameters:
29 | /// - title: The row's title.
30 | /// - value: The selected value.
31 | /// - min: The minimum value.
32 | /// - max: The maximum value.
33 | public init(_ title: String, value: Binding, min: Double, max: Double) {
34 | self.init(climbRate: 1, digits: 0)
35 | self = self.title(title)
36 | self = self.value(value)
37 | self = self.step(1)
38 | updateFunctions.append { storage, _, _ in
39 | adw_spin_row_set_range(storage.opaquePointer, min, max)
40 | }
41 | }
42 |
43 | /// Set the difference a single click on the increase/decrease buttons makes.
44 | /// - Parameter step: The increase/decrease step.
45 | /// - Returns: The spin row.
46 | public func step(_ step: Int) -> Self {
47 | self.step(.init(step))
48 | }
49 |
50 | /// Set the difference a single click on the increase/decrease buttons makes.
51 | /// - Parameter step: The increase/decrease step.
52 | /// - Returns: The spin row.
53 | public func step(_ step: Double) -> Self {
54 | var newSelf = self
55 | newSelf.updateFunctions.append { storage, _, _ in
56 | let adjustment = adw_spin_row_get_adjustment(storage.opaquePointer)
57 | gtk_adjustment_set_step_increment(adjustment, step)
58 | }
59 | return newSelf
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Forms/SwitchRow+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwitchRow+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | /// A row representing a boolean state.
9 | extension SwitchRow {
10 |
11 | /// Initialize a switch row.
12 | /// - Parameters:
13 | /// - title: The row's title.
14 | /// - isOn: Whether the switch is on.
15 | public init(_ title: String, isOn: Binding) {
16 | self.init()
17 | self = self.title(title)
18 | self = self.active(isOn)
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/Bin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bin.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A widget with one child.
12 | ///
13 | ///
14 | ///
15 | /// The `AdwBin` widget has only one child, set with the [property@Bin:child]
16 | /// property.
17 | ///
18 | /// It is useful for deriving subclasses, since it provides common code needed
19 | /// for handling a single child widget.
20 | public struct Bin: AdwaitaWidget {
21 |
22 | /// Additional update functions for type extensions.
23 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
24 | /// Additional appear functions for type extensions.
25 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
26 |
27 | /// The child widget of the `AdwBin`.
28 | var child: (() -> Body)?
29 |
30 | /// Initialize `Bin`.
31 | public init() {
32 | }
33 |
34 | /// The view storage.
35 | /// - Parameters:
36 | /// - modifiers: Modify views before being updated.
37 | /// - type: The view render data type.
38 | /// - Returns: The view storage.
39 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
40 | let storage = ViewStorage(adw_bin_new()?.opaque())
41 | for function in appearFunctions {
42 | function(storage, data)
43 | }
44 | update(storage, data: data, updateProperties: true, type: type)
45 | if let childStorage = child?().storage(data: data, type: type) {
46 | storage.content["child"] = [childStorage]
47 | adw_bin_set_child(storage.opaquePointer?.cast(), childStorage.opaquePointer?.cast())
48 | }
49 |
50 | return storage
51 | }
52 |
53 | /// Update the stored content.
54 | /// - Parameters:
55 | /// - storage: The storage to update.
56 | /// - modifiers: Modify views before being updated
57 | /// - updateProperties: Whether to update the view's properties.
58 | /// - type: The view render data type.
59 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
60 | storage.modify { widget in
61 |
62 | if let widget = storage.content["child"]?.first {
63 | child?().updateStorage(widget, data: data, updateProperties: updateProperties, type: type)
64 | }
65 |
66 |
67 | }
68 | for function in updateFunctions {
69 | function(storage, data, updateProperties)
70 | }
71 | if updateProperties {
72 | storage.previousState = self
73 | }
74 | }
75 |
76 | /// The child widget of the `AdwBin`.
77 | public func child(@ViewBuilder _ child: @escaping (() -> Body)) -> Self {
78 | var newSelf = self
79 | newSelf.child = child
80 |
81 | return newSelf
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/Fixed.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fixed.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// `GtkFixed` places its child widgets at fixed positions and with fixed sizes.
12 | ///
13 | /// `GtkFixed` performs no automatic layout management.
14 | ///
15 | /// For most applications, you should not use this container! It keeps
16 | /// you from having to learn about the other GTK containers, but it
17 | /// results in broken applications. With `GtkFixed`, the following
18 | /// things will result in truncated text, overlapping widgets, and
19 | /// other display bugs:
20 | ///
21 | /// - Themes, which may change widget sizes.
22 | ///
23 | /// - Fonts other than the one you used to write the app will of course
24 | /// change the size of widgets containing text; keep in mind that
25 | /// users may use a larger font because of difficulty reading the
26 | /// default, or they may be using a different OS that provides different fonts.
27 | ///
28 | /// - Translation of text into other languages changes its size. Also,
29 | /// display of non-English text will use a different font in many
30 | /// cases.
31 | ///
32 | /// In addition, `GtkFixed` does not pay attention to text direction and
33 | /// thus may produce unwanted results if your app is run under right-to-left
34 | /// languages such as Hebrew or Arabic. That is: normally GTK will order
35 | /// containers appropriately for the text direction, e.g. to put labels to
36 | /// the right of the thing they label when using an RTL language, but it can’t
37 | /// do that with `GtkFixed`. So if you need to reorder widgets depending on
38 | /// the text direction, you would need to manually detect it and adjust child
39 | /// positions accordingly.
40 | ///
41 | /// Finally, fixed positioning makes it kind of annoying to add/remove
42 | /// UI elements, since you have to reposition all the other elements. This
43 | /// is a long-term maintenance problem for your application.
44 | ///
45 | /// If you know none of these things are an issue for your application,
46 | /// and prefer the simplicity of `GtkFixed`, by all means use the
47 | /// widget. But you should be aware of the tradeoffs.
48 | public struct Fixed: AdwaitaWidget {
49 |
50 | /// Additional update functions for type extensions.
51 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
52 | /// Additional appear functions for type extensions.
53 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
54 |
55 | /// The accessible role of the given `GtkAccessible` implementation.
56 | ///
57 | /// The accessible role cannot be changed once set.
58 | var accessibleRole: String?
59 |
60 | /// Initialize `Fixed`.
61 | public init() {
62 | }
63 |
64 | /// The view storage.
65 | /// - Parameters:
66 | /// - modifiers: Modify views before being updated.
67 | /// - type: The view render data type.
68 | /// - Returns: The view storage.
69 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
70 | let storage = ViewStorage(gtk_fixed_new()?.opaque())
71 | for function in appearFunctions {
72 | function(storage, data)
73 | }
74 | update(storage, data: data, updateProperties: true, type: type)
75 |
76 | return storage
77 | }
78 |
79 | /// Update the stored content.
80 | /// - Parameters:
81 | /// - storage: The storage to update.
82 | /// - modifiers: Modify views before being updated
83 | /// - updateProperties: Whether to update the view's properties.
84 | /// - type: The view render data type.
85 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
86 | storage.modify { widget in
87 |
88 |
89 |
90 | }
91 | for function in updateFunctions {
92 | function(storage, data, updateProperties)
93 | }
94 | if updateProperties {
95 | storage.previousState = self
96 | }
97 | }
98 |
99 | /// The accessible role of the given `GtkAccessible` implementation.
100 | ///
101 | /// The accessible role cannot be changed once set.
102 | public func accessibleRole(_ accessibleRole: String?) -> Self {
103 | var newSelf = self
104 | newSelf.accessibleRole = accessibleRole
105 |
106 | return newSelf
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/Separator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Separator.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// `GtkSeparator` is a horizontal or vertical separator widget.
12 | ///
13 | /// 
14 | ///
15 | /// A `GtkSeparator` can be used to group the widgets within a window.
16 | /// It displays a line with a shadow to make it appear sunken into the
17 | /// interface.
18 | ///
19 | /// # CSS nodes
20 | ///
21 | /// `GtkSeparator` has a single CSS node with name separator. The node
22 | /// gets one of the .horizontal or .vertical style classes.
23 | ///
24 | /// # Accessibility
25 | ///
26 | /// `GtkSeparator` uses the %GTK_ACCESSIBLE_ROLE_SEPARATOR role.
27 | public struct Separator: AdwaitaWidget {
28 |
29 | /// Additional update functions for type extensions.
30 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
31 | /// Additional appear functions for type extensions.
32 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
33 |
34 | /// The accessible role of the given `GtkAccessible` implementation.
35 | ///
36 | /// The accessible role cannot be changed once set.
37 | var accessibleRole: String?
38 |
39 | /// Initialize `Separator`.
40 | public init() {
41 | }
42 |
43 | /// The view storage.
44 | /// - Parameters:
45 | /// - modifiers: Modify views before being updated.
46 | /// - type: The view render data type.
47 | /// - Returns: The view storage.
48 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
49 | let storage = ViewStorage(gtk_separator_new(GTK_ORIENTATION_VERTICAL)?.opaque())
50 | for function in appearFunctions {
51 | function(storage, data)
52 | }
53 | update(storage, data: data, updateProperties: true, type: type)
54 |
55 | return storage
56 | }
57 |
58 | /// Update the stored content.
59 | /// - Parameters:
60 | /// - storage: The storage to update.
61 | /// - modifiers: Modify views before being updated
62 | /// - updateProperties: Whether to update the view's properties.
63 | /// - type: The view render data type.
64 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
65 | storage.modify { widget in
66 |
67 |
68 |
69 | }
70 | for function in updateFunctions {
71 | function(storage, data, updateProperties)
72 | }
73 | if updateProperties {
74 | storage.previousState = self
75 | }
76 | }
77 |
78 | /// The accessible role of the given `GtkAccessible` implementation.
79 | ///
80 | /// The accessible role cannot be changed once set.
81 | public func accessibleRole(_ accessibleRole: String?) -> Self {
82 | var newSelf = self
83 | newSelf.accessibleRole = accessibleRole
84 |
85 | return newSelf
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/Spinner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Spinner.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A `GtkSpinner` widget displays an icon-size spinning animation.
12 | ///
13 | /// It is often used as an alternative to a [class@Gtk.ProgressBar]
14 | /// for displaying indefinite activity, instead of actual progress.
15 | ///
16 | /// 
17 | ///
18 | /// To start the animation, use [method@Gtk.Spinner.start], to stop it
19 | /// use [method@Gtk.Spinner.stop].
20 | ///
21 | /// # CSS nodes
22 | ///
23 | /// `GtkSpinner` has a single CSS node with the name spinner.
24 | /// When the animation is active, the :checked pseudoclass is
25 | /// added to this node.
26 | public struct Spinner: AdwaitaWidget {
27 |
28 | /// Additional update functions for type extensions.
29 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
30 | /// Additional appear functions for type extensions.
31 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
32 |
33 | /// The accessible role of the given `GtkAccessible` implementation.
34 | ///
35 | /// The accessible role cannot be changed once set.
36 | var accessibleRole: String?
37 | /// Whether the spinner is spinning
38 | var spinning: Bool?
39 |
40 | /// Initialize `Spinner`.
41 | public init() {
42 | }
43 |
44 | /// The view storage.
45 | /// - Parameters:
46 | /// - modifiers: Modify views before being updated.
47 | /// - type: The view render data type.
48 | /// - Returns: The view storage.
49 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
50 | let storage = ViewStorage(gtk_spinner_new()?.opaque())
51 | for function in appearFunctions {
52 | function(storage, data)
53 | }
54 | update(storage, data: data, updateProperties: true, type: type)
55 |
56 | return storage
57 | }
58 |
59 | /// Update the stored content.
60 | /// - Parameters:
61 | /// - storage: The storage to update.
62 | /// - modifiers: Modify views before being updated
63 | /// - updateProperties: Whether to update the view's properties.
64 | /// - type: The view render data type.
65 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
66 | storage.modify { widget in
67 |
68 | if let spinning, updateProperties, (storage.previousState as? Self)?.spinning != spinning {
69 | gtk_spinner_set_spinning(widget, spinning.cBool)
70 | }
71 |
72 |
73 | }
74 | for function in updateFunctions {
75 | function(storage, data, updateProperties)
76 | }
77 | if updateProperties {
78 | storage.previousState = self
79 | }
80 | }
81 |
82 | /// The accessible role of the given `GtkAccessible` implementation.
83 | ///
84 | /// The accessible role cannot be changed once set.
85 | public func accessibleRole(_ accessibleRole: String?) -> Self {
86 | var newSelf = self
87 | newSelf.accessibleRole = accessibleRole
88 |
89 | return newSelf
90 | }
91 |
92 | /// Whether the spinner is spinning
93 | public func spinning(_ spinning: Bool? = true) -> Self {
94 | var newSelf = self
95 | newSelf.spinning = spinning
96 |
97 | return newSelf
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/StatusPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusPage.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A page used for empty/error states and similar use-cases.
12 | ///
13 | ///
14 | ///
15 | /// The `AdwStatusPage` widget can have an icon, a title, a description and a
16 | /// custom widget which is displayed below them.
17 | ///
18 | /// ## CSS nodes
19 | ///
20 | /// `AdwStatusPage` has a main CSS node with name `statuspage`.
21 | ///
22 | /// `AdwStatusPage` can use the
23 | /// [`.compact`](style-classes.html#compact-status-page) style class for when it
24 | /// needs to fit into a small space such a sidebar or a popover.
25 | public struct StatusPage: AdwaitaWidget {
26 |
27 | /// Additional update functions for type extensions.
28 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
29 | /// Additional appear functions for type extensions.
30 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
31 |
32 | /// The child widget.
33 | var child: (() -> Body)?
34 | /// The description markup to be displayed below the title.
35 | var description: String?
36 | /// The name of the icon to be used.
37 | ///
38 | /// Changing this will set [property@StatusPage:paintable] to `NULL`.
39 | var iconName: String?
40 | /// The title to be displayed below the icon.
41 | ///
42 | /// It is not parsed as Pango markup.
43 | var title: String?
44 |
45 | /// Initialize `StatusPage`.
46 | public init() {
47 | }
48 |
49 | /// The view storage.
50 | /// - Parameters:
51 | /// - modifiers: Modify views before being updated.
52 | /// - type: The view render data type.
53 | /// - Returns: The view storage.
54 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
55 | let storage = ViewStorage(adw_status_page_new()?.opaque())
56 | for function in appearFunctions {
57 | function(storage, data)
58 | }
59 | update(storage, data: data, updateProperties: true, type: type)
60 | if let childStorage = child?().storage(data: data, type: type) {
61 | storage.content["child"] = [childStorage]
62 | adw_status_page_set_child(storage.opaquePointer, childStorage.opaquePointer?.cast())
63 | }
64 |
65 | return storage
66 | }
67 |
68 | /// Update the stored content.
69 | /// - Parameters:
70 | /// - storage: The storage to update.
71 | /// - modifiers: Modify views before being updated
72 | /// - updateProperties: Whether to update the view's properties.
73 | /// - type: The view render data type.
74 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
75 | storage.modify { widget in
76 |
77 | if let widget = storage.content["child"]?.first {
78 | child?().updateStorage(widget, data: data, updateProperties: updateProperties, type: type)
79 | }
80 | if let description, updateProperties, (storage.previousState as? Self)?.description != description {
81 | adw_status_page_set_description(widget, description)
82 | }
83 | if let iconName, updateProperties, (storage.previousState as? Self)?.iconName != iconName {
84 | adw_status_page_set_icon_name(widget, iconName)
85 | }
86 | if let title, updateProperties, (storage.previousState as? Self)?.title != title {
87 | adw_status_page_set_title(widget, title)
88 | }
89 |
90 |
91 | }
92 | for function in updateFunctions {
93 | function(storage, data, updateProperties)
94 | }
95 | if updateProperties {
96 | storage.previousState = self
97 | }
98 | }
99 |
100 | /// The child widget.
101 | public func child(@ViewBuilder _ child: @escaping (() -> Body)) -> Self {
102 | var newSelf = self
103 | newSelf.child = child
104 |
105 | return newSelf
106 | }
107 |
108 | /// The description markup to be displayed below the title.
109 | public func description(_ description: String?) -> Self {
110 | var newSelf = self
111 | newSelf.description = description
112 |
113 | return newSelf
114 | }
115 |
116 | /// The name of the icon to be used.
117 | ///
118 | /// Changing this will set [property@StatusPage:paintable] to `NULL`.
119 | public func iconName(_ iconName: String?) -> Self {
120 | var newSelf = self
121 | newSelf.iconName = iconName
122 |
123 | return newSelf
124 | }
125 |
126 | /// The title to be displayed below the icon.
127 | ///
128 | /// It is not parsed as Pango markup.
129 | public func title(_ title: String?) -> Self {
130 | var newSelf = self
131 | newSelf.title = title
132 |
133 | return newSelf
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/ToastOverlay.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToastOverlay.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A widget showing toasts above its content.
12 | ///
13 | ///
14 | ///
15 | /// Much like [class@Gtk.Overlay], `AdwToastOverlay` is a container with a single
16 | /// main child, on top of which it can display a [class@Toast], overlaid.
17 | /// Toasts can be shown with [method@ToastOverlay.add_toast].
18 | ///
19 | /// See [class@Toast] for details.
20 | ///
21 | /// ## CSS nodes
22 | ///
23 | /// ```
24 | /// toastoverlay
25 | /// ├── [child]
26 | /// ├── toast
27 | /// ┊ ├── widget
28 | /// ┊ │ ├── [label.heading]
29 | /// │ ╰── [custom title]
30 | /// ├── [button]
31 | /// ╰── button.circular.flat
32 | /// ```
33 | ///
34 | /// `AdwToastOverlay`'s CSS node is called `toastoverlay`. It contains the child,
35 | /// as well as zero or more `toast` subnodes.
36 | ///
37 | /// Each of the `toast` nodes contains a `widget` subnode, optionally a `button`
38 | /// subnode, and another `button` subnode with `.circular` and `.flat` style
39 | /// classes.
40 | ///
41 | /// The `widget` subnode contains a `label` subnode with the `.heading` style
42 | /// class, or a custom widget provided by the application.
43 | ///
44 | /// ## Accessibility
45 | ///
46 | /// `AdwToastOverlay` uses the `GTK_ACCESSIBLE_ROLE_TAB_GROUP` role.
47 | public struct ToastOverlay: AdwaitaWidget {
48 |
49 | /// Additional update functions for type extensions.
50 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
51 | /// Additional appear functions for type extensions.
52 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
53 |
54 | /// The child widget.
55 | var child: (() -> Body)?
56 |
57 | /// Initialize `ToastOverlay`.
58 | public init() {
59 | }
60 |
61 | /// The view storage.
62 | /// - Parameters:
63 | /// - modifiers: Modify views before being updated.
64 | /// - type: The view render data type.
65 | /// - Returns: The view storage.
66 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
67 | let storage = ViewStorage(adw_toast_overlay_new()?.opaque())
68 | for function in appearFunctions {
69 | function(storage, data)
70 | }
71 | update(storage, data: data, updateProperties: true, type: type)
72 | if let childStorage = child?().storage(data: data, type: type) {
73 | storage.content["child"] = [childStorage]
74 | adw_toast_overlay_set_child(storage.opaquePointer, childStorage.opaquePointer?.cast())
75 | }
76 |
77 | return storage
78 | }
79 |
80 | /// Update the stored content.
81 | /// - Parameters:
82 | /// - storage: The storage to update.
83 | /// - modifiers: Modify views before being updated
84 | /// - updateProperties: Whether to update the view's properties.
85 | /// - type: The view render data type.
86 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
87 | storage.modify { widget in
88 |
89 | if let widget = storage.content["child"]?.first {
90 | child?().updateStorage(widget, data: data, updateProperties: updateProperties, type: type)
91 | }
92 |
93 |
94 | }
95 | for function in updateFunctions {
96 | function(storage, data, updateProperties)
97 | }
98 | if updateProperties {
99 | storage.previousState = self
100 | }
101 | }
102 |
103 | /// The child widget.
104 | public func child(@ViewBuilder _ child: @escaping (() -> Body)) -> Self {
105 | var newSelf = self
106 | newSelf.child = child
107 |
108 | return newSelf
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Generated/WindowTitle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowTitle.swift
3 | // Adwaita
4 | //
5 | // Created by auto-generation on 15.08.24.
6 | //
7 |
8 | import CAdw
9 | import LevenshteinTransformations
10 |
11 | /// A helper widget for setting a window's title and subtitle.
12 | ///
13 | ///
14 | ///
15 | /// `AdwWindowTitle` shows a title and subtitle. It's intended to be used as the
16 | /// title child of [class@Gtk.HeaderBar] or [class@HeaderBar].
17 | ///
18 | /// ## CSS nodes
19 | ///
20 | /// `AdwWindowTitle` has a single CSS node with name `windowtitle`.
21 | public struct WindowTitle: AdwaitaWidget {
22 |
23 | /// Additional update functions for type extensions.
24 | var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
25 | /// Additional appear functions for type extensions.
26 | var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
27 |
28 | /// The subtitle to display.
29 | ///
30 | /// The subtitle should give the user additional details.
31 | var subtitle: String
32 | /// The title to display.
33 | ///
34 | /// The title typically identifies the current view or content item, and
35 | /// generally does not use the application name.
36 | var title: String
37 |
38 | /// Initialize `WindowTitle`.
39 | public init(subtitle: String, title: String) {
40 | self.subtitle = subtitle
41 | self.title = title
42 | }
43 |
44 | /// The view storage.
45 | /// - Parameters:
46 | /// - modifiers: Modify views before being updated.
47 | /// - type: The view render data type.
48 | /// - Returns: The view storage.
49 | public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData {
50 | let storage = ViewStorage(adw_window_title_new(title, subtitle)?.opaque())
51 | for function in appearFunctions {
52 | function(storage, data)
53 | }
54 | update(storage, data: data, updateProperties: true, type: type)
55 |
56 | return storage
57 | }
58 |
59 | /// Update the stored content.
60 | /// - Parameters:
61 | /// - storage: The storage to update.
62 | /// - modifiers: Modify views before being updated
63 | /// - updateProperties: Whether to update the view's properties.
64 | /// - type: The view render data type.
65 | public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) where Data: ViewRenderData {
66 | storage.modify { widget in
67 |
68 | if updateProperties, (storage.previousState as? Self)?.subtitle != subtitle {
69 | adw_window_title_set_subtitle(widget, subtitle)
70 | }
71 | if updateProperties, (storage.previousState as? Self)?.title != title {
72 | adw_window_title_set_title(widget, title)
73 | }
74 |
75 |
76 | }
77 | for function in updateFunctions {
78 | function(storage, data, updateProperties)
79 | }
80 | if updateProperties {
81 | storage.previousState = self
82 | }
83 | }
84 |
85 | /// The subtitle to display.
86 | ///
87 | /// The subtitle should give the user additional details.
88 | public func subtitle(_ subtitle: String) -> Self {
89 | var newSelf = self
90 | newSelf.subtitle = subtitle
91 |
92 | return newSelf
93 | }
94 |
95 | /// The title to display.
96 | ///
97 | /// The title typically identifies the current view or content item, and
98 | /// generally does not use the application name.
99 | public func title(_ title: String) -> Self {
100 | var newSelf = self
101 | newSelf.title = title
102 |
103 | return newSelf
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/HStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HStack.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 26.09.23.
6 | //
7 |
8 | /// A horizontal GtkBox equivalent.
9 | public struct HStack: SimpleView {
10 |
11 | /// The content.
12 | var content: () -> Body
13 | /// Whether the linked style should be used.
14 | var linked = false
15 |
16 | /// The view's body.
17 | public var view: Body {
18 | VStack(horizontal: true, content: content)
19 | .linked(linked)
20 | }
21 |
22 | /// Initialize a `HStack`.
23 | /// - Parameter content: The view content.
24 | public init(@ViewBuilder content: @escaping () -> Body) {
25 | self.content = content
26 | }
27 |
28 | /// Link the children.
29 | public func linked(_ active: Bool = true) -> Self {
30 | var newSelf = self
31 | newSelf.linked = active
32 | return newSelf
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/HeaderBar+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaderBar+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.23.
6 | //
7 |
8 | extension HeaderBar {
9 |
10 | /// Initialize a header bar.
11 | /// - Parameters:
12 | /// - titleButtons: Whether the title buttons (e.g. close button) are visible.
13 | /// - start: The start content.
14 | /// - end: The end content.
15 | public init(
16 | titleButtons: Bool = true,
17 | @ViewBuilder start: @escaping () -> Body,
18 | @ViewBuilder end: @escaping () -> Body
19 | ) {
20 | self.init()
21 | self = self.showStartTitleButtons(titleButtons).showEndTitleButtons(titleButtons)
22 | self = self.start(start).end(end)
23 | }
24 |
25 | /// Initialize an empty header bar.
26 | /// - Returns: The header bar.
27 | public static func empty() -> Self {
28 | .init(start: { }, end: { })
29 | }
30 |
31 | /// Initialize a header bar with only views at the start.
32 | /// - Parameter start: The views.
33 | /// - Returns: The header bar.
34 | public static func start(@ViewBuilder start: @escaping () -> Body) -> Self {
35 | .init(start: start) { }
36 | }
37 |
38 | /// Initialize a header bar with only views at the end.
39 | /// - Parameter start: The views.
40 | /// - Returns: The header bar.
41 | public static func end(@ViewBuilder end: @escaping () -> Body) -> Self {
42 | .init(start: { }, end: end)
43 | }
44 |
45 | /// Set the title widget for the header bar.
46 | /// - Parameter view: The widget in the header bar.
47 | /// - Returns: The header bar.
48 | public func headerBarTitle(@ViewBuilder view: @escaping () -> Body) -> Self {
49 | titleWidget(view)
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/List.swift:
--------------------------------------------------------------------------------
1 | //
2 | // List.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 25.09.23.
6 | //
7 |
8 | import CAdw
9 | import Meta
10 |
11 | /// A list box widget.
12 | public typealias List = ListBox
13 |
14 | extension List {
15 |
16 | /// The ID for the field storing the selection value.
17 | static var selectionField: String { "selection" }
18 | /// The ID for the field storing the elements.
19 | static var elementsField: String { "element" }
20 |
21 | /// Initialize `List`.
22 | /// - Parameters:
23 | /// - elements: The elements.
24 | /// - selection: The identifier of the selected element. Selection disabled if `nil`.
25 | /// - content: The view for an element.
26 | public init(
27 | _ elements: [Element],
28 | selection: Binding?,
29 | @ViewBuilder content: @escaping (Element) -> Body
30 | ) {
31 | self.init(elements, content: content)
32 | let id: (ViewStorage, [Element]) -> Element.ID? = { storage, elements in
33 | if let row = gtk_list_box_get_selected_row(storage.opaquePointer) {
34 | return elements[safe: .init(gtk_list_box_row_get_index(row))]?.id
35 | }
36 | return nil
37 | }
38 | if let selection {
39 | updateFunctions.append { storage, _, _ in
40 | storage.connectSignal(name: "selected_rows_changed", id: Self.selectionField) {
41 | if let elements = storage.fields[Self.elementsField] as? [Element],
42 | let id = id(storage, elements),
43 | selection.wrappedValue != id {
44 | selection.wrappedValue = id
45 | }
46 | }
47 | if selection.wrappedValue != id(storage, elements),
48 | let index = elements.firstIndex(where: { $0.id == selection.wrappedValue })?.cInt {
49 | gtk_list_box_select_row(
50 | storage.opaquePointer,
51 | gtk_list_box_get_row_at_index(storage.opaquePointer, index)
52 | )
53 | }
54 | }
55 | } else {
56 | appearFunctions.append { storage, _ in
57 | gtk_list_box_set_selection_mode(storage.opaquePointer, GTK_SELECTION_NONE)
58 | }
59 | }
60 | }
61 |
62 | /// Add the "navigation-sidebar" style class.
63 | /// - Parameter active: Whether the style is applied.
64 | /// - Returns: A view.
65 | public func sidebarStyle(_ active: Bool = true) -> AnyView {
66 | style("navigation-sidebar", active: active)
67 | }
68 |
69 | /// Apply the boxed list style class.
70 | /// - Parameter active: Whether the style is applied.
71 | /// - Returns: A view.
72 | public func boxedList(_ active: Bool = true) -> AnyView {
73 | style("boxed-list", active: active)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Menu+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Menu+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.10.23.
6 | //
7 |
8 | import CAdw
9 |
10 | extension Menu {
11 |
12 | // swiftlint:disable function_default_parameter_at_end
13 | /// Initialize a menu button.
14 | /// - Parameters:
15 | /// - label: The button's label.
16 | /// - icon: The button's icon.
17 | /// - content: The menu's content.
18 | public init(
19 | _ label: String? = nil,
20 | icon: Icon,
21 | @ViewBuilder content: @escaping () -> Body
22 | ) {
23 | self.init()
24 | self = self
25 | .child {
26 | ButtonContent()
27 | .iconName(icon.string)
28 | .label(label)
29 | }
30 | .menuModel(content)
31 | }
32 | // swiftlint:enable function_default_parameter_at_end
33 |
34 | /// Initialize a menu button.
35 | /// - Parameters:
36 | /// - label: The buttons label.
37 | /// - content: The menu's content.
38 | public init(
39 | _ label: String,
40 | @ViewBuilder content: @escaping () -> Body
41 | ) {
42 | self.init()
43 | self = self
44 | .label(label)
45 | .menuModel(content)
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Modifiers/Clamp+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Clamp+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension Clamp {
11 |
12 | /// Initialize either a horizontal or vertical clamp.
13 | /// - Parameter vertical: Whether it is a vertical clamp.
14 | init(vertical: Bool) {
15 | self.init()
16 | if vertical {
17 | appearFunctions.append { storage, _ in
18 | gtk_orientable_set_orientation(storage.opaquePointer, GTK_ORIENTATION_VERTICAL)
19 | }
20 | }
21 | }
22 |
23 | }
24 |
25 | extension AnyView {
26 |
27 | /// Set the view's maximum width.
28 | /// - Parameter maxWidth: The maximum width.
29 | /// - Returns: A view.
30 | public func frame(maxWidth: Int? = nil) -> Clamp {
31 | .init()
32 | .child { self }
33 | .maximumSize(maxWidth ?? -1)
34 | }
35 |
36 | /// Set the view's maximum height.
37 | /// - Parameter maxHeight: The maximum height.
38 | /// - Returns: A view.
39 | public func frame(maxHeight: Int? = nil) -> Clamp {
40 | .init(vertical: true)
41 | .child { self }
42 | .maximumSize(maxHeight ?? -1)
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Modifiers/Popover+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popover+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 10.02.24.
6 | //
7 |
8 | import CAdw
9 |
10 | extension Popover {
11 |
12 | /// Initialize either a horizontal or vertical clamp.
13 | /// - Parameter vertical: Whether it is a vertical clamp.
14 | init(visible: Binding) {
15 | self.init()
16 | updateFunctions.append { storage, _, _ in
17 | storage.connectSignal(name: "closed", id: "visible") {
18 | if visible.wrappedValue {
19 | visible.wrappedValue = false
20 | }
21 | }
22 | if visible.wrappedValue {
23 | gtk_popover_popup(storage.opaquePointer?.cast())
24 | } else {
25 | gtk_popover_popdown(storage.opaquePointer?.cast())
26 | }
27 | }
28 | }
29 |
30 | }
31 |
32 | extension AnyView {
33 |
34 | /// Add a popover on top of the view.
35 | /// - Parameters:
36 | /// - visible: Whether the popover is displayed.
37 | /// - content: The popover's content.
38 | /// - Returns: The view.
39 | public func popover(visible: Binding, @ViewBuilder content: @escaping () -> Body) -> Overlay {
40 | overlay {
41 | Popover(visible: visible)
42 | .child(content)
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Modifiers/ToastOverlay+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToastOverlay+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.24.
6 | //
7 |
8 | import CAdw
9 | import Foundation
10 |
11 | /// A wrapper around a view presenting toasts.
12 | extension ToastOverlay {
13 |
14 | /// Initialize a toast overlay.
15 | /// - Parameters:
16 | /// - title: The toast's title.
17 | /// - signal: The signal for adding a toast.
18 | public init(_ title: String, signal: Signal) {
19 | updateFunctions.append { storage, _, _ in
20 | if signal.update {
21 | let toast = ViewStorage(adw_toast_new(title))
22 | storage.fields[UUID().uuidString] = toast
23 | if let button = storage.fields["button"] as? String,
24 | let handler = storage.fields["handler"] as? () -> Void {
25 | adw_toast_set_button_label(toast.opaquePointer, button)
26 | toast.connectSignal(name: "button-clicked", handler: handler)
27 | }
28 | adw_toast_overlay_add_toast(storage.opaquePointer, toast.opaquePointer)
29 | }
30 | }
31 | }
32 |
33 | /// Add an action button to the toast.
34 | /// - Parameters:
35 | /// - button: The button's label.
36 | /// - handler: The handler.
37 | /// - Returns: The toast overlay.
38 | public func action(button: String, handler: @escaping () -> Void) -> Self {
39 | var newSelf = self
40 | let action: (ViewStorage, WidgetData, Bool) -> Void = { storage, _, _ in
41 | storage.fields["button"] = button
42 | storage.fields["handler"] = handler
43 | }
44 | newSelf.updateFunctions.insert(action, at: 0)
45 | return newSelf
46 | }
47 |
48 | }
49 |
50 | extension AnyView {
51 |
52 | /// Present a toast when the signal gets activated.
53 | /// - Parameters:
54 | /// - title: The title of the toast.
55 | /// - signal: The signal which activates the presentation of a toast.
56 | /// - Returns: A view.
57 | public func toast(_ title: String, signal: Signal) -> ToastOverlay {
58 | .init(title, signal: signal)
59 | .child { self }
60 | }
61 |
62 | /// Present a toast with a button when the signal gets activated.
63 | /// - Parameters:
64 | /// - title: The title of the toast.
65 | /// - signal: The signal which activates the presentation of a toast.
66 | /// - button: The button's label.
67 | /// - handler: The handler for the button.
68 | /// - Returns: A view.
69 | public func toast(_ title: String, signal: Signal, button: String, handler: @escaping () -> Void) -> ToastOverlay {
70 | .init(title, signal: signal)
71 | .child { self }
72 | .action(button: button, handler: handler)
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/NavigationSplitView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationSplitView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 24.09.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A navigation split view widget.
11 | public struct NavigationSplitView: AdwaitaWidget {
12 |
13 | /// The sidebar's content.
14 | var sidebar: Body
15 | /// The split view's main content.
16 | var content: Body
17 | /// Whether the split view is collapsed.
18 | var collapsed = false
19 | /// Whether the content is visible (if the split view is collapsed).
20 | var showContent: Binding?
21 |
22 | /// The sidebar content's id.
23 | let sidebarID = "sidebar"
24 | /// The main content's id.
25 | let contentID = "content"
26 |
27 | /// Initialize a navigation split view.
28 | /// - Parameters:
29 | /// - sidebar: The sidebar content.
30 | /// - content: The main content.
31 | public init(@ViewBuilder sidebar: @escaping () -> Body, @ViewBuilder content: @escaping () -> Body) {
32 | self.sidebar = sidebar()
33 | self.content = content()
34 | }
35 |
36 | /// The view storage.
37 | /// - Parameters:
38 | /// - modifiers: Modify views before being updated.
39 | /// - type: The view render data type.
40 | /// - Returns: The view storage.
41 | public func container(
42 | data: WidgetData,
43 | type: Data.Type
44 | ) -> ViewStorage where Data: ViewRenderData {
45 | let splitView = adw_navigation_split_view_new()
46 | var content: [String: [ViewStorage]] = [:]
47 |
48 | let sidebar = sidebar.storage(data: data, type: type)
49 | let label = sidebar.fields[.navigationLabel] as? String ?? ""
50 | let sidebarPage = adw_navigation_page_new(sidebar.opaquePointer?.cast(), label)
51 | adw_navigation_split_view_set_sidebar(.init(splitView), sidebarPage?.cast())
52 | content[sidebarID] = [sidebar]
53 |
54 | let mainContent = self.content.storage(data: data, type: type)
55 | let mainLabel = mainContent.fields[.navigationLabel] as? String ?? ""
56 | let mainPage = adw_navigation_page_new(mainContent.opaquePointer?.cast(), mainLabel)
57 | adw_navigation_split_view_set_content(.init(splitView), mainPage?.cast())
58 | content[contentID] = [mainContent]
59 |
60 | let storage = ViewStorage(splitView?.opaque(), content: content)
61 | update(storage, data: data, updateProperties: true, type: type)
62 |
63 | storage.notify(name: "show-content") {
64 | showContent?.wrappedValue = adw_navigation_split_view_get_show_content(storage.opaquePointer) != 0
65 | }
66 |
67 | return storage
68 | }
69 |
70 | /// Update the stored content.
71 | /// - Parameters:
72 | /// - storage: The storage to update.
73 | /// - modifiers: Modify views before being updated
74 | /// - updateProperties: Whether to update the view's properties.
75 | /// - type: The view render data type.
76 | public func update(
77 | _ storage: ViewStorage,
78 | data: WidgetData,
79 | updateProperties: Bool,
80 | type: Data.Type
81 | ) where Data: ViewRenderData {
82 | if let storage = storage.content[contentID]?[safe: 0] {
83 | content
84 | .updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
85 | }
86 | if let storage = storage.content[sidebarID]?[safe: 0] {
87 | sidebar
88 | .updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
89 | }
90 | guard updateProperties else {
91 | return
92 | }
93 | let collapsed = adw_navigation_split_view_get_collapsed(storage.opaquePointer) != 0
94 | if collapsed != self.collapsed {
95 | adw_navigation_split_view_set_collapsed(storage.opaquePointer, self.collapsed.cBool)
96 | }
97 | let showContent = adw_navigation_split_view_get_show_content(storage.opaquePointer) != 0
98 | if let binding = self.showContent, showContent != binding.wrappedValue {
99 | adw_navigation_split_view_set_show_content(storage.opaquePointer, binding.wrappedValue.cBool)
100 | }
101 | }
102 |
103 | /// Whether the navigation split view is collapsed, meaning in its compact form.
104 | /// - Parameter collapsed: Whether the view is collapsed.
105 | /// - Returns: The navigation split view.
106 | public func collapsed(_ collapsed: Bool) -> Self {
107 | var newSelf = self
108 | newSelf.collapsed = collapsed
109 | return newSelf
110 | }
111 |
112 | /// Whether the content view is visible if the split view is collapsed.
113 | /// - Parameter showContent: Whether the content view is visible.
114 | /// - Returns: The navigation split view.
115 | public func showContent(_ showContent: Binding) -> Self {
116 | var newSelf = self
117 | newSelf.showContent = showContent
118 | return newSelf
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/OverlaySplitView+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OverlaySplitView+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.01.23.
6 | //
7 |
8 | import CAdw
9 |
10 | extension OverlaySplitView {
11 |
12 | /// Initialize an overlay split view.
13 | /// - Parameters:
14 | /// - visible: Whether the sidebar is visible.
15 | /// - sidebar: The sidebar content.
16 | /// - content: The main content.
17 | public init(
18 | visible: Binding = .constant(true),
19 | @ViewBuilder sidebar: @escaping () -> Body,
20 | @ViewBuilder content: @escaping () -> Body
21 | ) {
22 | self.init()
23 | self = self.sidebar(sidebar)
24 | self = self.content(content)
25 | self = self.showSidebar(visible)
26 | }
27 |
28 | /// The position of the sidebar.
29 | /// - Parameter trailing: Whether the sidebar is at the trailing position.
30 | /// - Returns: The navigation split view.
31 | public func trailingSidebar(_ trailing: Bool = true) -> Self {
32 | var newSelf = self
33 | newSelf.updateFunctions.append { storage, _, _ in
34 | adw_overlay_split_view_set_sidebar_position(storage.opaquePointer, trailing ? GTK_PACK_END : GTK_PACK_START)
35 | }
36 | return newSelf
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Picture+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Picture+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.04.24.
6 | //
7 |
8 | import CAdw
9 | import Foundation
10 |
11 | extension Picture {
12 |
13 | /// Load the picture from Foundation's `Data`.
14 | /// - Parameter data: The data.
15 | /// - Returns: The view.
16 | public func data(_ data: Data?) -> AnyView {
17 | let oldData = "old-data"
18 | return inspect { storage, updateProperties in
19 | if updateProperties {
20 | let pointer = storage.opaquePointer
21 | guard let data else {
22 | if storage.fields[oldData] != nil {
23 | gtk_picture_set_paintable(pointer, gdk_paintable_new_empty(0, 0))
24 | storage.fields[oldData] = nil
25 | }
26 | return
27 | }
28 | guard data != storage.fields[oldData] as? Data else {
29 | return
30 | }
31 | let bytes = data.withUnsafeBytes { ptr in
32 | g_bytes_new(ptr.baseAddress, .init(data.count))
33 | }
34 | let texture = gdk_texture_new_from_bytes(bytes, nil)
35 | gtk_picture_set_paintable(pointer, texture)
36 | storage.fields[oldData] = data
37 | }
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/ProgressBar+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Progressbar+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 15.01.24.
6 | //
7 |
8 | extension ProgressBar {
9 |
10 | /// Initialize a progress bar widget.
11 | /// - Parameters:
12 | /// - value: The value.
13 | /// - total: The maximum value.
14 | public init(value: Double, total: Double) {
15 | self.init()
16 | if total != 0 {
17 | self = self.fraction(value / total)
18 | } else {
19 | self = self.fraction(0)
20 | }
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/ScrollView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollView.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 26.09.23.
6 | //
7 |
8 | /// A GtkScrolledWindow equivalent.
9 | public typealias ScrollView = ScrolledWindow
10 |
11 | extension ScrollView {
12 |
13 | /// Initialize a `ScrollView`.
14 | /// - Parameter content: The view content.
15 | public init(@ViewBuilder content: @escaping () -> Body) {
16 | self.init()
17 | self = self.child(content)
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/StatusPage+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusPage+.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 17.01.23.
6 | //
7 |
8 | extension StatusPage {
9 |
10 | /// Initialize a status page widget.
11 | /// - Parameters:
12 | /// - title: The title.
13 | /// - icon: The icon.
14 | /// - description: Additional details.
15 | /// - content: Additional content.
16 | public init(
17 | _ title: String,
18 | icon: Icon? = nil,
19 | description: String = "",
20 | @ViewBuilder content: @escaping () -> Body = { [] }
21 | ) {
22 | self.init()
23 | self = self.title(title)
24 | self = self.description(description)
25 | self = self.iconName(icon?.string ?? "")
26 | self = self.child(content)
27 | }
28 |
29 | /// Make the status page more compact.
30 | /// - Parameter active: Whether the style is applied.
31 | /// - Returns: A view.
32 | public func compact(_ active: Bool = true) -> AnyView {
33 | style("compact", active: active)
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Text.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Text.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 23.08.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A text widget.
11 | public typealias Text = Label
12 |
13 | extension Text {
14 |
15 | /// Initialize a text widget.
16 | /// - Parameter text: The content.
17 | public init(_ text: String) {
18 | self.init(label: text)
19 | }
20 |
21 | /// Set whether the text should ellipsize at the end.
22 | /// - Parameter ellipsize: Whether it should ellipsize.
23 | /// - Returns: The text widget.
24 | public func ellipsize(_ ellipsize: Bool = true) -> AnyView {
25 | inspect { storage, _ in gtk_label_set_ellipsize(storage.opaquePointer, PANGO_ELLIPSIZE_END) }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/Toggle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Toggle.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 19.12.23.
6 | //
7 |
8 | /// A toggle button widget.
9 | public typealias Toggle = ToggleButton
10 |
11 | /// A toggle button widget.
12 | extension Toggle {
13 |
14 | // swiftlint:disable function_default_parameter_at_end
15 | /// Initialize a toggle button.
16 | /// - Parameters:
17 | /// - label: The button's label.
18 | /// - icon: The button's icon.
19 | /// - isOn: Whether the toggle is on.
20 | public init(_ label: String? = nil, icon: Icon? = nil, isOn: Binding) {
21 | self = self.child {
22 | ButtonContent()
23 | .label(label)
24 | .iconName(icon?.string)
25 | }
26 | self.active = isOn
27 | }
28 | // swiftlint:enable function_default_parameter_at_end
29 |
30 | /// Initialize a toggle button.
31 | /// - Parameters:
32 | /// - label: The buttons label.
33 | /// - isOn: Whether the toggle is on.
34 | public init(_ label: String, isOn: Binding) {
35 | self.label = label
36 | self.active = isOn
37 | }
38 |
39 | /// Use the check button style.
40 | /// - Returns: The toggle.
41 | public func checkButton() -> CheckButton {
42 | if let child {
43 | return .init()
44 | .active(active)
45 | .child(child)
46 | } else {
47 | return .init()
48 | .active(active)
49 | .label(label)
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/VStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VStack.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 23.08.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A GtkBox equivalent.
11 | public typealias VStack = Box
12 |
13 | extension VStack {
14 |
15 | /// Initialize a `VStack`.
16 | /// - Parameter content: The view content.
17 | public init(@ViewBuilder content: @escaping () -> Body) {
18 | self.init(horizontal: false, content: content)
19 | }
20 |
21 | /// Initialize a `VStack`.
22 | /// - Parameters:
23 | /// - horizontal: Whether the box is horizontal.
24 | /// - content: The view content.
25 | init(horizontal: Bool, @ViewBuilder content: @escaping () -> Body) {
26 | self.init(spacing: 0)
27 | self = self.append(content)
28 | if horizontal {
29 | appearFunctions.append { storage, _ in
30 | gtk_orientable_set_orientation(storage.opaquePointer, GTK_ORIENTATION_HORIZONTAL)
31 | }
32 | }
33 | }
34 |
35 | /// Link the children.
36 | public func linked(_ active: Bool = true) -> AnyView {
37 | style("linked", active: active)
38 | }
39 |
40 | }
41 |
42 | /// A wrapper around ``VStack`` which applies the ``VStack`` only if there is more than one view.
43 | public struct VStackWrapper: AdwaitaWidget, Wrapper {
44 |
45 | /// The content.
46 | var content: Body
47 |
48 | /// Initialize the wrapper.
49 | /// - Parameter content: The view content.
50 | public init(@ViewBuilder content: () -> Body) {
51 | self.content = content()
52 | }
53 |
54 | /// The view storage.
55 | /// - Parameters:
56 | /// - modifiers: Modify views before being updated.
57 | /// - type: The view render data type.
58 | /// - Returns: The view storage.
59 | public func container(
60 | data: WidgetData,
61 | type: Data.Type
62 | ) -> ViewStorage where Data: ViewRenderData {
63 | if content.count == 1, let element = content.first {
64 | return element.storage(data: data, type: type)
65 | } else {
66 | return VStack { content }.storage(data: data, type: type)
67 | }
68 | }
69 |
70 | /// Update the stored content.
71 | /// - Parameters:
72 | /// - storage: The storage to update.
73 | /// - modifiers: Modify views before being updated
74 | /// - updateProperties: Whether to update the view's properties.
75 | /// - type: The view render data type.
76 | public func update(
77 | _ storage: ViewStorage,
78 | data: WidgetData,
79 | updateProperties: Bool,
80 | type: Data.Type
81 | ) where Data: ViewRenderData {
82 | if content.count == 1, let element = content.first {
83 | element.updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
84 | } else {
85 | VStack { content }
86 | .updateStorage(storage, data: data, updateProperties: updateProperties, type: type)
87 | }
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/ViewStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewStack.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 30.12.23.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A widget holding multiple children but only displaying one.
11 | public struct ViewStack: AdwaitaWidget {
12 |
13 | /// The stack's active content.
14 | var content: Body
15 | /// The stack's active identifier.
16 | var id: CustomStringConvertible
17 | /// Whether the view stack allocates the same height and width for all children.
18 | var homogeneous: Bool?
19 |
20 | /// Initialize the stack.
21 | /// - Parameters:
22 | /// - id: The identifier of the current view.
23 | /// - view: The current view.
24 | public init(
25 | id: Identifier,
26 | @ViewBuilder view: @escaping (Identifier) -> Body
27 | ) where Identifier: CustomStringConvertible {
28 | content = view(id)
29 | self.id = id
30 | }
31 |
32 | /// Initialize the stack.
33 | /// - Parameters:
34 | /// - element: The current identifiable element.
35 | /// - view: The current view.
36 | public init(
37 | element: Element,
38 | @ViewBuilder view: @escaping (Element) -> Body
39 | ) where Element: Identifiable, Element.ID: CustomStringConvertible {
40 | content = view(element)
41 | self.id = element.id
42 | }
43 |
44 | /// The view storage.
45 | /// - Parameters:
46 | /// - modifiers: Modify views before being updated.
47 | /// - type: The view render data type.
48 | /// - Returns: The view storage.
49 | public func container(
50 | data: WidgetData,
51 | type: Data.Type
52 | ) -> ViewStorage where Data: ViewRenderData {
53 | let stack = gtk_stack_new()
54 | let storage = ViewStorage(stack?.opaque())
55 | update(storage, data: data, updateProperties: true, type: type)
56 | return storage
57 | }
58 |
59 | /// Update the stored content.
60 | /// - Parameters:
61 | /// - storage: The storage to update.
62 | /// - modifiers: Modify views before being updated
63 | /// - updateProperties: Whether to update the view's properties.
64 | /// - type: The view render data type.
65 | public func update(
66 | _ storage: ViewStorage,
67 | data: WidgetData,
68 | updateProperties: Bool,
69 | type: Data.Type
70 | ) where Data: ViewRenderData {
71 | if let view = storage.content[id.description]?.first {
72 | content.updateStorage(view, data: data, updateProperties: updateProperties, type: type)
73 | } else {
74 | let view = content.storage(data: data, type: type)
75 | gtk_stack_add_named(storage.opaquePointer, view.opaquePointer?.cast(), id.description)
76 | storage.content[id.description] = [view]
77 | }
78 | if let visibleView = storage.content[id.description]?.first {
79 | if let transition = visibleView.fields[.transition] as? Transition {
80 | gtk_stack_set_transition_type(storage.opaquePointer, transition.cTransition)
81 | }
82 | if (storage.previousState as? Self)?.id.description != id.description {
83 | gtk_stack_set_visible_child_name(storage.opaquePointer, id.description)
84 | }
85 | }
86 | if let homogeneous, homogeneous != (storage.previousState as? Self)?.homogeneous {
87 | gtk_stack_set_vhomogeneous(storage.opaquePointer, homogeneous.cBool)
88 | gtk_stack_set_hhomogeneous(storage.opaquePointer, homogeneous.cBool)
89 | }
90 | storage.previousState = self
91 | }
92 |
93 | /// Whether the view stack allocates the same height and width for all children.
94 | /// - Parameter homogeneous: Whether this is enabled.
95 | /// - Returns: The stack.
96 | public func homogeneous(_ homogeneous: Bool? = nil) -> Self {
97 | modify { $0.homogeneous = homogeneous }
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/Adwaita/View/ViewSwitcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewSwitcher.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 03.01.24.
6 | //
7 |
8 | import CAdw
9 |
10 | /// A picker used for indicating multiple views.
11 | ///
12 | /// It normally controls a `ViewStack` (e.g. via `switch` statements).
13 | public struct ViewSwitcher: AdwaitaWidget where Element: ViewSwitcherOption {
14 |
15 | /// The selected element.
16 | @Binding var selection: Element
17 | /// Whether the wide style is used, that means the icons and titles are on the same line.
18 | var wide = false
19 |
20 | /// Initialize a view switcher.
21 | /// - Parameter selection: The selected element.
22 | public init(selection: Binding) {
23 | self._selection = selection
24 | }
25 |
26 | /// The view storage.
27 | /// - Parameters:
28 | /// - modifiers: Modify views before being updated.
29 | /// - type: The view render data type.
30 | /// - Returns: The view storage.
31 | public func container(
32 | data: WidgetData,
33 | type: Data.Type
34 | ) -> ViewStorage where Data: ViewRenderData {
35 | let switcher = ViewStorage(adw_view_switcher_new()?.opaque())
36 | let stack = ViewStorage(adw_view_stack_new()?.opaque())
37 | adw_view_switcher_set_stack(switcher.opaquePointer, stack.opaquePointer)
38 | for option in Element.allCases {
39 | adw_view_stack_add_titled_with_icon(
40 | stack.opaquePointer,
41 | gtk_label_new(""),
42 | option.title,
43 | option.title,
44 | option.icon.string
45 | )
46 | }
47 | updateSwitcher(switcher: switcher)
48 | switcher.fields["stack"] = stack
49 | return switcher
50 | }
51 |
52 | /// Update the stored content.
53 | /// - Parameters:
54 | /// - storage: The storage to update.
55 | /// - modifiers: Modify views before being updated
56 | /// - updateProperties: Whether to update the view's properties.
57 | /// - type: The view render data type.
58 | public func update(
59 | _ storage: ViewStorage,
60 | data: WidgetData,
61 | updateProperties: Bool,
62 | type: Data.Type
63 | ) where Data: ViewRenderData {
64 | updateSwitcher(switcher: storage)
65 | }
66 |
67 | /// Update a view switcher's style and selection.
68 | /// - Parameter switcher: The view switcher.
69 | func updateSwitcher(switcher: ViewStorage) {
70 | let stack = switcher.fields["stack"] as? ViewStorage
71 | stack?.notify(name: "visible-child") {
72 | if let title = adw_view_stack_get_visible_child_name(stack?.opaquePointer),
73 | let option = Element(title: .init(cString: title)) {
74 | selection = option
75 | }
76 | }
77 | if (switcher.previousState as? Self)?.wide != wide {
78 | adw_view_switcher_set_policy(
79 | switcher.opaquePointer,
80 | wide ? ADW_VIEW_SWITCHER_POLICY_WIDE : ADW_VIEW_SWITCHER_POLICY_NARROW
81 | )
82 | }
83 | if (switcher.previousState as? Self)?.selection.title != selection.title {
84 | let stack = adw_view_switcher_get_stack(switcher.opaquePointer)
85 | adw_view_stack_set_visible_child_name(stack, selection.title)
86 | }
87 | switcher.previousState = self
88 | }
89 |
90 | /// Set whether to use the wide design.
91 | /// - Parameter wide: Whether to use the wide design.
92 | /// - Returns: The view switcher.
93 | public func wideDesign(_ wide: Bool = true) -> Self {
94 | var newSelf = self
95 | newSelf.wide = wide
96 | return newSelf
97 | }
98 |
99 | }
100 |
101 | /// The protocol an element type for view switcher has to conform to.
102 | public protocol ViewSwitcherOption: CaseIterable {
103 |
104 | /// The title displayed in the switcher and used for identification.
105 | var title: String { get }
106 | /// A symbolic representation in the view switcher.
107 | var icon: Icon { get }
108 |
109 | /// Get the element from the title.
110 | init?(title: String)
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/Sources/CAdw/module.modulemap:
--------------------------------------------------------------------------------
1 | module CAdw [system] {
2 | header "shim.h"
3 | link "adwaita-1"
4 | link "gtk-4"
5 | export *
6 | }
--------------------------------------------------------------------------------
/Sources/CAdw/shim.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | static void
5 | filedialog_on_open_cb (void *, void *, void *);
6 | static void
7 | filedialog_on_save_cb (void *, void *, void *);
8 | static void
9 | alertdialog_on_close_cb (void *, void *, void *);
10 |
11 | static void
12 | gtui_filedialog_save_finish (uint64_t dialog, uint64_t result, uint64_t data)
13 | {
14 | GFile *file = gtk_file_dialog_save_finish (dialog, result, NULL);
15 | const char *path = g_file_get_path (file);
16 | filedialog_on_save_cb (dialog, path, data);
17 | }
18 |
19 | static void
20 | gtui_filedialog_save (uint64_t dialog, uint64_t data, uint64_t window)
21 | {
22 | swift_retain (data);
23 | gtk_file_dialog_save (dialog, window, NULL, G_CALLBACK (gtui_filedialog_save_finish), (void *)data);
24 | }
25 |
26 | static void
27 | gtui_filedialog_open_finish (uint64_t dialog, uint64_t result, uint64_t data)
28 | {
29 | GFile *file = gtk_file_dialog_open_finish (dialog, result, NULL);
30 | const char *path = g_file_peek_path (file);
31 | g_object_unref (file);
32 | filedialog_on_open_cb (dialog, path, data);
33 | }
34 |
35 | static void
36 | gtui_filedialog_open (uint64_t dialog, uint64_t data, uint64_t window)
37 | {
38 | swift_retain (data);
39 | gtk_file_dialog_open (dialog, window, NULL, G_CALLBACK (gtui_filedialog_open_finish), (void *)data);
40 | }
41 |
42 | static void
43 | gtui_alertdialog_cb (uint64_t dialog, uint64_t result, uint64_t data)
44 | {
45 | const char *response = adw_alert_dialog_choose_finish (dialog, result);
46 | alertdialog_on_close_cb (dialog, response, data);
47 | }
48 |
49 | static void
50 | gtui_alertdialog_choose (uint64_t dialog, uint64_t data, uint64_t parent)
51 | {
52 | adw_alert_dialog_choose (dialog, parent, NULL, gtui_alertdialog_cb, data);
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/Sources/Demo/AlertDialogDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlertDialogDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 05.04.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct AlertDialogDemo: View {
13 |
14 | @State private var dialog = false
15 | let padding = 20
16 |
17 | var view: Body {
18 | VStack {
19 | Button("Show Dialog") {
20 | dialog = true
21 | }
22 | .pill()
23 | .suggested()
24 | .frame(maxWidth: 100)
25 | .padding()
26 | }
27 | .alertDialog(visible: $dialog, heading: "Alert Dialog", body: "This is an alert dialog")
28 | .response("Cancel", role: .close) {
29 | print("Cancel")
30 | }
31 | .response("Done", appearance: .suggested, role: .default) {
32 | print("Done")
33 | }
34 | }
35 |
36 | }
37 |
38 | // swiftlint:enable missing_docs
39 |
--------------------------------------------------------------------------------
/Sources/Demo/CarouselDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CarouselDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 01.01.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | struct CarouselDemo: View {
14 |
15 | @State private var items: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")]
16 |
17 | var view: Body {
18 | Button("Add Card") {
19 | let element = ListDemo.Element(id: UUID().uuidString)
20 | items.append(element)
21 | }
22 | .padding()
23 | .halign(.center)
24 | Carousel(items) { element in
25 | VStack {
26 | Text(element.id)
27 | .vexpand()
28 | Button("Delete") {
29 | items = items.filter { $0.id != element.id }
30 | }
31 | .padding()
32 | }
33 | .vexpand()
34 | .hexpand()
35 | .card()
36 | .onClick { print(element.id) }
37 | .padding(20)
38 | .frame(minWidth: 300, minHeight: 200)
39 | .frame(maxWidth: 500)
40 | }
41 | .longSwipes()
42 | }
43 |
44 | }
45 |
46 | // swiftlint:enable missing_docs no_magic_numbers
47 |
--------------------------------------------------------------------------------
/Sources/Demo/CounterDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CounterDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 25.09.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct CounterDemo: View {
13 |
14 | @State("count")
15 | private var count = 0
16 |
17 | var view: Body {
18 | VStack {
19 | HStack {
20 | CountButton(count: $count, icon: .goPrevious) { $0 -= 1 }
21 | Text("\(count)")
22 | .title1()
23 | .frame(minWidth: 100)
24 | CountButton(count: $count, icon: .goNext) { $0 += 1 }
25 | }
26 | .halign(.center)
27 | }
28 | .valign(.center)
29 | .padding()
30 | }
31 |
32 | private struct CountButton: View {
33 |
34 | @Binding var count: Int
35 | var icon: Icon.DefaultIcon
36 | var action: (inout Int) -> Void
37 |
38 | var view: Body {
39 | Button(icon: .default(icon: icon)) {
40 | action(&count)
41 | }
42 | .circular()
43 | }
44 | }
45 |
46 | }
47 |
48 | // swiftlint:enable missing_docs
49 |
--------------------------------------------------------------------------------
/Sources/Demo/DialogDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DialogDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 20.13.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct DialogDemo: View {
13 |
14 | @State private var dialog = false
15 | let padding = 20
16 | let width = 300
17 | let height = 200
18 |
19 | var view: Body {
20 | VStack {
21 | Button("Show Dialog") {
22 | dialog = true
23 | }
24 | .pill()
25 | .suggested()
26 | .frame(maxWidth: 100)
27 | .padding()
28 | }
29 | .dialog(visible: $dialog, title: "Counter", width: width, height: height) {
30 | CounterDemo()
31 | .padding(padding)
32 | .topToolbar {
33 | HeaderBar.empty()
34 | }
35 | }
36 | }
37 |
38 | }
39 |
40 | // swiftlint:enable missing_docs
41 |
--------------------------------------------------------------------------------
/Sources/Demo/DiceDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DiceDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 12.10.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 |
12 | struct DiceDemo: View {
13 |
14 | @State private var number: Int?
15 |
16 | private var label: String {
17 | if let number {
18 | return "\(number)"
19 | } else {
20 | return "Roll the Dice!"
21 | }
22 | }
23 |
24 | var view: Body {
25 | VStack {
26 | Button(label) {
27 | number = .random(in: 1...6)
28 | }
29 | .pill()
30 | .suggested()
31 | .style("dice-button")
32 | .css {
33 | """
34 | .dice-button {
35 | background-color: @green_5;
36 | }
37 | """
38 | }
39 | .frame(maxWidth: 100)
40 | }
41 | .valign(.center)
42 | .padding()
43 | }
44 |
45 | }
46 |
47 | // swiftlint:enable missing_docs no_magic_numbers
48 |
--------------------------------------------------------------------------------
/Sources/Demo/FixedDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FixedDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.07.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | struct FixedDemo: View {
14 |
15 | @State private var button = (x: 0.0, y: 0.0)
16 |
17 | var view: Body {
18 | Fixed()
19 | .element(x: button.x, y: button.y, id: "button") {
20 | Button("Move") {
21 | button = (x: Double.random(in: 0...100), y: Double.random(in: 0...100))
22 | }
23 | }
24 | }
25 |
26 | }
27 |
28 | // swiftlint:enable missing_docs
29 |
--------------------------------------------------------------------------------
/Sources/Demo/FlowBoxDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 01.01.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | struct FlowBoxDemo: View {
14 |
15 | @State private var items: [ListDemo.Element] = []
16 | @State private var selectedItem = ""
17 |
18 | var view: Body {
19 | HStack {
20 | Button("Add Element") {
21 | let element = ListDemo.Element(id: UUID().uuidString)
22 | items.append(element)
23 | selectedItem = element.id
24 | }
25 | Button("Delete Selected Element") {
26 | let index = items.firstIndex { $0.id == selectedItem }
27 | items = items.filter { $0.id != selectedItem }
28 | selectedItem = items[safe: index]?.id ?? items[safe: index ?? 0 - 1]?.id ?? items.first?.id ?? ""
29 | }
30 | }
31 | .linked()
32 | .padding()
33 | .halign(.center)
34 | if !items.isEmpty {
35 | FlowBox(items, selection: $selectedItem) { item in
36 | HStack {
37 | Text(.init("\(item.id)".prefix(5)))
38 | .hexpand()
39 | }
40 | .padding()
41 | }
42 | .valign(.center)
43 | .padding()
44 | }
45 | }
46 |
47 | }
48 |
49 | // swiftlint:enable missing_docs no_magic_numbers
50 |
--------------------------------------------------------------------------------
/Sources/Demo/FormDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 03.01.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 |
12 | struct FormDemo: View {
13 |
14 | var app: AdwaitaApp
15 |
16 | var view: Body {
17 | VStack {
18 | Button("View Demo") {
19 | app.showWindow("form-demo")
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | }
25 | }
26 |
27 | struct WindowContent: View {
28 |
29 | @State private var text = "They also have a subtitle"
30 | @State private var password = "Password"
31 | @State private var value = 0
32 | @State private var isOn = true
33 | @State private var selection = "World"
34 |
35 | let values: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")]
36 |
37 | var view: Body {
38 | ScrollView {
39 | VStack {
40 | actionRows
41 | FormSection("Entry Rows") {
42 | Form {
43 | EntryRow("Entry Row", text: $text)
44 | .suffix {
45 | Button(icon: .default(icon: .editCopy)) { AdwaitaApp.copy(text) }
46 | .flat()
47 | .valign(.center)
48 | }
49 | EntryRow(password, text: $password)
50 | .secure(text: $password)
51 | }
52 | }
53 | .padding()
54 | rowDemo("Spin Rows", row: SpinRow("Spin Row", value: $value, min: 0, max: 100).subtitle("\(value)"))
55 | rowDemo("Switch Rows", row: SwitchRow("Switch Row", isOn: $isOn).subtitle(isOn ? "On" : "Off"))
56 | rowDemo(
57 | "Combo Rows",
58 | row: ComboRow("Combo Row", selection: $selection, values: values).subtitle(selection)
59 | )
60 | rowDemo("Expander Rows", row: ExpanderRow().title("Expander Row").rows {
61 | ActionRow("Hello")
62 | ActionRow("World")
63 | })
64 | }
65 | .padding()
66 | .frame(maxWidth: 400)
67 | }
68 | .topToolbar {
69 | HeaderBar.empty()
70 | }
71 | }
72 |
73 | var actionRows: AnyView {
74 | Form {
75 | ActionRow("Rows have a title")
76 | .subtitle(text)
77 | ActionRow("Rows can have suffix widgets")
78 | .suffix {
79 | Button("Action") { }
80 | .valign(.center)
81 | }
82 | }
83 | .padding()
84 | }
85 |
86 | func rowDemo(_ title: String, row: AnyView) -> AnyView {
87 | FormSection(title) {
88 | Form {
89 | row
90 | }
91 | }
92 | .padding()
93 | }
94 |
95 | }
96 |
97 | }
98 |
99 | // swiftlint:enable missing_docs no_magic_numbers
100 |
--------------------------------------------------------------------------------
/Sources/Demo/IdleDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IdleDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 05.05.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct IdleDemo: View {
13 |
14 | @State private var progress = 0.0
15 | @State private var activeProcess = false
16 | let max = 500.0
17 | let delayFactor = 5_000.0
18 | let maxWidth = 300
19 |
20 | var view: Body {
21 | ProgressBar(value: progress, total: max)
22 | .vexpand()
23 | .valign(.center)
24 | .frame(maxWidth: maxWidth)
25 | Button("Play") {
26 | Task {
27 | activeProcess = true
28 | progress = 0
29 | Idle(delay: .init(delayFactor / max)) {
30 | progress += 1
31 | let done = progress == max
32 | if done {
33 | activeProcess = false
34 | }
35 | return !done
36 | }
37 | }
38 | }
39 | .padding()
40 | .pill()
41 | .hexpand()
42 | .halign(.center)
43 | .insensitive(activeProcess)
44 | }
45 |
46 | }
47 |
48 | // swiftlint:enable missing_docs
49 |
--------------------------------------------------------------------------------
/Sources/Demo/ListDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 01.01.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | struct ListDemo: View {
14 |
15 | @State private var items: [Element] = []
16 | @State private var selectedItem = ""
17 |
18 | var view: Body {
19 | HStack {
20 | Button("Add Row") {
21 | let element = Element(id: UUID().uuidString)
22 | items.append(element)
23 | selectedItem = element.id
24 | }
25 | Button("Delete Selected Row") {
26 | let index = items.firstIndex { $0.id == selectedItem }
27 | items = items.filter { $0.id != selectedItem }
28 | selectedItem = items[safe: index]?.id ?? items[safe: index ?? 0 - 1]?.id ?? items.first?.id ?? ""
29 | }
30 | }
31 | .linked()
32 | .padding()
33 | .halign(.center)
34 | if !items.isEmpty {
35 | List(items, selection: $selectedItem) { item in
36 | HStack {
37 | Text("\(item.id)")
38 | .hexpand()
39 | }
40 | .padding()
41 | }
42 | .boxedList()
43 | .valign(.center)
44 | .padding()
45 | }
46 | }
47 |
48 | struct Element: Identifiable, CustomStringConvertible, Equatable {
49 |
50 | var id: String
51 | var description: String { id }
52 |
53 | }
54 |
55 | }
56 |
57 | // swiftlint:enable missing_docs
58 |
--------------------------------------------------------------------------------
/Sources/Demo/NavigationViewDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationViewDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 17.02.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct NavigationViewDemo: View {
13 |
14 | var app: AdwaitaApp
15 |
16 | var view: Body {
17 | VStack {
18 | Button("View Demo") {
19 | app.showWindow("navigation")
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | .padding()
25 | }
26 | }
27 |
28 | struct WindowContent: View {
29 |
30 | @State private var stack = NavigationStack()
31 | let spacing = 10
32 |
33 | var view: Body {
34 | NavigationView($stack, "Initial View") { component in
35 | VStack {
36 | Button("Add Next View") {
37 | stack.push(component + 1)
38 | }
39 | Button("Pop View") {
40 | stack.pop()
41 | }
42 | }
43 | .spacing(spacing)
44 | .padding()
45 | .topToolbar {
46 | HeaderBar.empty()
47 | }
48 | } initialView: {
49 | VStack {
50 | Button("Add First View") {
51 | stack.push(1)
52 | }
53 | .padding()
54 | }
55 | .topToolbar {
56 | HeaderBar.empty()
57 | }
58 | }
59 | }
60 |
61 | }
62 |
63 | }
64 |
65 | // swiftlint:enable missing_docs
66 |
--------------------------------------------------------------------------------
/Sources/Demo/Page.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Page.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 25.09.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs implicitly_unwrapped_optional
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible {
14 |
15 | case welcome
16 | case counter
17 | case windows
18 | case toolbar
19 | case transition
20 | case dice
21 | case dialog
22 | case alertDialog
23 | case toast
24 | case list
25 | case carousel
26 | case viewSwitcher
27 | case form
28 | case passwordChecker
29 | case popover
30 | case flowBox
31 | case navigationView
32 | case picture
33 | case idle
34 | case fixed
35 |
36 | var id: Self {
37 | self
38 | }
39 |
40 | var label: String {
41 | switch self {
42 | case .viewSwitcher:
43 | return "View Switcher"
44 | case .flowBox:
45 | return "Flow Box"
46 | case .navigationView:
47 | return "Navigation View"
48 | case .alertDialog:
49 | return "Alert Dialog"
50 | case .passwordChecker:
51 | return "Password Checker"
52 | default:
53 | return rawValue.capitalized
54 | }
55 | }
56 |
57 | var icon: Icon? {
58 | switch self {
59 | case .welcome:
60 | return .default(icon: .emojiNature)
61 | default:
62 | return nil
63 | }
64 | }
65 |
66 | var description: String {
67 | switch self {
68 | case .welcome:
69 | return "This is a collection of examples for the Swift Adwaita package"
70 | case .counter:
71 | return "A simple sample view"
72 | case .windows:
73 | return "Showcase window management"
74 | case .toolbar:
75 | return "Toggle the bottom toolbar"
76 | case .transition:
77 | return "A slide transition between two views"
78 | case .dice:
79 | return "Roll the dice"
80 | case .dialog:
81 | return "A window on top of another window"
82 | case .alertDialog:
83 | return "A dialog presenting a message or question"
84 | case .toast:
85 | return "Show a notification inside of your app"
86 | case .list:
87 | return "Organize content in multiple rows"
88 | case .carousel:
89 | return "Scroll horizontally on a touchpad or touchscreen, or scroll down on your mouse wheel"
90 | case .viewSwitcher:
91 | return "Switch the window's view"
92 | case .form:
93 | return "Group controls used for data entry"
94 | case .passwordChecker:
95 | return "Check the validity of a password"
96 | case .popover:
97 | return "Present content in a bubble-like context popup"
98 | case .flowBox:
99 | return "Display views in a reflowing grid"
100 | case .navigationView:
101 | return "A page-based navigation container"
102 | case .picture:
103 | return "Display an image"
104 | case .idle:
105 | return "Update UI from an asynchronous context"
106 | case .fixed:
107 | return "Place widgets in a coordinate system"
108 | }
109 | }
110 |
111 | // swiftlint:disable cyclomatic_complexity
112 | @ViewBuilder
113 | func view(app: AdwaitaApp!, window: AdwaitaWindow, toast: Signal) -> Body {
114 | switch self {
115 | case .welcome:
116 | []
117 | case .counter:
118 | CounterDemo()
119 | case .windows:
120 | WindowsDemo(app: app)
121 | case .toolbar:
122 | ToolbarDemo(app: app)
123 | case .transition:
124 | TransitionDemo()
125 | case .dice:
126 | DiceDemo()
127 | case .dialog:
128 | DialogDemo()
129 | case .alertDialog:
130 | AlertDialogDemo()
131 | case .toast:
132 | ToastDemo(toast: toast)
133 | case .list:
134 | ListDemo()
135 | case .carousel:
136 | CarouselDemo()
137 | case .viewSwitcher:
138 | ViewSwitcherDemo(app: app)
139 | case .form:
140 | FormDemo(app: app)
141 | case .passwordChecker:
142 | PasswordCheckerDemo(app: app)
143 | case .popover:
144 | PopoverDemo()
145 | case .flowBox:
146 | FlowBoxDemo()
147 | case .navigationView:
148 | NavigationViewDemo(app: app)
149 | case .picture:
150 | PictureDemo(app: app, window: window)
151 | case .idle:
152 | IdleDemo()
153 | case .fixed:
154 | FixedDemo()
155 | }
156 | }
157 | // swiftlint:enable cyclomatic_complexity
158 |
159 | }
160 |
161 | // swiftlint:enable missing_docs implicitly_unwrapped_optional
162 |
--------------------------------------------------------------------------------
/Sources/Demo/PictureDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PictureDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 21.04.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 | import Foundation
12 |
13 | struct PictureDemo: View {
14 |
15 | @State private var fileDialog = Signal()
16 | @State private var url: URL?
17 | var app: AdwaitaApp
18 | var window: AdwaitaWindow
19 |
20 | var data: Data {
21 | guard let url, let data = try? Data(contentsOf: url) else {
22 | return .init()
23 | }
24 | return data
25 | }
26 |
27 | var view: Body {
28 | Picture()
29 | .data(data)
30 | Button("Import") {
31 | fileDialog.signal()
32 | }
33 | .halign(.center)
34 | .pill()
35 | .suggested()
36 | .padding()
37 | .fileImporter(open: fileDialog, extensions: ["jpg", "jpeg", "png", "svg"]) { url = $0 } onClose: { }
38 | }
39 |
40 | }
41 |
42 | // swiftlint:enable missing_docs
43 |
--------------------------------------------------------------------------------
/Sources/Demo/PopoverDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 10.02.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct PopoverDemo: View {
13 |
14 | @State private var visible = false
15 |
16 | var view: Body {
17 | VStack {
18 | Button("Present Popover") {
19 | visible = true
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | .popover(visible: $visible) {
25 | CounterDemo()
26 | }
27 | }
28 | }
29 |
30 | }
31 |
32 | // swiftlint:enable missing_docs
33 |
--------------------------------------------------------------------------------
/Sources/Demo/ToastDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToastDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 30.11.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct ToastDemo: View {
13 |
14 | var toast: Signal
15 |
16 | var view: Body {
17 | VStack {
18 | Button("Add Toast") {
19 | toast.signal()
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | }
25 | }
26 |
27 | }
28 |
29 | // swiftlint:enable missing_docs
30 |
--------------------------------------------------------------------------------
/Sources/Demo/ToolbarDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 12.10.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 |
12 | struct ToolbarDemo: View {
13 |
14 | var app: AdwaitaApp
15 |
16 | var view: Body {
17 | VStack {
18 | Button("View Demo") {
19 | app.showWindow("toolbar-demo")
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | }
25 | }
26 |
27 | struct WindowContent: View {
28 |
29 | @State private var visible = false
30 | @State private var moreContent = false
31 |
32 | var view: Body {
33 | VStack {
34 | Button("Toggle Toolbar") {
35 | visible.toggle()
36 | }
37 | .suggested()
38 | .pill()
39 | .frame(maxWidth: 100)
40 | .padding(15)
41 | }
42 | .valign(.center)
43 | .bottomToolbar(visible: visible) {
44 | HeaderBar(titleButtons: false) {
45 | Button(icon: .default(icon: .audioInputMicrophone)) { }
46 | } end: {
47 | Button(icon: .default(icon: .userTrash)) { }
48 | }
49 | .headerBarTitle { }
50 | }
51 | .topToolbar {
52 | HeaderBar.empty()
53 | }
54 | }
55 |
56 | }
57 |
58 | }
59 |
60 | // swiftlint:enable missing_docs no_magic_numbers
61 |
--------------------------------------------------------------------------------
/Sources/Demo/TransitionDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransitionDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 12.10.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs no_magic_numbers
9 |
10 | import Adwaita
11 |
12 | struct TransitionDemo: View {
13 |
14 | @State private var firstView = true
15 |
16 | var view: Body {
17 | VStack {
18 | if firstView {
19 | Text("First View")
20 | .accent()
21 | .transition(.slideDown)
22 | } else {
23 | Text("Second View")
24 | .success()
25 | .transition(.slideUp)
26 | }
27 | }
28 | .modifyContent(Text.self) { $0.title2().padding() }
29 | .card()
30 | .frame(maxWidth: 200)
31 | .padding()
32 | Button("Toggle View") {
33 | firstView.toggle()
34 | }
35 | .pill()
36 | .padding()
37 | .frame(maxWidth: 100)
38 | }
39 |
40 | }
41 |
42 | // swiftlint:enable missing_docs no_magic_numbers
43 |
--------------------------------------------------------------------------------
/Sources/Demo/ViewSwitcherDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToolbarDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 03.01.24.
6 | //
7 |
8 | // swiftlint:disable missing_docs
9 |
10 | import Adwaita
11 |
12 | struct ViewSwitcherDemo: View {
13 |
14 | var app: AdwaitaApp
15 |
16 | var view: Body {
17 | VStack {
18 | Button("View Demo") {
19 | app.showWindow("switcher-demo")
20 | }
21 | .suggested()
22 | .pill()
23 | .frame(maxWidth: 100)
24 | }
25 | }
26 |
27 | struct WindowContent: View {
28 |
29 | @State private var selection: ViewSwitcherView = .albums
30 | @State private var bottom = false
31 |
32 | var view: Body {
33 | VStack {
34 | Text(selection.title)
35 | .padding()
36 | HStack {
37 | Button(bottom ? "Show Top Bar" : "Show Bottom Bar") {
38 | bottom.toggle()
39 | }
40 | }
41 | .halign(.center)
42 | }
43 | .valign(.center)
44 | .topToolbar {
45 | if bottom {
46 | HeaderBar
47 | .empty()
48 | } else {
49 | toolbar
50 | }
51 | }
52 | .bottomToolbar(visible: bottom) {
53 | toolbar
54 | }
55 | }
56 |
57 | var toolbar: AnyView {
58 | HeaderBar(titleButtons: !bottom) { } end: { }
59 | .headerBarTitle {
60 | ViewSwitcher(selection: $selection)
61 | .wideDesign(!bottom)
62 | }
63 | }
64 |
65 | }
66 |
67 | enum ViewSwitcherView: String, ViewSwitcherOption {
68 |
69 | case albums
70 | case artists
71 | case songs
72 | case playlists
73 |
74 | var title: String {
75 | rawValue.capitalized
76 | }
77 |
78 | var icon: Icon {
79 | .default(icon: {
80 | switch self {
81 | case .albums:
82 | return .mediaOpticalCdAudio
83 | case .artists:
84 | return .avatarDefault
85 | case .songs:
86 | return .emblemMusic
87 | case .playlists:
88 | return .viewList
89 | }
90 | }())
91 | }
92 |
93 | init?(title: String) {
94 | self.init(rawValue: title.lowercased())
95 | }
96 |
97 | }
98 |
99 | }
100 |
101 | // swiftlint:enable missing_docs
102 |
--------------------------------------------------------------------------------
/Sources/Demo/WindowsDemo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowsDemo.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 25.09.23.
6 | //
7 |
8 | // swiftlint:disable missing_docs implicitly_unwrapped_optional
9 |
10 | import Adwaita
11 |
12 | struct WindowsDemo: View {
13 |
14 | var app: AdwaitaApp!
15 |
16 | var view: Body {
17 | HStack {
18 | VStack {
19 | Button("Show Window") {
20 | app.showWindow("content")
21 | }
22 | .hexpand()
23 | Button("Add Window") {
24 | app.addWindow("main")
25 | }
26 | .hexpand()
27 | }
28 | .linked()
29 | .valign(.center)
30 | .padding()
31 | }
32 | .frame(maxWidth: 100)
33 | }
34 |
35 | struct WindowContent: View {
36 |
37 | var view: Body {
38 | Text("This window exists at most once.")
39 | .padding()
40 | .topToolbar {
41 | HeaderBar.empty()
42 | }
43 | }
44 |
45 | }
46 |
47 | }
48 |
49 | // swiftlint:enable missing_docs implicitly_unwrapped_optional
50 |
--------------------------------------------------------------------------------
/Sources/Generation/Extensions/String.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | extension String: @retroactive CodingKey {
9 |
10 | /// The string.
11 | public var stringValue: String {
12 | self
13 | }
14 |
15 | /// A string cannot be represented as an integer.
16 | public var intValue: Int? {
17 | nil
18 | }
19 |
20 | /// Initialize from an int value.
21 | /// - Parameter intValue: The int value.
22 | public init?(intValue: Int) {
23 | nil
24 | }
25 |
26 | /// Initialize from a string value.
27 | /// - Parameter stringValue: The string value.
28 | public init?(stringValue: String) {
29 | self = stringValue
30 | }
31 |
32 | /// Generate a doc comment out of the string.
33 | /// - Parameter indent: Indentation added at the beginning of every line.
34 | /// - Returns: The comment.
35 | func docComment(indent: String = "") -> String {
36 | split(separator: "\n", omittingEmptySubsequences: false)
37 | .map { $0.trimmingCharacters(in: .whitespaces) }
38 | .enumerated()
39 | .map { $0.offset == 0 ? $0.element.prefix(1).capitalized + $0.element.dropFirst() : $0.element }
40 | .map { "\(indent)/// \($0)" }
41 | .joined(separator: "\n")
42 | }
43 |
44 | /// Convert delimited to camel casing.
45 | /// - Parameters:
46 | /// - delimiter: The demiliter.
47 | /// - unshorten: Whether to unshorten.
48 | /// - configuration: The generation configuration.
49 | /// - Returns: The string using camel casing.
50 | func convertDelimitedCasingToCamel(
51 | delimiter: Character,
52 | configuration: GenerationConfiguration,
53 | unshorten: Bool = false
54 | ) -> String {
55 | var parts = split(separator: delimiter).map(String.init)
56 | for (index, part) in parts.enumerated() {
57 | if let replacement = configuration.unshorteningMap[part] {
58 | parts[index] = replacement
59 | }
60 | }
61 | let first = parts.removeFirst()
62 | return first + parts.map(\.capitalized).joined()
63 | }
64 |
65 | /// Convert a C type to its Swift equivalent using the generation configuration.
66 | /// - Parameter configuration: The generation configuration.
67 | /// - Returns: The Swift type.
68 | func convertCType(configuration: GenerationConfiguration) -> String {
69 | if let replacement = configuration.cTypeReplacements[self] {
70 | return replacement
71 | }
72 | var type = self
73 | if type.last == "*" {
74 | let pointeeType = String(type.dropLast()).convertCType(configuration: configuration)
75 | type = "UnsafeMutablePointer<\(pointeeType)>!"
76 | }
77 | return type
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Conformance.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Conformance.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.02.24.
6 | //
7 |
8 | /// A protocol conformance.
9 | struct Conformance: Decodable {
10 |
11 | /// The type name.
12 | var name: String
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Constructor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constructor.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// An initializer.
9 | struct Constructor: Decodable {
10 |
11 | /// The identifier of the C constructor.
12 | var cIdentifier: String
13 | /// The parameters.
14 | var parameters: Parameters?
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/GIR.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GIR.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | import Foundation
9 | import XMLCoder
10 |
11 | /// The result of decoding a GIR file.
12 | struct GIR: Decodable {
13 |
14 | /// The namespace.
15 | var namespace: Namespace
16 |
17 | /// Decode a GIR file.
18 | /// - Returns: The GIR data.
19 | static func decodeGIR(_ data: Data) throws -> Self {
20 | let decoder = XMLDecoder()
21 | decoder.keyDecodingStrategy = .custom { path in
22 | let codingKey = path[path.count - 1]
23 | let containsColon = codingKey.stringValue.contains(":")
24 | let containsHyphen = codingKey.stringValue.contains("-")
25 | if containsColon || containsHyphen {
26 | var input = codingKey.stringValue
27 | var output = ""
28 |
29 | // Remove namespace
30 | if containsColon {
31 | let parts = input.split(separator: ":").map(String.init)
32 | output = parts[0]
33 | input = parts[1]
34 | }
35 |
36 | // Convert kebab-case to camelCase
37 | if containsHyphen {
38 | var parts = input.split(separator: "-")
39 | let firstPart = String(parts.removeFirst())
40 | if containsColon {
41 | output += firstPart.capitalized
42 | } else {
43 | output += firstPart
44 | }
45 |
46 | for part in parts {
47 | output += part.capitalized
48 | }
49 | } else {
50 | output += input.capitalized
51 | }
52 | return output
53 | } else {
54 | return codingKey
55 | }
56 | }
57 | return try decoder.decode(Self.self, from: data)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/GIRType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GIRType.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// A GIR type.
9 | struct GIRType: Decodable {
10 |
11 | /// The type's name.
12 | var name: String?
13 | /// The C type.
14 | var cType: String?
15 |
16 | /// Whether the type is `GtkWidget`.
17 | var isWidget: Bool { name == "Widget" || name == "Gtk.Widget" }
18 | /// Whether the type is `GioMenuModel`.
19 | var isMenu: Bool { name == "MenuModel" || name == "Gio.MenuModel" }
20 |
21 | /// Generate the Swift representation of the type.
22 | /// - Parameter configuration: The generation configuration.
23 | /// - Returns: The code.
24 | func generate(configuration: GenerationConfiguration) throws -> String {
25 | if let cType {
26 | return cType.convertCType(configuration: configuration)
27 | } else if let name, let type = configuration.cTypeReplacements[name] {
28 | return type
29 | }
30 | return "String"
31 | }
32 |
33 | /// Generate the Swift type as a binding.
34 | /// - Parameter configuration: The generation configuration.
35 | /// - Returns: The code.
36 | func binding(configuration: GenerationConfiguration) throws -> String {
37 | "Binding<\(try generate(configuration: configuration))>"
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Interface.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interface.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.02.24.
6 | //
7 |
8 | /// An interface (protocol).
9 | struct Interface: ClassLike, Decodable {
10 |
11 | /// The type's name.
12 | var name: String
13 | /// The C symbol prefix.
14 | var cSymbolPrefix: String
15 | /// The C type.
16 | var cType: String?
17 |
18 | /// The signals.
19 | var signals: [Signal]
20 | /// The properties.
21 | var properties: [Property]
22 |
23 | /// The coding keys.
24 | enum CodingKeys: String, CodingKey {
25 |
26 | /// Coding key.
27 | case name, cSymbolPrefix, cType
28 | /// Coding key.
29 | case signals = "glibSignal"
30 | /// Coding key
31 | case properties = "property"
32 |
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Namespace.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Namespace.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// A namespace.
9 | struct Namespace: Decodable {
10 |
11 | /// The classes.
12 | var classes: [Class]
13 | /// The interfaces (protocols).
14 | var interfaces: [Interface]
15 |
16 | /// The coding keys.
17 | enum CodingKeys: String, CodingKey {
18 |
19 | /// Coding key.
20 | case classes = "class"
21 | /// Coding key.
22 | case interfaces = "interface"
23 |
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Parameter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parameter.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// A parameter.
9 | struct Parameter: Decodable { }
10 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Parameters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parameters.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// Parameters.
9 | struct Parameters: Decodable {
10 |
11 | /// The parameters.
12 | var parameters: [Parameter]
13 |
14 | /// The coding keys.
15 | enum CodingKeys: String, CodingKey {
16 |
17 | /// Coding key.
18 | case parameters = "parameter"
19 |
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/Generation/GIR/Signal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Signal.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | /// A signal.
9 | struct Signal: Decodable {
10 |
11 | /// The signal's name.
12 | var name: String
13 | /// The signal's documentation.
14 | var doc: String?
15 | /// The signal's parameters.
16 | var parameters: Parameters?
17 |
18 | /// Generate the signal's property.
19 | /// - Parameters:
20 | /// - config: The widget configuration.
21 | /// - genConfig: The generation configuration.
22 | /// - Returns: The code.
23 | func generateProperty(config: WidgetConfiguration, genConfig: GenerationConfiguration) -> String {
24 | let name = name.convertDelimitedCasingToCamel(delimiter: "-", configuration: genConfig, unshorten: true)
25 | return """
26 |
27 | \(doc?.docComment(indent: " ") ?? " /// \(name)")
28 | var \(name): (() -> Void)?
29 | """
30 | }
31 |
32 | /// Generate the signal's modifier.
33 | /// - Parameters:
34 | /// - config: The widget configuration.
35 | /// - genConfig: The generation configuration.
36 | /// - Returns: The code.
37 | func generateModifier(config: WidgetConfiguration, genConfig: GenerationConfiguration) -> String {
38 | let name = name.convertDelimitedCasingToCamel(delimiter: "-", configuration: genConfig, unshorten: true)
39 | return """
40 |
41 | \(doc?.docComment(indent: " ") ?? " /// \(name)")
42 | public func \(name)(_ \(name): @escaping () -> Void) -> Self {
43 | var newSelf = self
44 | newSelf.\(name) = \(name)
45 | return newSelf
46 | }
47 |
48 | """
49 | }
50 |
51 | /// Generate the signal's modification.
52 | /// - Parameters:
53 | /// - config: The widget configuration.
54 | /// - genConfig: The generation configuration.
55 | /// - Returns: The code.
56 | func generateModification(config: WidgetConfiguration, genConfig: GenerationConfiguration) -> String {
57 | let name = name.convertDelimitedCasingToCamel(delimiter: "-", configuration: genConfig, unshorten: true)
58 | return """
59 |
60 | if let \(name) {
61 | storage.connectSignal(name: "\(self.name)", argCount: \(parameters?.parameters.count ?? 0)) {
62 | \(name)()
63 | }
64 | }
65 | """
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/Generation/WidgetConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WidgetConfiguration.swift
3 | // Adwaita
4 | //
5 | // Created by david-swift on 14.01.24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The configuration for a single widget.
11 | struct WidgetConfiguration {
12 |
13 | /// The class name.
14 | var `class`: String
15 | /// The name of the Swift structure.
16 | var name: String?
17 | /// Explicitly set the C initializer.
18 | var initializer: String?
19 | /// The properties that should be treated as bindings.
20 | var bindings: [BindingConfiguration] = []
21 | /// The dynamic widget.
22 | var dynamicWidget: DynamicWidget?
23 | /// The static widgets.
24 | var staticWidgets: [StaticWidget] = []
25 | /// Non-optional properties.
26 | var requiredProperties: [String] = []
27 | /// Excluded properties.
28 | var excludeProperties: [String] = []
29 | /// Excluded signals.
30 | var excludeSignals: [String] = []
31 | /// Whether to cast the type.
32 | var cast = false
33 | /// Conditions for when to update a property.
34 | var setConditions: [String: String] = [:]
35 | /// Properties that are set in the end of an update.
36 | var lastProperties: [String] = []
37 |
38 | /// The configuration for a binding.
39 | struct BindingConfiguration {
40 |
41 | /// The property.
42 | var property: String
43 | /// The signal. If not specified, use "notify::property-name".
44 | var signal: String?
45 |
46 | }
47 |
48 | /// The configuration for a dynamic widget.
49 | struct DynamicWidget {
50 |
51 | /// The insert function.
52 | var insert: String
53 | /// The remove function.
54 | var remove: String
55 | /// Get a child to remove from its index.
56 | var getElement: String = "index.cInt"
57 |
58 | }
59 |
60 | /// The configuration for a static widget.
61 | struct StaticWidget {
62 |
63 | /// The widget's name.
64 | var name: String
65 | /// The function for adding a child.
66 | var add: String
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/io.github.AparokshaUI.Demo.json:
--------------------------------------------------------------------------------
1 | {
2 | "app-id": "io.github.AparokshaUI.Demo",
3 | "runtime": "org.gnome.Platform",
4 | "runtime-version": "47",
5 | "sdk": "org.gnome.Sdk",
6 | "sdk-extensions": [
7 | "org.freedesktop.Sdk.Extension.swift6"
8 | ],
9 | "command": "Demo",
10 | "finish-args": [
11 | "--share=network",
12 | "--share=ipc",
13 | "--socket=fallback-x11",
14 | "--device=dri",
15 | "--socket=wayland"
16 | ],
17 | "build-options": {
18 | "append-path": "/usr/lib/sdk/swift6/bin",
19 | "prepend-ld-library-path": "/usr/lib/sdk/swift6/lib"
20 | },
21 | "cleanup": [
22 | "/include",
23 | "/lib/pkgconfig",
24 | "/man",
25 | "/share/doc",
26 | "/share/gtk-doc",
27 | "/share/man",
28 | "/share/pkgconfig",
29 | "/share/vala",
30 | "*.la",
31 | "*.a"
32 | ],
33 | "modules": [
34 | {
35 | "name": "Demo",
36 | "builddir": true,
37 | "buildsystem": "simple",
38 | "sources": [
39 | {
40 | "type": "dir",
41 | "path": "."
42 | }
43 | ],
44 | "build-commands": [
45 | "swift build -c debug --static-swift-stdlib",
46 | "install -Dm755 .build/debug/Demo /app/bin/Demo"
47 | ]
48 | }
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------