├── .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 | > [![No GitHub](https://nogithub.codeberg.page/badge.svg)](https://sfconservancy.org/GiveUpGitHub/) 10 | 11 |

12 | Adwaita Icon 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 | /// bin 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 | /// ![An example GtkSeparator](separator.png) 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 | /// ![An example GtkSpinner](spinner.png) 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 | /// status-page 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 | /// toast-overlay 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 | /// window-title 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 | --------------------------------------------------------------------------------