├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── nix-flake.check.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── README.md ├── REUSE.toml ├── deny.toml ├── examples ├── basic-alc │ ├── Cargo.toml │ ├── layout.alc │ └── main.rs ├── basic-rs │ ├── Cargo.toml │ └── main.rs ├── calculator-alc │ ├── Cargo.toml │ ├── build.rs │ ├── layout.alc │ └── src │ │ ├── calculator.udl │ │ └── main.rs ├── calculator-rs │ ├── Cargo.toml │ ├── build.rs │ ├── calculator-cs.sln │ ├── calculator-cs │ │ ├── Program.cs │ │ ├── calc.cs │ │ └── calculator-cs.csproj │ └── src │ │ ├── bin.rs │ │ ├── calculator.toml │ │ ├── calculator.udl │ │ └── lib.rs ├── graph-rs │ ├── Cargo.toml │ └── main.rs ├── grid-rs │ ├── Cargo.toml │ └── main.rs ├── list-rs │ ├── Cargo.toml │ └── main.rs ├── paragraph-rs │ ├── Cargo.toml │ └── main.rs └── textbox-rs │ ├── Cargo.toml │ └── main.rs ├── feather-macro ├── Cargo.toml └── src │ └── lib.rs ├── feather-ui ├── Cargo.toml ├── b-tree.alc ├── feather.alc ├── rrb-vector.alc └── src │ ├── component │ ├── button.rs │ ├── domain_line.rs │ ├── domain_point.rs │ ├── flexbox.rs │ ├── gridbox.rs │ ├── line.rs │ ├── listbox.rs │ ├── mod.rs │ ├── mouse_area.rs │ ├── paragraph.rs │ ├── region.rs │ ├── shape.rs │ ├── text.rs │ ├── textbox.rs │ └── window.rs │ ├── draw.rs │ ├── input.rs │ ├── layout │ ├── base.rs │ ├── domain_write.rs │ ├── fixed.rs │ ├── flex.rs │ ├── grid.rs │ ├── leaf.rs │ ├── list.rs │ ├── mod.rs │ ├── root.rs │ └── text.rs │ ├── lib.rs │ ├── lua.rs │ ├── persist.rs │ ├── propbag.rs │ ├── render │ ├── chain.rs │ ├── domain │ │ ├── line.rs │ │ └── mod.rs │ ├── line.rs │ ├── mod.rs │ ├── standard.rs │ └── text.rs │ ├── rtree.rs │ ├── shaders │ ├── Arc.frag.glsl │ ├── Arc.wgsl │ ├── Circle.frag.glsl │ ├── Circle.wgsl │ ├── Image.frag.glsl │ ├── Image.vert.glsl │ ├── Line.frag.glsl │ ├── Line.frag.wgsl │ ├── Line.vert.glsl │ ├── Line.vert.wgsl │ ├── RoundRect.frag.glsl │ ├── RoundRect.wgsl │ ├── Triangle.frag.glsl │ ├── Triangle.wgsl │ ├── mod.rs │ ├── standard.vert.glsl │ └── standard.wgsl │ └── text.rs ├── flake.lock └── flake.nix /.envrc: -------------------------------------------------------------------------------- 1 | use flake; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: cargo 7 | directory: "/" 8 | schedule: 9 | interval: daily 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | permissions: 5 | contents: read 6 | 7 | name: CI 8 | 9 | on: 10 | push: 11 | branches: [ master ] 12 | pull_request: 13 | branches: [ master ] 14 | schedule: 15 | # run weekly 16 | - cron: '0 0 * * 0' 17 | 18 | env: 19 | CARGO_TERM_COLOR: always 20 | 21 | jobs: 22 | build: 23 | strategy: 24 | matrix: 25 | rust: 26 | - nightly 27 | - beta 28 | - stable 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Install Rust 35 | run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} 36 | 37 | - name: Install Clang 38 | run: | 39 | export DEBIAN_FRONTEND=noninteractive 40 | sudo apt update 41 | sudo apt install -y clang-15 42 | sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-15 60 43 | sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-15 60 44 | 45 | - name: Build 46 | run: cargo build --all 47 | 48 | - name: Build in release mode 49 | run: cargo build --all --release 50 | 51 | # We can't run any tests, because they would need a window manager and graphics to exist, which aren't available on standard CI 52 | 53 | fmt: 54 | name: formatting 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | toolchain: nightly 61 | override: true 62 | profile: minimal 63 | components: rustfmt 64 | - uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --all -- --check --unstable-features 68 | 69 | cargo-deny: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: EmbarkStudios/cargo-deny-action@v2 74 | with: 75 | log-level: warn 76 | command: check 77 | arguments: --all-features 78 | 79 | # Check for typos in the repository based on a static dictionary 80 | typos: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | 85 | # This is pinned to a specific version because the typos dictionary can 86 | # be updated between patch versions, and a new dictionary can find new 87 | # typos in the repo thus suddenly breaking CI unless we pin the version. 88 | - uses: crate-ci/typos@v1.32.0 89 | -------------------------------------------------------------------------------- /.github/workflows/nix-flake.check.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | permissions: 5 | contents: read 6 | 7 | name: nix flake check 8 | 9 | on: 10 | push: 11 | pull_request: 12 | 13 | jobs: 14 | check: 15 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: DeterminateSystems/nix-installer-action@main 20 | - run: nix flake check -L -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | .vs/ 5 | .vscode/ 6 | *.exe 7 | /target/ 8 | *.user 9 | /.direnv/ 10 | /examples/calculator-rs/calculator-cs/obj 11 | /examples/calculator-rs/calculator-cs/bin 12 | .rustfmt.toml -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [workspace] 5 | members = [ 6 | "feather-ui", 7 | "examples/basic-rs", 8 | "examples/basic-alc", 9 | "examples/paragraph-rs", 10 | "examples/calculator-rs", 11 | # "examples/calculator-alc", # This breaks cargo for some reason 12 | "examples/graph-rs", 13 | "examples/grid-rs", 14 | "examples/list-rs", 15 | "examples/textbox-rs", 16 | "feather-macro", 17 | ] 18 | resolver = "2" 19 | default-members = [ 20 | "feather-ui", 21 | "examples/basic-rs", 22 | "examples/paragraph-rs", 23 | "examples/graph-rs", 24 | "examples/list-rs", 25 | "examples/grid-rs", 26 | "feather-macro", 27 | ] 28 | 29 | [workspace.package] 30 | version = "0.1.5" 31 | edition = "2024" 32 | rust-version = "1.86.0" 33 | license = "Apache-2.0" 34 | homepage = "https://github.com/Fundament-Software/feathergui" 35 | repository = "https://github.com/Fundament-Software/feathergui/" 36 | readme = "README.md" 37 | 38 | [workspace.dependencies] 39 | im = "15.1" 40 | wgpu = "25" 41 | winit = "0.30" 42 | eyre = "0.6" 43 | ultraviolet = "0.10" 44 | rand = { version = "0.8.5", features = ["std_rng"] } 45 | tracing-subscriber = { version = "0.3.18", features = ["time"] } 46 | tracing = "0.1.40" 47 | tokio = { version = "1.40", features = ["rt", "macros", "tracing", "signal"] } 48 | feather-ui = { path = "feather-ui" } 49 | alicorn = "0.1.2" 50 | mlua = { version = "0.10", features = ["luajit52", "vendored"] } 51 | glyphon = "0.9.0" 52 | feather-macro = { version = "0.1", path = "feather-macro" } 53 | 54 | [workspace.lints] 55 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Feather UI 2 | 3 | Feather is a universal UI library that applies user inputs to application state, and maps application state to an interactive visualization using a custom graphics rendering language capable of compiling to arbitrary GPU code or vectorized CPU code. 4 | 5 | This project is currently in a prototyping stage, it is not yet suitable for production, and not all planned features are implemented. 6 | 7 | ## Building 8 | 9 | Feather is a standard rust project, simply run `cargo build` on your platform of choice. A NixOS flake is included that provides a develop environment for nix developers who do not have rust installed system-wide. 10 | 11 | ## Running 12 | 13 | Two working examples are available: `basic-rs` and `paragraph-rs`. To run either, navigate into the folder and run `cargo run`. 14 | 15 | The examples have currently only been tested on NixOS and Windows 11, but should work on most systems. 16 | 17 | ## Funding 18 | 19 | This project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/FeatherUI). 20 | 21 | [NLnet foundation logo](https://nlnet.nl) 22 | [NGI Zero Logo](https://nlnet.nl/core) 23 | 24 | ## License 25 | Copyright © 2025 Fundament Software SPC 26 | 27 | Distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 28 | 29 | SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "Feather-UI" 3 | SPDX-PackageSupplier = "Fundament Software " 4 | SPDX-PackageDownloadLocation = "https://github.com/Fundament-Software/feathergui" 5 | 6 | [[annotations]] 7 | path = "feather-ui/src/shaders/**" 8 | precedence = "aggregate" 9 | SPDX-FileCopyrightText = "2025 Fundament Software SPC " 10 | SPDX-License-Identifier = "Apache-2.0" 11 | 12 | 13 | [[annotations]] 14 | path = "examples/calculator-rs/calculator-cs/**" 15 | precedence = "aggregate" 16 | SPDX-FileCopyrightText = "2025 Fundament Software SPC " 17 | SPDX-License-Identifier = "Apache-2.0" 18 | 19 | [[annotations]] 20 | path = "*.lock" 21 | precedence = "aggregate" 22 | SPDX-FileCopyrightText = "2025 Fundament Software SPC " 23 | SPDX-License-Identifier = "Apache-2.0" 24 | 25 | 26 | [[annotations]] 27 | path = ".envrc" 28 | precedence = "aggregate" 29 | SPDX-FileCopyrightText = "2025 Fundament Software SPC " 30 | SPDX-License-Identifier = "Apache-2.0" 31 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [graph] 5 | targets = [] 6 | all-features = false 7 | no-default-features = false 8 | exclude = [] 9 | 10 | [output] 11 | feature-depth = 1 12 | 13 | [advisories] 14 | db-path = "$CARGO_HOME/advisory-dbs" 15 | db-urls = ["https://github.com/rustsec/advisory-db"] 16 | ignore = ["RUSTSEC-2024-0436"] 17 | [licenses] 18 | allow = [ 19 | "MIT", 20 | "Apache-2.0", 21 | "Apache-2.0 WITH LLVM-exception", 22 | "Zlib", 23 | "MPL-2.0", 24 | "BSD-3-Clause", 25 | "BSD-2-Clause", 26 | "Unicode-3.0", 27 | "CC0-1.0", 28 | "ISC", 29 | "BSL-1.0", 30 | ] 31 | confidence-threshold = 0.8 32 | 33 | [licenses.private] 34 | ignore = false 35 | registries = [] 36 | 37 | [bans] 38 | multiple-versions = "allow" 39 | allow = [] 40 | deny = [] 41 | skip = [] 42 | skip-tree = [] 43 | 44 | [sources] 45 | unknown-registry = "warn" 46 | unknown-git = "warn" 47 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 48 | allow-git = [] 49 | -------------------------------------------------------------------------------- /examples/basic-alc/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "basic-alc" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Basic feather window using Alicorn 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "basic-alc" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | alicorn.workspace = true 29 | mlua.workspace = true 30 | -------------------------------------------------------------------------------- /examples/basic-alc/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use feather_ui::App; 5 | use feather_ui::lua::{AppState, LuaApp}; 6 | use mlua::Function; 7 | use mlua::prelude::*; 8 | 9 | fn wrap_luafunc( 10 | f: Function, 11 | ) -> impl FnMut(feather_ui::DispatchPair, AppState) -> Result { 12 | move |pair, state| Ok(f.call((pair.0, state)).unwrap()) 13 | } 14 | 15 | fn main() { 16 | let lua = Lua::new(); 17 | let mut feather_interface = lua.create_table().unwrap(); 18 | feather_ui::lua::init_environment(&lua, &mut feather_interface).unwrap(); 19 | let alicorn = Box::new(alicorn::Alicorn::new(lua, &feather_interface).unwrap()); 20 | 21 | // Load the built-in GLSL prelude from alicorn 22 | alicorn.load_glsl_prelude().unwrap(); 23 | 24 | // Because of constraints on lifetimes, this needs to technically last forever. 25 | let alicorn = Box::leak(alicorn); 26 | { 27 | // This compiles and executes an alicorn program, which then calls back into the lua environment we have created in feather-ui/src/lua.rs 28 | let (window, init, onclick): (Function, Function, Function) = alicorn 29 | .execute(include_str!("layout.alc"), "layout.alc") 30 | .unwrap(); 31 | 32 | let onclick = Box::new(wrap_luafunc(onclick)); 33 | let outline = LuaApp { window, init }; 34 | let (mut app, event_loop): (App, winit::event_loop::EventLoop<()>) = 35 | App::new(LuaValue::Integer(0), vec![onclick], outline).unwrap(); 36 | 37 | event_loop.run_app(&mut app).unwrap(); 38 | } 39 | //drop(unsafe { Box::from_raw(alicorn) }); 40 | } 41 | -------------------------------------------------------------------------------- /examples/basic-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "basic-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Basic feather window 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "basic-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | feather-macro.workspace = true 29 | -------------------------------------------------------------------------------- /examples/basic-rs/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use feather_macro::*; 5 | use feather_ui::component::button::Button; 6 | use feather_ui::component::region::Region; 7 | use feather_ui::component::shape::Shape; 8 | use feather_ui::component::text::Text; 9 | use feather_ui::component::window::Window; 10 | use feather_ui::component::{ComponentFrom, mouse_area}; 11 | use feather_ui::layout::{fixed, leaf}; 12 | use feather_ui::persist::FnPersist; 13 | use feather_ui::{ 14 | AbsRect, App, DAbsRect, DPoint, DRect, RelRect, Slot, SourceID, UNSIZED_AXIS, URect, gen_id, 15 | }; 16 | use std::rc::Rc; 17 | use ultraviolet::{Vec2, Vec4}; 18 | 19 | #[derive(PartialEq, Clone, Debug)] 20 | struct CounterState { 21 | count: i32, 22 | } 23 | 24 | #[derive(Default, Empty, Area, Anchor, ZIndex, Limits, RLimits, Padding)] 25 | struct FixedData { 26 | area: DRect, 27 | anchor: DPoint, 28 | limits: feather_ui::DLimits, 29 | rlimits: feather_ui::RelLimits, 30 | padding: DAbsRect, 31 | zindex: i32, 32 | } 33 | 34 | impl fixed::Prop for FixedData {} 35 | impl fixed::Child for FixedData {} 36 | impl leaf::Prop for FixedData {} 37 | impl leaf::Padded for FixedData {} 38 | 39 | struct BasicApp {} 40 | 41 | impl FnPersist, Option>> for BasicApp { 42 | type Store = (CounterState, im::HashMap, Option>); 43 | 44 | fn init(&self) -> Self::Store { 45 | (CounterState { count: -1 }, im::HashMap::new()) 46 | } 47 | fn call( 48 | &self, 49 | mut store: Self::Store, 50 | args: &CounterState, 51 | ) -> (Self::Store, im::HashMap, Option>) { 52 | if store.0 != *args { 53 | let button = { 54 | let text = Text:: { 55 | id: gen_id!().into(), 56 | props: Rc::new(FixedData { 57 | area: URect { 58 | abs: AbsRect::new(8.0, 0.0, 8.0, 0.0), 59 | rel: RelRect::new(0.0, 0.5, UNSIZED_AXIS, UNSIZED_AXIS), 60 | } 61 | .into(), 62 | anchor: feather_ui::RelPoint(Vec2 { x: 0.0, y: 0.5 }).into(), 63 | ..Default::default() 64 | }), 65 | text: format!("Clicks: {}", args.count), 66 | font_size: 40.0, 67 | line_height: 56.0, 68 | ..Default::default() 69 | }; 70 | 71 | let mut children: im::Vector>>> = 72 | im::Vector::new(); 73 | children.push_back(Some(Box::new(text))); 74 | 75 | let rect = Shape::::round_rect( 76 | gen_id!().into(), 77 | feather_ui::FILL_DRECT.into(), 78 | 0.0, 79 | 0.0, 80 | Vec4::broadcast(10.0), 81 | Vec4::new(0.2, 0.7, 0.4, 1.0), 82 | Vec4::zero(), 83 | ); 84 | children.push_back(Some(Box::new(rect))); 85 | 86 | Button::::new( 87 | gen_id!().into(), 88 | FixedData { 89 | area: URect { 90 | abs: AbsRect::new(45.0, 45.0, 0.0, 0.0), 91 | rel: RelRect::new(0.0, 0.0, UNSIZED_AXIS, 1.0), 92 | } 93 | .into(), 94 | ..Default::default() 95 | }, 96 | Slot(feather_ui::APP_SOURCE_ID.into(), 0), 97 | children, 98 | ) 99 | }; 100 | 101 | let unusedbutton = { 102 | let text = Text:: { 103 | id: gen_id!().into(), 104 | props: Rc::new(FixedData { 105 | area: RelRect::new(0.5, 0.0, UNSIZED_AXIS, UNSIZED_AXIS).into(), 106 | limits: feather_ui::AbsLimits::new( 107 | Vec2::new(f32::NEG_INFINITY, 10.0), 108 | Vec2::new(f32::INFINITY, 200.0), 109 | ) 110 | .into(), 111 | rlimits: feather_ui::RelLimits::new( 112 | Vec2::new(f32::NEG_INFINITY, f32::NEG_INFINITY), 113 | Vec2::new(1.0, f32::INFINITY), 114 | ), 115 | anchor: feather_ui::RelPoint(Vec2 { x: 0.5, y: 0.0 }).into(), 116 | padding: AbsRect::new(8.0, 8.0, 8.0, 8.0).into(), 117 | ..Default::default() 118 | }), 119 | text: (0..args.count).map(|_| "█").collect::(), 120 | font_size: 40.0, 121 | line_height: 56.0, 122 | wrap: feather_ui::Wrap::WordOrGlyph, 123 | ..Default::default() 124 | }; 125 | 126 | let mut children: im::Vector>>> = 127 | im::Vector::new(); 128 | children.push_back(Some(Box::new(text))); 129 | 130 | let rect = Shape::::round_rect( 131 | gen_id!().into(), 132 | feather_ui::FILL_DRECT.into(), 133 | 0.0, 134 | 0.0, 135 | Vec4::broadcast(10.0), 136 | Vec4::new(0.7, 0.2, 0.4, 1.0), 137 | Vec4::zero(), 138 | ); 139 | children.push_back(Some(Box::new(rect))); 140 | 141 | Button::::new( 142 | gen_id!().into(), 143 | FixedData { 144 | area: URect { 145 | abs: AbsRect::new(45.0, 245.0, 0.0, 0.0), 146 | rel: RelRect::new(0.0, 0.0, UNSIZED_AXIS, UNSIZED_AXIS), 147 | } 148 | .into(), 149 | limits: feather_ui::AbsLimits::new( 150 | Vec2::new(100.0, f32::NEG_INFINITY), 151 | Vec2::new(300.0, f32::INFINITY), 152 | ) 153 | .into(), 154 | ..Default::default() 155 | }, 156 | Slot(feather_ui::APP_SOURCE_ID.into(), 0), 157 | children, 158 | ) 159 | }; 160 | 161 | let mut children: im::Vector>>> = 162 | im::Vector::new(); 163 | children.push_back(Some(Box::new(button))); 164 | children.push_back(Some(Box::new(unusedbutton))); 165 | 166 | let region = Region { 167 | id: gen_id!().into(), 168 | props: FixedData { 169 | area: URect { 170 | abs: AbsRect::new(90.0, 90.0, 0.0, 200.0), 171 | rel: RelRect::new(0.0, 0.0, UNSIZED_AXIS, 0.0), 172 | } 173 | .into(), 174 | zindex: 0, 175 | ..Default::default() 176 | } 177 | .into(), 178 | children, 179 | }; 180 | let window = Window::new( 181 | gen_id!().into(), 182 | winit::window::Window::default_attributes() 183 | .with_title(env!("CARGO_CRATE_NAME")) 184 | .with_resizable(true), 185 | Box::new(region), 186 | ); 187 | 188 | store.1 = im::HashMap::new(); 189 | store.1.insert(window.id.clone(), Some(window)); 190 | store.0 = args.clone(); 191 | } 192 | let windows = store.1.clone(); 193 | (store, windows) 194 | } 195 | } 196 | 197 | use feather_ui::WrapEventEx; 198 | 199 | fn main() { 200 | let onclick = Box::new( 201 | |_: mouse_area::MouseAreaEvent, 202 | mut appdata: CounterState| 203 | -> Result { 204 | { 205 | appdata.count += 1; 206 | Ok(appdata) 207 | } 208 | } 209 | .wrap(), 210 | ); 211 | 212 | let (mut app, event_loop): ( 213 | App, 214 | winit::event_loop::EventLoop<()>, 215 | ) = App::new(CounterState { count: 0 }, vec![onclick], BasicApp {}).unwrap(); 216 | 217 | event_loop.run_app(&mut app).unwrap(); 218 | } 219 | -------------------------------------------------------------------------------- /examples/calculator-alc/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "calculator-alc" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Basic calculator using Alicorn 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "calculator-alc" 19 | path = "src/main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | alicorn.workspace = true 29 | mlua.workspace = true 30 | uniffi = { version = "0.29", features = ["scaffolding-ffi-buffer-fns"] } 31 | uniffi-alicorn = { version = "0.1.2", features = [ 32 | "scaffolding-ffi-buffer-fns", 33 | ] } 34 | thiserror = "2.0" 35 | 36 | [build-dependencies] 37 | # Add the "scaffolding-ffi-buffer-fns" feature to make sure things can build correctly 38 | uniffi = { version = "0.29", features = [ 39 | "build", 40 | "scaffolding-ffi-buffer-fns", 41 | ] } 42 | uniffi-alicorn = { version = "0.1.2", features = [ 43 | "build", 44 | "scaffolding-ffi-buffer-fns", 45 | ] } 46 | 47 | [dev-dependencies] 48 | uniffi-alicorn = { version = "0.1.2", features = ["bindgen-tests"] } 49 | -------------------------------------------------------------------------------- /examples/calculator-alc/build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | fn main() { 5 | println!("cargo::rerun-if-changed=src/calculator.udl"); 6 | println!("cargo::rerun-if-changed=layout.alc"); // avoid cachelighting when rust behaves badly and doesn't realize the file changed 7 | uniffi::generate_scaffolding("src/calculator.udl").unwrap(); 8 | uniffi_alicorn::generate_alicorn_scaffolding("src/calculator.udl").unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /examples/calculator-alc/src/calculator.udl: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | enum CalcOp { 5 | "None", 6 | "Add", 7 | "Sub", 8 | "Mul", 9 | "Div", 10 | "Mod", 11 | "Pow", 12 | "Square", 13 | "Sqrt", 14 | "Inv", 15 | "Negate", 16 | "Clear", 17 | }; 18 | 19 | [Trait, WithForeign] 20 | interface Calculator { 21 | Calculator copy(); 22 | boolean eq(Calculator rhs); 23 | Calculator add_digit(u8 digit); 24 | Calculator backspace(); 25 | Calculator apply_op(); 26 | Calculator set_op(CalcOp op); 27 | Calculator toggle_decimal(); 28 | double get(); 29 | }; 30 | 31 | namespace calc { 32 | Calculator register(); 33 | }; -------------------------------------------------------------------------------- /examples/calculator-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "calculator-rs" 6 | version.workspace = true 7 | edition = "2021" # This is stuck on uniffi 0.27 which doesn't emit valid code under 2024 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Simple calculator 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [lib] 18 | name = "calculator" 19 | path = "src/lib.rs" 20 | crate-type = ["lib", "cdylib"] 21 | 22 | [[bin]] 23 | name = "calculator-rs" 24 | path = "src/bin.rs" 25 | 26 | [dependencies] 27 | wgpu.workspace = true 28 | winit.workspace = true 29 | tokio.workspace = true 30 | feather-ui.workspace = true 31 | feather-macro.workspace = true 32 | ultraviolet.workspace = true 33 | im.workspace = true 34 | glyphon.workspace = true 35 | uniffi = "0.27.0" 36 | 37 | [build-dependencies] 38 | uniffi = { version = "0.27.0", features = ["build"] } 39 | -------------------------------------------------------------------------------- /examples/calculator-rs/build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use std::path::PathBuf; 5 | 6 | fn get_cargo_target_dir() -> Result> { 7 | let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); 8 | let profile = std::env::var("PROFILE")?; 9 | let mut target_dir = None; 10 | let mut sub_path = out_dir.as_path(); 11 | while let Some(parent) = sub_path.parent() { 12 | if parent.ends_with(&profile) { 13 | target_dir = Some(parent); 14 | break; 15 | } 16 | sub_path = parent; 17 | } 18 | let target_dir = target_dir.ok_or("not found")?; 19 | Ok(target_dir.to_path_buf()) 20 | } 21 | 22 | fn main() { 23 | uniffi::generate_scaffolding("src/calculator.udl").unwrap(); 24 | 25 | // Attempt to build C# example and copy it to target dir 26 | match std::process::Command::new("dotnet") 27 | .args(["build", "calculator-cs/calculator-cs.csproj"]) 28 | .spawn() 29 | .and_then(|mut c| c.wait()) 30 | .map(|e| e.success()) 31 | { 32 | Ok(true) => { 33 | if let Ok(s) = get_cargo_target_dir() { 34 | let curdir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); 35 | let bin = curdir.join("calculator-cs/bin"); 36 | 37 | let debug = bin.read_dir().unwrap().last().unwrap().unwrap(); 38 | let net8 = debug.path().read_dir().unwrap().last().unwrap().unwrap(); 39 | 40 | std::fs::copy( 41 | net8.path().join("calculator-cs.runtimeconfig.json"), 42 | s.join("calculator-cs.runtimeconfig.json"), 43 | ) 44 | .unwrap(); 45 | std::fs::copy( 46 | net8.path().join("calculator-cs.dll"), 47 | s.join("calculator-cs.dll"), 48 | ) 49 | .unwrap(); 50 | if std::fs::copy( 51 | net8.path().join("calculator-cs.exe"), 52 | s.join("calculator-cs.exe"), 53 | ) 54 | .is_err() 55 | { 56 | std::fs::copy(net8.path().join("calculator-cs"), s.join("calculator-cs")) 57 | .unwrap(); 58 | } 59 | } else { 60 | print!("Couldn't get TARGET_DIR for current crate, C# example not copied to output dir."); 61 | } 62 | } 63 | // We do not panic on error here so systems without dotnet can still build the rust example 64 | Ok(false) => print!("dotnet build failed, calculator-cs will not be available."), 65 | Err(e) => print!( 66 | "Error running dotnet build, calculator-cs will not be available: {}", 67 | e 68 | ), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/calculator-rs/calculator-cs.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | # SPDX-License-Identifier: Apache-2.0 4 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 5 | 6 | VisualStudioVersion = 17.13.35806.99 7 | MinimumVisualStudioVersion = 10.0.40219.1 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "calculator-cs", "calculator-cs\calculator-cs.csproj", "{9D965116-7C3D-4409-A11E-15506F285334}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {9D965116-7C3D-4409-A11E-15506F285334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {9D965116-7C3D-4409-A11E-15506F285334}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {9D965116-7C3D-4409-A11E-15506F285334}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {9D965116-7C3D-4409-A11E-15506F285334}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ExtensibilityGlobals) = postSolution 25 | SolutionGuid = {76FCE354-22B6-4164-BB01-7F6A0E8FA418} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /examples/calculator-rs/calculator-cs/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Win32; 3 | using System.Text.RegularExpressions; 4 | using uniffi.calc; 5 | 6 | // To build the necessary `calc.cs` file that this depends on, run this command from the parent directory: 7 | // 8 | // uniffi-bindgen-cs .\src\calculator.udl --config .\src\calculator.toml 9 | // 10 | // Then, copy calc.cs from src/calc.cs to this directory. 11 | 12 | public class Impl : uniffi.calc.Calculator 13 | { 14 | private double last; 15 | private double? cur; 16 | private List digits; 17 | private List decimals; 18 | private bool decimal_mode; 19 | private CalcOp cur_op; 20 | 21 | public Impl() 22 | { 23 | last = 0.0; 24 | cur = null; 25 | digits = new List(); 26 | decimals = new List(); 27 | decimal_mode = false; 28 | cur_op = CalcOp.None; 29 | } 30 | void update_cur() 31 | { 32 | var x = 0.0; 33 | var mul = 1.0; 34 | digits.Reverse(); 35 | foreach (var v in digits) { 36 | x += v * mul; 37 | mul *= 10; 38 | } 39 | digits.Reverse(); 40 | 41 | mul = 0.1; 42 | foreach (var v in decimals) 43 | { 44 | x += v * mul; 45 | mul /= 10; 46 | } 47 | cur = x; 48 | } 49 | 50 | void apply_op() 51 | { 52 | if(cur != null) { 53 | var x = cur ?? 0; 54 | switch(cur_op) { 55 | case CalcOp.Add: 56 | last = last + x; 57 | break; 58 | case CalcOp.Sub: 59 | last = last - x; 60 | break; 61 | case CalcOp.Mul: 62 | last = last * x; 63 | break; 64 | case CalcOp.Div: 65 | last = last / x; 66 | break; 67 | case CalcOp.Mod: 68 | last = last % x; 69 | break; 70 | case CalcOp.Pow: 71 | last = Math.Pow(last, x); 72 | break; 73 | default: 74 | break; 75 | } 76 | } 77 | 78 | switch(cur_op) 79 | { 80 | case CalcOp.Square: 81 | last = last * last; 82 | break; 83 | case CalcOp.Sqrt: 84 | last = Math.Sqrt(last); 85 | break; 86 | case CalcOp.Inv: 87 | last = Math.ReciprocalEstimate(last); 88 | break; 89 | case CalcOp.Negate: 90 | last = -last; 91 | break; 92 | case CalcOp.Clear: 93 | last = 0.0; 94 | break; 95 | } 96 | 97 | cur = null; 98 | cur_op = CalcOp.None; 99 | decimal_mode = false; 100 | decimals.Clear(); 101 | digits.Clear(); 102 | } 103 | 104 | void Calculator.AddDigit(byte digit) 105 | { 106 | if (@digit == 0 && digits.Count == 0 && !decimal_mode) 107 | { 108 | return; 109 | } 110 | if (decimal_mode) 111 | { 112 | decimals.Add(@digit); 113 | } 114 | else 115 | { 116 | digits.Add(@digit); 117 | } 118 | update_cur(); 119 | } 120 | 121 | void Calculator.ApplyOp() 122 | { 123 | apply_op(); 124 | } 125 | 126 | void Calculator.Backspace() 127 | { 128 | if (decimal_mode) 129 | { 130 | decimals.RemoveAt(decimals.Count - 1); 131 | } 132 | else 133 | { 134 | digits.RemoveAt(decimals.Count - 1); 135 | } 136 | update_cur(); 137 | } 138 | 139 | Calculator Calculator.Copy() 140 | { 141 | var self = new Impl(); 142 | 143 | self.last = last; 144 | self.cur = cur; 145 | self.digits = digits; 146 | self.decimals = decimals; 147 | self.decimal_mode = decimal_mode; 148 | self.cur_op = cur_op; 149 | 150 | return self; 151 | } 152 | 153 | bool Calculator.Eq(Calculator rhs) 154 | { 155 | var v = rhs as Impl; 156 | 157 | if(v == null) 158 | { 159 | return false; 160 | } 161 | 162 | return v.last == last && 163 | v.cur == cur && 164 | v.digits == digits && 165 | v.decimals == decimals && 166 | v.decimal_mode == decimal_mode && 167 | v.cur_op == cur_op; 168 | } 169 | 170 | double Calculator.Get() 171 | { 172 | return cur ?? last; 173 | } 174 | 175 | void Calculator.SetOp(CalcOp op) 176 | { 177 | switch(op) { 178 | case CalcOp.Square | CalcOp.Sqrt | CalcOp.Inv | CalcOp.Negate | CalcOp.Clear: 179 | cur_op = op; 180 | apply_op(); 181 | break; 182 | default: 183 | apply_op(); 184 | cur_op = op; 185 | break; 186 | } 187 | } 188 | 189 | void Calculator.ToggleDecimal() 190 | { 191 | decimal_mode = !decimal_mode; 192 | } 193 | } 194 | 195 | namespace calculator_cs 196 | { 197 | internal class Program 198 | { 199 | static void Main(string[] args) 200 | { 201 | uniffi.calc.CalcMethods.Register(new Impl()); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /examples/calculator-rs/calculator-cs/calculator-cs.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | calculator_cs 7 | enable 8 | enable 9 | True 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/calculator-rs/src/bin.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use calculator::{CalcOp, Calculator}; 5 | use std::ops::Deref; 6 | use std::sync::{Arc, RwLock}; 7 | 8 | #[derive(PartialEq, Clone, Debug, Default)] 9 | struct CalcState { 10 | last: f64, 11 | cur: Option, 12 | digits: Vec, 13 | decimals: Vec, 14 | decimal_mode: bool, 15 | op: CalcOp, 16 | } 17 | 18 | impl CalcState { 19 | fn update_cur(&mut self) { 20 | let mut cur = 0.0; 21 | let mut mul = 1.0; 22 | for v in self.digits.iter().rev() { 23 | cur += *v as f64 * mul; 24 | mul *= 10.0; 25 | } 26 | let mut mul = 0.1; 27 | for v in self.decimals.iter() { 28 | cur += *v as f64 * mul; 29 | mul /= 10.0; 30 | } 31 | self.cur = Some(cur); 32 | } 33 | 34 | pub fn add_digit(&mut self, digit: u8) { 35 | if digit == 0 && !self.digits.is_empty() && !self.decimal_mode { 36 | return; 37 | } 38 | if self.decimal_mode { 39 | self.decimals.push(digit); 40 | } else { 41 | self.digits.push(digit); 42 | } 43 | self.update_cur(); 44 | } 45 | pub fn backspace(&mut self) { 46 | if self.decimal_mode { 47 | self.decimals.pop(); 48 | } else { 49 | self.digits.pop(); 50 | } 51 | self.update_cur(); 52 | } 53 | 54 | pub fn apply_op(&mut self) { 55 | if let Some(cur) = self.cur { 56 | self.last = match self.op { 57 | CalcOp::None => cur, 58 | CalcOp::Add => self.last + cur, 59 | CalcOp::Sub => self.last - cur, 60 | CalcOp::Mul => self.last * cur, 61 | CalcOp::Div => self.last / cur, 62 | CalcOp::Mod => self.last % cur, 63 | CalcOp::Pow => self.last.powf(cur), 64 | _ => cur, // If this is an instant op, move cur to last 65 | }; 66 | } 67 | self.last = match self.op { 68 | CalcOp::Square => self.last * self.last, 69 | CalcOp::Sqrt => self.last.sqrt(), 70 | CalcOp::Inv => self.last.recip(), 71 | CalcOp::Negate => -self.last, 72 | CalcOp::Clear => { 73 | self.last = 0.0; 74 | self.cur = Some(0.0); 75 | self.op = CalcOp::None; 76 | self.decimal_mode = false; 77 | self.decimals.clear(); 78 | self.digits.clear(); 79 | return; 80 | } 81 | _ => self.last, 82 | }; 83 | self.cur = None; 84 | self.op = CalcOp::None; 85 | self.decimal_mode = false; 86 | self.decimals.clear(); 87 | self.digits.clear(); 88 | } 89 | pub fn set_op(&mut self, op: CalcOp) { 90 | match op { 91 | CalcOp::Square | CalcOp::Sqrt | CalcOp::Inv | CalcOp::Negate | CalcOp::Clear => { 92 | self.op = op; 93 | self.apply_op(); 94 | } 95 | _ => { 96 | self.apply_op(); 97 | self.op = op; 98 | } 99 | }; 100 | } 101 | } 102 | 103 | struct Calc(RwLock); 104 | 105 | impl Calculator for Calc { 106 | fn add_digit(&self, digit: u8) { 107 | self.0.write().unwrap().add_digit(digit) 108 | } 109 | fn backspace(&self) { 110 | self.0.write().unwrap().backspace() 111 | } 112 | fn apply_op(&self) { 113 | self.0.write().unwrap().apply_op() 114 | } 115 | fn set_op(&self, op: CalcOp) { 116 | self.0.write().unwrap().set_op(op) 117 | } 118 | fn get(&self) -> f64 { 119 | let state = self.0.read().unwrap(); 120 | state.cur.unwrap_or(state.last) 121 | } 122 | fn toggle_decimal(&self) { 123 | let prev = self.0.read().unwrap().decimal_mode; 124 | self.0.write().unwrap().decimal_mode = !prev; 125 | } 126 | 127 | fn copy(&self) -> Arc { 128 | Arc::new(Calc(RwLock::new(self.0.read().unwrap().clone()))) 129 | } 130 | 131 | fn eq(&self, rhs: Arc) -> bool { 132 | let rhs = >::as_ref(&rhs); 133 | let rhs = match rhs.downcast_ref::() { 134 | Some(rhs) => rhs, 135 | None => return false, 136 | }; 137 | let lhs = self.0.read().unwrap(); 138 | let rhs = rhs.0.read().unwrap(); 139 | lhs.deref() == rhs.deref() 140 | } 141 | } 142 | 143 | fn main() { 144 | calculator::register(Arc::new(Calc(RwLock::new(CalcState { 145 | last: 0.0, 146 | cur: None, 147 | digits: Vec::new(), 148 | decimals: Vec::new(), 149 | decimal_mode: false, 150 | op: CalcOp::None, 151 | })))); 152 | } 153 | -------------------------------------------------------------------------------- /examples/calculator-rs/src/calculator.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [bindings.csharp] 5 | cdylib_name = "calculator.dll" 6 | -------------------------------------------------------------------------------- /examples/calculator-rs/src/calculator.udl: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | enum CalcOp { 5 | "None", 6 | "Add", 7 | "Sub", 8 | "Mul", 9 | "Div", 10 | "Mod", 11 | "Pow", 12 | "Square", 13 | "Sqrt", 14 | "Inv", 15 | "Negate", 16 | "Clear", 17 | }; 18 | 19 | [Trait, WithForeign] 20 | interface Calculator { 21 | Calculator copy(); 22 | boolean eq(Calculator rhs); 23 | void add_digit(u8 digit); 24 | void backspace(); 25 | void apply_op(); 26 | void set_op(CalcOp op); 27 | void toggle_decimal(); 28 | double get(); 29 | }; 30 | 31 | namespace calc { 32 | void register(Calculator calc); 33 | }; -------------------------------------------------------------------------------- /examples/graph-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "graph-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Simple graph editor 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "graph-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | glyphon.workspace = true 29 | feather-macro.workspace = true 30 | -------------------------------------------------------------------------------- /examples/grid-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "grid-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Testbed for grids 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "grid-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | glyphon.workspace = true 29 | feather-macro.workspace = true 30 | -------------------------------------------------------------------------------- /examples/list-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "list-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Testbed for margins in Lists and Flexboxes 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "list-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | glyphon.workspace = true 29 | feather-macro.workspace = true 30 | -------------------------------------------------------------------------------- /examples/paragraph-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "paragraph-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Basic paragraph example 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "paragraph-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | glyphon.workspace = true 29 | feather-macro.workspace = true 30 | -------------------------------------------------------------------------------- /examples/paragraph-rs/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use feather_ui::layout::{fixed, flex, leaf}; 5 | use feather_ui::{DAbsRect, DValue, gen_id}; 6 | 7 | use feather_ui::component::ComponentFrom; 8 | use feather_ui::component::paragraph::Paragraph; 9 | use feather_ui::component::region::Region; 10 | use feather_ui::component::shape::Shape; 11 | use feather_ui::component::window::Window; 12 | use feather_ui::layout::base; 13 | use feather_ui::persist::FnPersist; 14 | use feather_ui::{AbsRect, App, DRect, FILL_DRECT, RelRect, SourceID}; 15 | use std::f32; 16 | use std::rc::Rc; 17 | use ultraviolet::Vec4; 18 | 19 | #[derive(PartialEq, Clone, Debug)] 20 | struct Blocker { 21 | area: AbsRect, 22 | } 23 | 24 | struct BasicApp {} 25 | 26 | #[derive(Default, Clone, feather_macro::Area)] 27 | struct MinimalFlexChild { 28 | area: DRect, 29 | } 30 | 31 | impl flex::Child for MinimalFlexChild { 32 | fn grow(&self) -> f32 { 33 | 0.0 34 | } 35 | 36 | fn shrink(&self) -> f32 { 37 | 1.0 38 | } 39 | 40 | fn basis(&self) -> DValue { 41 | 100.0.into() 42 | } 43 | } 44 | 45 | impl base::Order for MinimalFlexChild {} 46 | impl base::Anchor for MinimalFlexChild {} 47 | impl base::Padding for MinimalFlexChild {} 48 | impl base::Margin for MinimalFlexChild {} 49 | impl base::Limits for MinimalFlexChild {} 50 | impl base::RLimits for MinimalFlexChild {} 51 | impl leaf::Prop for MinimalFlexChild {} 52 | impl leaf::Padded for MinimalFlexChild {} 53 | 54 | #[derive(Default, Clone, feather_macro::Empty, feather_macro::Area)] 55 | struct MinimalArea { 56 | area: DRect, 57 | } 58 | 59 | impl base::ZIndex for MinimalArea {} 60 | impl base::Anchor for MinimalArea {} 61 | impl base::Limits for MinimalArea {} 62 | impl fixed::Prop for MinimalArea {} 63 | 64 | #[derive(Default, Clone, feather_macro::Empty, feather_macro::Area)] 65 | struct MinimalFlex { 66 | obstacles: Vec, 67 | area: DRect, 68 | } 69 | impl base::Direction for MinimalFlex {} 70 | impl base::ZIndex for MinimalFlex {} 71 | impl base::Limits for MinimalFlex {} 72 | impl base::RLimits for MinimalFlex {} 73 | impl fixed::Child for MinimalFlex {} 74 | 75 | impl base::Obstacles for MinimalFlex { 76 | fn obstacles(&self) -> &[DAbsRect] { 77 | &self.obstacles 78 | } 79 | } 80 | 81 | impl flex::Prop for MinimalFlex { 82 | fn wrap(&self) -> bool { 83 | true 84 | } 85 | 86 | fn justify(&self) -> flex::FlexJustify { 87 | flex::FlexJustify::Start 88 | } 89 | 90 | fn align(&self) -> flex::FlexJustify { 91 | flex::FlexJustify::Start 92 | } 93 | } 94 | 95 | impl FnPersist, Option>> for BasicApp { 96 | type Store = (Blocker, im::HashMap, Option>); 97 | 98 | fn init(&self) -> Self::Store { 99 | ( 100 | Blocker { 101 | area: AbsRect::new(f32::NAN, f32::NAN, f32::NAN, f32::NAN), 102 | }, 103 | im::HashMap::new(), 104 | ) 105 | } 106 | fn call( 107 | &self, 108 | mut store: Self::Store, 109 | args: &Blocker, 110 | ) -> (Self::Store, im::HashMap, Option>) { 111 | if store.0 != *args { 112 | let flex = { 113 | let rect = Shape::round_rect( 114 | gen_id!().into(), 115 | MinimalFlexChild { 116 | area: AbsRect::new(0.0, 0.0, 40.0, 40.0).into(), 117 | } 118 | .into(), 119 | 0.0, 120 | 0.0, 121 | Vec4::broadcast(10.0), 122 | Vec4::new(0.2, 0.7, 0.4, 1.0), 123 | Vec4::zero(), 124 | ); 125 | 126 | let mut p = Paragraph::new( 127 | gen_id!().into(), 128 | MinimalFlex { 129 | area: FILL_DRECT, 130 | obstacles: vec![AbsRect::new(200.0, 30.0, 300.0, 150.0).into()], 131 | }, 132 | ); 133 | 134 | let text = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"; 135 | p.set_text( 136 | text, 137 | 40.0, 138 | 56.0, 139 | glyphon::FamilyOwned::SansSerif, 140 | glyphon::Color::rgba(255, 255, 255, 255), 141 | Default::default(), 142 | Default::default(), 143 | true, 144 | ); 145 | p.children.push_front(Some(Box::new(rect.clone()))); 146 | p.children.push_back(Some(Box::new(rect.clone()))); 147 | p.children.push_back(Some(Box::new(rect.clone()))); 148 | 149 | p 150 | }; 151 | 152 | let mut children: im::Vector>>> = 153 | im::Vector::new(); 154 | children.push_back(Some(Box::new(flex))); 155 | 156 | let region = Region { 157 | id: gen_id!().into(), 158 | props: MinimalArea { 159 | area: feather_ui::URect { 160 | abs: AbsRect::new(90.0, 90.0, -90.0, -90.0), 161 | rel: RelRect::new(0.0, 0.0, 1.0, 1.0), 162 | } 163 | .into(), 164 | } 165 | .into(), 166 | children, 167 | }; 168 | let window = Window::new( 169 | gen_id!().into(), 170 | winit::window::Window::default_attributes() 171 | .with_title(env!("CARGO_CRATE_NAME")) 172 | .with_resizable(true), 173 | Box::new(region), 174 | ); 175 | 176 | store.1 = im::HashMap::new(); 177 | store.1.insert(window.id.clone(), Some(window)); 178 | store.0 = args.clone(); 179 | } 180 | let windows = store.1.clone(); 181 | (store, windows) 182 | } 183 | } 184 | 185 | fn main() { 186 | let (mut app, event_loop): (App, winit::event_loop::EventLoop<()>) = 187 | App::new( 188 | Blocker { 189 | area: AbsRect::new(-1.0, -1.0, -1.0, -1.0), 190 | }, 191 | vec![], 192 | BasicApp {}, 193 | ) 194 | .unwrap(); 195 | 196 | event_loop.run_app(&mut app).unwrap(); 197 | } 198 | -------------------------------------------------------------------------------- /examples/textbox-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "textbox-rs" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Basic textbox example 12 | """ 13 | homepage.workspace = true 14 | readme.workspace = true 15 | license.workspace = true 16 | 17 | [[bin]] 18 | name = "textbox-rs" 19 | path = "main.rs" 20 | 21 | [dependencies] 22 | wgpu.workspace = true 23 | winit.workspace = true 24 | tokio.workspace = true 25 | feather-ui.workspace = true 26 | ultraviolet.workspace = true 27 | im.workspace = true 28 | glyphon.workspace = true 29 | feather-macro.workspace = true 30 | -------------------------------------------------------------------------------- /examples/textbox-rs/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use feather_ui::layout::{fixed, leaf}; 5 | use feather_ui::text::{EditObj, Snapshot}; 6 | use feather_ui::{DAbsRect, gen_id}; 7 | 8 | use feather_ui::component::region::Region; 9 | use feather_ui::component::textbox::TextBox; 10 | use feather_ui::component::window::Window; 11 | use feather_ui::component::{ComponentFrom, textbox}; 12 | use feather_ui::layout::base; 13 | use feather_ui::persist::FnPersist; 14 | use feather_ui::{AbsRect, App, DRect, FILL_DRECT, RelRect, SourceID}; 15 | use std::rc::Rc; 16 | 17 | #[derive(PartialEq, Clone, Debug, Default)] 18 | struct TextState { 19 | text: Snapshot, 20 | } 21 | 22 | struct BasicApp {} 23 | 24 | #[derive(Default, Clone, feather_macro::Empty, feather_macro::Area)] 25 | struct MinimalArea { 26 | area: DRect, 27 | } 28 | 29 | impl base::ZIndex for MinimalArea {} 30 | impl base::Anchor for MinimalArea {} 31 | impl base::Limits for MinimalArea {} 32 | impl fixed::Prop for MinimalArea {} 33 | 34 | #[derive( 35 | Clone, 36 | feather_macro::Empty, 37 | feather_macro::Area, 38 | feather_macro::TextEdit, 39 | feather_macro::Padding, 40 | )] 41 | struct MinimalText { 42 | area: DRect, 43 | padding: DAbsRect, 44 | textedit: Snapshot, 45 | } 46 | impl base::Direction for MinimalText {} 47 | impl base::ZIndex for MinimalText {} 48 | impl base::Limits for MinimalText {} 49 | impl base::RLimits for MinimalText {} 50 | impl base::Anchor for MinimalText {} 51 | impl leaf::Padded for MinimalText {} 52 | impl leaf::Prop for MinimalText {} 53 | impl fixed::Child for MinimalText {} 54 | impl textbox::Prop for MinimalText {} 55 | 56 | impl FnPersist, Option>> for BasicApp { 57 | type Store = (TextState, im::HashMap, Option>); 58 | 59 | fn init(&self) -> Self::Store { 60 | ( 61 | TextState { 62 | ..Default::default() 63 | }, 64 | im::HashMap::new(), 65 | ) 66 | } 67 | fn call( 68 | &self, 69 | mut store: Self::Store, 70 | args: &TextState, 71 | ) -> (Self::Store, im::HashMap, Option>) { 72 | if store.0 != *args { 73 | let textbox = TextBox::new( 74 | gen_id!().into(), 75 | MinimalText { 76 | area: FILL_DRECT, 77 | padding: AbsRect::broadcast(12.0).into(), 78 | textedit: store.0.text, 79 | }, 80 | 40.0, 81 | 56.0, 82 | glyphon::FamilyOwned::SansSerif, 83 | glyphon::Color::rgba(255, 255, 255, 255), 84 | Default::default(), 85 | Default::default(), 86 | glyphon::Wrap::Word, 87 | ); 88 | 89 | let mut children: im::Vector>>> = 90 | im::Vector::new(); 91 | children.push_back(Some(Box::new(textbox))); 92 | 93 | let region = Region { 94 | id: gen_id!().into(), 95 | props: MinimalArea { 96 | area: feather_ui::URect { 97 | abs: AbsRect::new(90.0, 90.0, -90.0, -90.0), 98 | rel: RelRect::new(0.0, 0.0, 1.0, 1.0), 99 | } 100 | .into(), 101 | } 102 | .into(), 103 | children, 104 | }; 105 | let window = Window::new( 106 | gen_id!().into(), 107 | winit::window::Window::default_attributes() 108 | .with_title(env!("CARGO_CRATE_NAME")) 109 | .with_resizable(true), 110 | Box::new(region), 111 | ); 112 | 113 | store.1 = im::HashMap::new(); 114 | store.1.insert(window.id.clone(), Some(window)); 115 | store.0 = args.clone(); 116 | } 117 | let windows = store.1.clone(); 118 | (store, windows) 119 | } 120 | } 121 | 122 | fn main() { 123 | let (mut app, event_loop): (App, winit::event_loop::EventLoop<()>) = 124 | App::new( 125 | TextState { 126 | text: EditObj::new("new text".to_string(), (0, 0)).into(), 127 | }, 128 | vec![], 129 | BasicApp {}, 130 | ) 131 | .unwrap(); 132 | 133 | event_loop.run_app(&mut app).unwrap(); 134 | } 135 | -------------------------------------------------------------------------------- /feather-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "feather-macro" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Helper macros for Feather UI library 12 | """ 13 | homepage.workspace = true 14 | repository = "https://github.com/Fundament-Software/feathergui/tree/main/feather-ui" 15 | readme.workspace = true 16 | keywords = ["macros"] 17 | license.workspace = true 18 | 19 | [lib] 20 | proc-macro = true 21 | 22 | [dependencies] 23 | syn = { version = "2", features = ["parsing"] } 24 | quote = { version = "1" } 25 | proc-macro2 = { version = "1", features = ["span-locations"] } 26 | itertools = { version = "0.14" } 27 | proc_macro_roids = "0.8.0" 28 | 29 | [lints] 30 | workspace = true 31 | -------------------------------------------------------------------------------- /feather-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | [package] 5 | name = "feather-ui" 6 | version.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | authors = ["Erik McClure "] 10 | description = """ 11 | Feather UI library 12 | """ 13 | homepage.workspace = true 14 | repository = "https://github.com/Fundament-Software/feathergui/tree/main/feather-ui" 15 | readme.workspace = true 16 | keywords = ["ui", "interface", "graphics", "gpu"] 17 | license.workspace = true 18 | 19 | [lib] 20 | path = "src/lib.rs" 21 | doctest = false 22 | 23 | [dependencies] 24 | im.workspace = true 25 | wgpu.workspace = true 26 | winit.workspace = true 27 | eyre.workspace = true 28 | tracing.workspace = true 29 | tracing-subscriber.workspace = true 30 | tokio.workspace = true 31 | ultraviolet.workspace = true 32 | dyn-clone = "1.0" 33 | derive-where = "1.2.7" 34 | mlua.workspace = true 35 | glyphon.workspace = true 36 | enum_variant_type = "0.3.1" 37 | smallvec = { version = "1.13", features = ["union", "const_generics"] } 38 | thiserror = "2.0" 39 | feather-macro.workspace = true 40 | derive_more = { version = "2.0.1", features = ["try_from"] } 41 | wide = "0.7.32" 42 | alloca = "0.4.0" 43 | unicode-segmentation = "1.12.0" 44 | arboard = { version = "3.5.0", features = ["wayland-data-control"] } 45 | parking_lot = { version = "0.12.3", features = [ 46 | "hardware-lock-elision", 47 | "arc_lock", 48 | ] } 49 | static_assertions = "1.1.0" 50 | windows-sys = { version = "0.59.0", features = [ 51 | "Win32_UI_WindowsAndMessaging", 52 | ] } 53 | 54 | [lints.clippy] 55 | too_many_arguments = { level = "allow" } 56 | -------------------------------------------------------------------------------- /feather-ui/b-tree.alc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | enum Option(T) 5 | Some(v : T) 6 | None 7 | 8 | enum Result(T, E) 9 | Ok(v : T) 10 | Err(e : E) 11 | 12 | enum Either(L, R) 13 | Left(l : L) 14 | Right(r : R) 15 | 16 | enum NodeValue(K : Ord, V) 17 | Child(node : BNode(K, V)) 18 | Value(value : V) 19 | 20 | struct BNode(K : Ord, V) 21 | size : uint 22 | keys : Array(K, size-1) 23 | children : Array(NodeValue(K, V), size) 24 | 25 | struct BTree(K, V, N : Int) 26 | root : BNode(K, V) 27 | size : uint 28 | 29 | def binary_search_inner('K : Ord, 'N : Int, src : Array(K, N), target : K, start : uint, end : uint) -> K 30 | if (start < end) 31 | then 32 | let center = (start + end) / 2 33 | return match compare(target, src[center]) 34 | LT 35 | binary_search_inner(src, target, start, center-1) 36 | EQ 37 | src[center] 38 | GE 39 | binary_search_inner(src, target, center+1, end) 40 | else 41 | return src[start] 42 | 43 | def binary_search('K : Ord, 'N : Int, src : Array(K, N), target : K) -> K 44 | return binary_search_inner(src, target, 0, N-1) 45 | 46 | def empty('K, 'V) 47 | return BNode(K, V){ 48 | size = 0 49 | keys = array-of() 50 | children = array-of() 51 | } 52 | 53 | 54 | module Internal ('K : Ord, 'V) 55 | let Node = BNode(K, V) 56 | 57 | def search(self : Node, key : K) -> Either(uint, uint) 58 | 59 | 60 | def lookup(self : Node, key : K) -> Option(V) 61 | if (self.keys.is_empty()) 62 | then 63 | return None 64 | else 65 | match search_value(key) 66 | Left(found) 67 | self.children( 68 | Right(missed) 69 | 70 | 71 | 72 | def insert(self : Node, key : K, value : V) -> Option(V) 73 | 74 | 75 | unlet Node 76 | 77 | module Methods ('K : Ord, 'V, 'N : Int) 78 | let Self = BTree(K, V, N) 79 | 80 | def new() 81 | return Self{ root = empty(), size = 0 } 82 | 83 | def is_empty(self : Self) -> bool 84 | return self.size == 0 85 | 86 | def len(self : Self) -> uint 87 | return self.size 88 | 89 | def clear(self : Self) 90 | self.size = 0 91 | self.root = empty() 92 | 93 | def get_max(self : Self) -> Option(tuple(K, V)) 94 | return Internal.max(self.root) 95 | 96 | def get_min(self : Self) -> Option(tuple(K, V)) 97 | return Internal.min(self.root) 98 | 99 | def get(key : K) -> Option(V) 100 | return Internal.lookup(self.root, key) 101 | 102 | def contains(key : K) -> bool 103 | return match get(key) 104 | Some(_) 105 | true 106 | None 107 | false 108 | 109 | def insert(key : K, value : V) -> Option(V) 110 | 111 | 112 | unlet Self 113 | 114 | 115 | open-module Methods 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | #[cfg(has_specialisation)] 132 | impl BTreeValue for (K, V) { 133 | default fn search_key(slice: &[Self], key: &BK) -> Result 134 | where 135 | BK: Ord + ?Sized, 136 | Self::Key: Borrow, 137 | { 138 | slice.binary_search_by(|value| Self::Key::borrow(&value.0).cmp(key)) 139 | } 140 | 141 | default fn search_value(slice: &[Self], key: &Self) -> Result { 142 | slice.binary_search_by(|value| value.0.cmp(&key.0)) 143 | } 144 | 145 | fn cmp_keys(&self, other: &BK) -> Ordering 146 | where 147 | BK: Ord + ?Sized, 148 | Self::Key: Borrow, 149 | { 150 | Self::Key::borrow(&self.0).cmp(other) 151 | } 152 | 153 | fn cmp_values(&self, other: &Self) -> Ordering { 154 | self.0.cmp(&other.0) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /feather-ui/feather.alc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | -------------------------------------------------------------------------------- /feather-ui/rrb-vector.alc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | enum Option(T) 5 | Some(v: T) 6 | None 7 | 8 | enum Result(T, E) 9 | Ok(v: T) 10 | Err(e: E) 11 | 12 | enum NodeValue(N, V) 13 | Child(node: N) 14 | Value(value: V) 15 | Empty 16 | 17 | struct Node(K, V, N) 18 | keys: Array(K, N-1) 19 | children: Array(NodeValue(Node(K, V, N), V), N-1) 20 | 21 | struct BTree(K, V, N) 22 | root: Node(K, V, N) 23 | size: usize 24 | 25 | def new(K, V, N) 26 | let self = BTree(K, V, N){ root = Node(K, V, N), size = 0 } 27 | return self 28 | 29 | def is_empty('K, 'V, 'N, self : BTree(K, V, N)) -> bool 30 | return self.size == 0 31 | 32 | def len('K, 'V, 'N, self : BTree(K, V, N)) -> usize 33 | return self.size 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | #[cfg(has_specialisation)] 52 | impl BTreeValue for (K, V) { 53 | default fn search_key(slice: &[Self], key: &BK) -> Result 54 | where 55 | BK: Ord + ?Sized, 56 | Self::Key: Borrow, 57 | { 58 | slice.binary_search_by(|value| Self::Key::borrow(&value.0).cmp(key)) 59 | } 60 | 61 | default fn search_value(slice: &[Self], key: &Self) -> Result { 62 | slice.binary_search_by(|value| value.0.cmp(&key.0)) 63 | } 64 | 65 | fn cmp_keys(&self, other: &BK) -> Ordering 66 | where 67 | BK: Ord + ?Sized, 68 | Self::Key: Borrow, 69 | { 70 | Self::Key::borrow(&self.0).cmp(other) 71 | } 72 | 73 | fn cmp_values(&self, other: &Self) -> Ordering { 74 | self.0.cmp(&other.0) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /feather-ui/src/component/button.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::mouse_area::MouseArea; 5 | 6 | use crate::component::{ComponentFrom, Desc}; 7 | use crate::layout::{Layout, fixed}; 8 | use crate::persist::{FnPersist, VectorMap}; 9 | use crate::{Component, DRect, Slot, SourceID, layout}; 10 | use derive_where::derive_where; 11 | use std::rc::Rc; 12 | 13 | // A button component that contains a mousearea alongside it's children 14 | #[derive_where(Clone)] 15 | pub struct Button { 16 | pub id: Rc, 17 | props: Rc, 18 | marea: MouseArea, 19 | children: im::Vector>>>, 20 | } 21 | 22 | impl Button { 23 | pub fn new( 24 | id: Rc, 25 | props: T, 26 | onclick: Slot, 27 | children: im::Vector>>>, 28 | ) -> Self { 29 | Self { 30 | id: id.clone(), 31 | props: props.into(), 32 | marea: MouseArea::new( 33 | SourceID { 34 | parent: Some(id.clone()), 35 | id: crate::DataID::Named("__marea_internal__"), 36 | } 37 | .into(), 38 | crate::FILL_DRECT, 39 | None, 40 | [Some(onclick), None, None, None, None, None], 41 | ), 42 | children, 43 | } 44 | } 45 | } 46 | 47 | impl crate::StateMachineChild for Button { 48 | fn id(&self) -> Rc { 49 | self.id.clone() 50 | } 51 | 52 | fn apply_children( 53 | &self, 54 | f: &mut dyn FnMut(&dyn crate::StateMachineChild) -> eyre::Result<()>, 55 | ) -> eyre::Result<()> { 56 | for child in self.children.iter() { 57 | f(child.as_ref().unwrap().as_ref())?; 58 | } 59 | f(&self.marea) 60 | } 61 | } 62 | 63 | impl Component for Button 64 | where 65 | for<'a> &'a T: Into<&'a (dyn fixed::Prop + 'static)>, 66 | { 67 | fn layout( 68 | &self, 69 | state: &crate::StateManager, 70 | driver: &crate::DriverState, 71 | window: &Rc, 72 | config: &wgpu::SurfaceConfiguration, 73 | ) -> Box> { 74 | let map = VectorMap::new( 75 | |child: &Option>>| -> Option::Child>>> { 76 | Some(child.as_ref().unwrap().layout(state, driver, window,config)) 77 | }, 78 | ); 79 | 80 | let (_, mut children) = map.call(Default::default(), &self.children); 81 | children.push_back(Some(Box::new( 82 | self.marea.layout(state, driver, window, config), 83 | ))); 84 | 85 | Box::new(layout::Node:: { 86 | props: self.props.clone(), 87 | children, 88 | id: Rc::downgrade(&self.id), 89 | renderable: None, 90 | }) 91 | } 92 | } 93 | 94 | crate::gen_component_wrap!(Button, fixed::Prop); 95 | -------------------------------------------------------------------------------- /feather-ui/src/component/domain_line.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Layout, base}; 5 | use crate::shaders::gen_uniform; 6 | use crate::{CrossReferenceDomain, DriverState, SourceID, layout, render}; 7 | use derive_where::derive_where; 8 | use std::rc::Rc; 9 | use ultraviolet::Vec4; 10 | use wgpu::util::DeviceExt; 11 | 12 | // This draws a line between two points that were previously stored in a Cross-reference Domain 13 | #[derive(feather_macro::StateMachineChild)] 14 | #[derive_where(Clone)] 15 | pub struct DomainLine { 16 | pub id: Rc, 17 | pub domain: Rc, 18 | pub start: Rc, 19 | pub end: Rc, 20 | pub props: Rc, 21 | pub fill: Vec4, 22 | } 23 | 24 | impl super::Component for DomainLine 25 | where 26 | for<'a> &'a T: Into<&'a (dyn base::Empty + 'static)>, 27 | { 28 | fn layout( 29 | &self, 30 | _: &crate::StateManager, 31 | driver: &DriverState, 32 | _window: &Rc, 33 | config: &wgpu::SurfaceConfiguration, 34 | ) -> Box> { 35 | let shader_idx = driver.shader_cache.write().register_shader( 36 | &driver.device, 37 | "Line FS", 38 | include_str!("../shaders/Line.frag.wgsl"), 39 | ); 40 | let pipeline = 41 | driver 42 | .shader_cache 43 | .write() 44 | .line_pipeline(&driver.device, shader_idx, config); 45 | 46 | let mvp = gen_uniform( 47 | driver, 48 | "MVP", 49 | crate::shaders::mat4_proj( 50 | 0.0, 51 | config.height as f32, 52 | config.width as f32, 53 | -(config.height as f32), 54 | 0.2, 55 | 10000.0, 56 | ) 57 | .as_byte_slice(), 58 | ); 59 | 60 | let posdim = driver 61 | .device 62 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 63 | label: Some("PosDim"), 64 | contents: Vec4::new(0.0, 0.0, 1.0, 1.0).as_byte_slice(), 65 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 66 | }); 67 | 68 | let fill = gen_uniform(driver, "Fill", self.fill.as_byte_slice()); 69 | let buffers = [mvp, posdim, fill]; 70 | let bindings: Vec = buffers 71 | .iter() 72 | .enumerate() 73 | .map(|(i, x)| wgpu::BindGroupEntry { 74 | binding: i as u32, 75 | resource: x.as_entire_binding(), 76 | }) 77 | .collect(); 78 | 79 | let bind_group = driver.device.create_bind_group(&wgpu::BindGroupDescriptor { 80 | layout: &pipeline.get_bind_group_layout(0), 81 | entries: &bindings, 82 | label: None, 83 | }); 84 | 85 | Box::new(layout::Node:: { 86 | props: self.props.clone(), 87 | children: Default::default(), 88 | id: Rc::downgrade(&self.id), 89 | renderable: Some(Rc::new_cyclic(|this| render::domain::line::Pipeline { 90 | this: this.clone(), 91 | pipeline, 92 | group: bind_group, 93 | _buffers: buffers, 94 | domain: self.domain.clone(), 95 | start: self.start.clone(), 96 | end: self.end.clone(), 97 | })), 98 | }) 99 | } 100 | } 101 | 102 | crate::gen_component_wrap!(DomainLine, base::Empty); 103 | -------------------------------------------------------------------------------- /feather-ui/src/component/domain_point.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::SourceID; 5 | use crate::layout::domain_write; 6 | use derive_where::derive_where; 7 | use std::rc::Rc; 8 | 9 | // This simply writes it's area to the given cross-reference domain during the layout phase 10 | #[derive(feather_macro::StateMachineChild)] 11 | #[derive_where(Clone)] 12 | pub struct DomainPoint { 13 | pub id: Rc, 14 | props: Rc, 15 | } 16 | 17 | impl DomainPoint { 18 | pub fn new(id: Rc, props: T) -> Self { 19 | Self { 20 | id, 21 | props: props.into(), 22 | } 23 | } 24 | } 25 | 26 | impl super::Component for DomainPoint 27 | where 28 | for<'a> &'a T: Into<&'a (dyn domain_write::Prop + 'static)>, 29 | { 30 | fn layout( 31 | &self, 32 | _: &crate::StateManager, 33 | _: &crate::DriverState, 34 | _: &Rc, 35 | _: &wgpu::SurfaceConfiguration, 36 | ) -> Box> { 37 | Box::new(crate::layout::Node:: { 38 | props: self.props.clone(), 39 | children: Default::default(), 40 | id: Rc::downgrade(&self.id), 41 | renderable: None, 42 | }) 43 | } 44 | } 45 | 46 | crate::gen_component_wrap!(DomainPoint, domain_write::Prop); 47 | -------------------------------------------------------------------------------- /feather-ui/src/component/flexbox.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Desc, Layout, flex}; 5 | use crate::persist::{FnPersist, VectorMap}; 6 | use crate::{SourceID, layout}; 7 | use derive_where::derive_where; 8 | use std::rc::Rc; 9 | 10 | use super::ComponentFrom; 11 | 12 | #[derive(feather_macro::StateMachineChild)] 13 | #[derive_where(Clone)] 14 | pub struct FlexBox { 15 | pub id: Rc, 16 | pub props: Rc, 17 | pub children: im::Vector>>>, 18 | } 19 | 20 | impl super::Component for FlexBox { 21 | fn layout( 22 | &self, 23 | state: &crate::StateManager, 24 | driver: &crate::DriverState, 25 | window: &Rc, 26 | config: &wgpu::SurfaceConfiguration, 27 | ) -> Box> { 28 | let map = VectorMap::new( 29 | |child: &Option>>| -> Option::Child>>> { 30 | Some(child.as_ref().unwrap().layout(state, driver,window, config)) 31 | }, 32 | ); 33 | 34 | let (_, children) = map.call(Default::default(), &self.children); 35 | Box::new(layout::Node:: { 36 | props: self.props.clone(), 37 | children, 38 | id: Rc::downgrade(&self.id), 39 | renderable: None, 40 | }) 41 | } 42 | } 43 | 44 | crate::gen_component_wrap!(FlexBox, flex::Prop); 45 | -------------------------------------------------------------------------------- /feather-ui/src/component/gridbox.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Desc, Layout, grid}; 5 | use crate::persist::{FnPersist, VectorMap}; 6 | use crate::{SourceID, layout}; 7 | use derive_where::derive_where; 8 | use std::rc::Rc; 9 | 10 | use super::ComponentFrom; 11 | 12 | #[derive(feather_macro::StateMachineChild)] 13 | #[derive_where(Clone)] 14 | pub struct GridBox { 15 | pub id: Rc, 16 | pub props: Rc, 17 | pub children: im::Vector>>>, 18 | } 19 | 20 | impl super::Component for GridBox { 21 | fn layout( 22 | &self, 23 | state: &crate::StateManager, 24 | driver: &crate::DriverState, 25 | window: &Rc, 26 | config: &wgpu::SurfaceConfiguration, 27 | ) -> Box> { 28 | let map = VectorMap::new( 29 | |child: &Option>>| -> Option::Child>>> { 30 | Some(child.as_ref().unwrap().layout(state, driver,window, config)) 31 | }, 32 | ); 33 | 34 | let (_, children) = map.call(Default::default(), &self.children); 35 | Box::new(layout::Node:: { 36 | props: self.props.clone(), 37 | children, 38 | id: Rc::downgrade(&self.id), 39 | renderable: None, 40 | }) 41 | } 42 | } 43 | 44 | crate::gen_component_wrap!(GridBox, grid::Prop); 45 | -------------------------------------------------------------------------------- /feather-ui/src/component/line.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Layout, base}; 5 | use crate::shaders::gen_uniform; 6 | use crate::{DriverState, SourceID, layout}; 7 | use derive_where::derive_where; 8 | use std::rc::Rc; 9 | use ultraviolet::{Vec2, Vec4}; 10 | use wgpu::util::DeviceExt; 11 | 12 | // This draws a line between two points relative to the parent 13 | #[derive(feather_macro::StateMachineChild)] 14 | #[derive_where(Clone)] 15 | pub struct Line { 16 | pub id: Rc, 17 | pub start: Vec2, 18 | pub end: Vec2, 19 | pub props: Rc, 20 | pub fill: Vec4, 21 | } 22 | 23 | impl super::Component for Line 24 | where 25 | for<'a> &'a T: Into<&'a (dyn base::Empty + 'static)>, 26 | { 27 | fn layout( 28 | &self, 29 | _: &crate::StateManager, 30 | driver: &DriverState, 31 | _window: &Rc, 32 | config: &wgpu::SurfaceConfiguration, 33 | ) -> Box> { 34 | Box::new(layout::Node:: { 35 | props: self.props.clone(), 36 | children: Default::default(), 37 | id: Rc::downgrade(&self.id), 38 | renderable: Some(build_pipeline( 39 | driver, 40 | config, 41 | (self.start, self.end), 42 | self.fill, 43 | )), 44 | }) 45 | } 46 | } 47 | 48 | crate::gen_component_wrap!(Line, base::Empty); 49 | 50 | pub fn build_pipeline( 51 | driver: &crate::DriverState, 52 | config: &wgpu::SurfaceConfiguration, 53 | pos: (Vec2, Vec2), 54 | fill: Vec4, 55 | ) -> Rc { 56 | let shader_idx = driver.shader_cache.write().register_shader( 57 | &driver.device, 58 | "Line FS", 59 | include_str!("../shaders/Line.frag.wgsl"), 60 | ); 61 | let pipeline = driver 62 | .shader_cache 63 | .write() 64 | .line_pipeline(&driver.device, shader_idx, config); 65 | 66 | let mvp = gen_uniform( 67 | driver, 68 | "MVP", 69 | crate::shaders::mat4_proj( 70 | 0.0, 71 | config.height as f32, 72 | config.width as f32, 73 | -(config.height as f32), 74 | 0.2, 75 | 10000.0, 76 | ) 77 | .as_byte_slice(), 78 | ); 79 | 80 | let posdim = driver 81 | .device 82 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 83 | label: Some("PosDim"), 84 | contents: Vec4::new(0.0, 0.0, 1.0, 1.0).as_byte_slice(), 85 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 86 | }); 87 | 88 | let fill = gen_uniform(driver, "Fill", fill.as_byte_slice()); 89 | let buffers = [mvp, posdim, fill]; 90 | let bindings: Vec = buffers 91 | .iter() 92 | .enumerate() 93 | .map(|(i, x)| wgpu::BindGroupEntry { 94 | binding: i as u32, 95 | resource: x.as_entire_binding(), 96 | }) 97 | .collect(); 98 | 99 | let bind_group = driver.device.create_bind_group(&wgpu::BindGroupDescriptor { 100 | layout: &pipeline.get_bind_group_layout(0), 101 | entries: &bindings, 102 | label: None, 103 | }); 104 | 105 | Rc::new_cyclic(|this| crate::render::line::Pipeline { 106 | this: this.clone(), 107 | pipeline, 108 | group: bind_group, 109 | _buffers: buffers, 110 | pos: pos.into(), 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /feather-ui/src/component/listbox.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Desc, Layout, list}; 5 | use crate::persist::{FnPersist, VectorMap}; 6 | use crate::{SourceID, layout}; 7 | use derive_where::derive_where; 8 | use std::rc::Rc; 9 | 10 | use super::ComponentFrom; 11 | 12 | #[derive(feather_macro::StateMachineChild)] 13 | #[derive_where(Clone)] 14 | pub struct ListBox { 15 | pub id: Rc, 16 | pub props: Rc, 17 | pub children: im::Vector>>>, 18 | } 19 | 20 | impl super::Component for ListBox { 21 | fn layout( 22 | &self, 23 | state: &crate::StateManager, 24 | driver: &crate::DriverState, 25 | window: &Rc, 26 | config: &wgpu::SurfaceConfiguration, 27 | ) -> Box> { 28 | let map = VectorMap::new( 29 | |child: &Option>>| -> Option::Child>>> { 30 | Some(child.as_ref().unwrap().layout(state, driver,window, config)) 31 | }, 32 | ); 33 | 34 | let (_, children) = map.call(Default::default(), &self.children); 35 | Box::new(layout::Node:: { 36 | props: self.props.clone(), 37 | children, 38 | id: Rc::downgrade(&self.id), 39 | renderable: None, 40 | }) 41 | } 42 | } 43 | 44 | crate::gen_component_wrap!(ListBox, list::Prop); 45 | -------------------------------------------------------------------------------- /feather-ui/src/component/paragraph.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::component::ComponentFrom; 5 | use crate::component::text::Text; 6 | use crate::layout::{Desc, Layout, base, flex, leaf}; 7 | use crate::persist::{FnPersist, VectorMap}; 8 | use crate::{SourceID, UNSIZED_AXIS, gen_id, layout}; 9 | use core::f32; 10 | use derive_where::derive_where; 11 | use std::rc::Rc; 12 | 13 | #[derive(feather_macro::StateMachineChild)] 14 | #[derive_where(Clone)] 15 | pub struct Paragraph { 16 | pub id: Rc, 17 | pub props: Rc, 18 | pub children: im::Vector>>>, 19 | } 20 | 21 | struct MinimalFlexChild { 22 | grow: f32, 23 | } 24 | 25 | impl flex::Child for MinimalFlexChild { 26 | fn grow(&self) -> f32 { 27 | self.grow 28 | } 29 | 30 | fn shrink(&self) -> f32 { 31 | 0.0 32 | } 33 | 34 | fn basis(&self) -> crate::DValue { 35 | crate::DValue { 36 | dp: 0.0, 37 | px: 0.0, 38 | rel: UNSIZED_AXIS, 39 | } 40 | } 41 | } 42 | 43 | impl base::Area for MinimalFlexChild { 44 | fn area(&self) -> &crate::DRect { 45 | &crate::AUTO_DRECT 46 | } 47 | } 48 | 49 | impl base::Anchor for MinimalFlexChild {} 50 | impl base::Order for MinimalFlexChild {} 51 | impl base::Margin for MinimalFlexChild {} 52 | impl base::RLimits for MinimalFlexChild {} 53 | impl base::Limits for MinimalFlexChild {} 54 | impl base::Padding for MinimalFlexChild {} 55 | impl leaf::Prop for MinimalFlexChild {} 56 | impl leaf::Padded for MinimalFlexChild {} 57 | 58 | impl Paragraph { 59 | pub fn new(id: Rc, props: T) -> Self { 60 | Self { 61 | id, 62 | props: props.into(), 63 | children: im::Vector::new(), 64 | } 65 | } 66 | 67 | #[allow(clippy::too_many_arguments)] 68 | pub fn set_text( 69 | &mut self, 70 | text: &str, 71 | font_size: f32, 72 | line_height: f32, 73 | font: glyphon::FamilyOwned, 74 | color: glyphon::Color, 75 | weight: glyphon::Weight, 76 | style: glyphon::Style, 77 | fullwidth: bool, 78 | ) { 79 | self.children.clear(); 80 | for word in text.split_ascii_whitespace() { 81 | let text = Text:: { 82 | id: gen_id!().into(), 83 | props: MinimalFlexChild { 84 | grow: if fullwidth { 1.0 } else { 0.0 }, 85 | } 86 | .into(), 87 | text: word.to_owned() + " ", 88 | font_size, 89 | line_height, 90 | font: font.clone(), 91 | color, 92 | weight, 93 | style, 94 | wrap: glyphon::Wrap::None, 95 | }; 96 | self.children.push_back(Some(Box::new(text))); 97 | } 98 | } 99 | } 100 | 101 | impl super::Component for Paragraph { 102 | fn layout( 103 | &self, 104 | state: &crate::StateManager, 105 | driver: &crate::DriverState, 106 | window: &Rc, 107 | config: &wgpu::SurfaceConfiguration, 108 | ) -> Box> { 109 | let map = VectorMap::new( 110 | |child: &Option>>| -> Option::Child>>> { 111 | Some(child.as_ref().unwrap().layout(state, driver, window, config)) 112 | }, 113 | ); 114 | 115 | let (_, children) = map.call(Default::default(), &self.children); 116 | Box::new(layout::Node:: { 117 | props: self.props.clone(), 118 | children, 119 | id: Rc::downgrade(&self.id), 120 | renderable: None, 121 | }) 122 | } 123 | } 124 | 125 | crate::gen_component_wrap!(Paragraph, flex::Prop); 126 | -------------------------------------------------------------------------------- /feather-ui/src/component/region.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::component::ComponentFrom; 5 | use crate::layout::{Desc, Layout, fixed}; 6 | use crate::persist::{FnPersist, VectorMap}; 7 | use crate::{SourceID, layout}; 8 | use derive_where::derive_where; 9 | use std::rc::Rc; 10 | 11 | #[derive(feather_macro::StateMachineChild)] 12 | #[derive_where(Clone, Default)] 13 | pub struct Region { 14 | pub id: Rc, 15 | pub props: Rc, 16 | pub children: im::Vector>>>, 17 | } 18 | 19 | impl super::Component for Region 20 | where 21 | for<'a> &'a T: Into<&'a (dyn fixed::Prop + 'static)>, 22 | { 23 | fn layout( 24 | &self, 25 | state: &crate::StateManager, 26 | driver: &crate::DriverState, 27 | window: &Rc, 28 | config: &wgpu::SurfaceConfiguration, 29 | ) -> Box> { 30 | let map = VectorMap::new( 31 | |child: &Option>>| -> Option::Child>>> { 32 | Some(child.as_ref().unwrap().layout(state, driver, window, config)) 33 | }, 34 | ); 35 | 36 | let (_, children) = map.call(Default::default(), &self.children); 37 | Box::new(layout::Node:: { 38 | props: self.props.clone(), 39 | children, 40 | id: Rc::downgrade(&self.id), 41 | renderable: None, 42 | }) 43 | } 44 | } 45 | 46 | crate::gen_component_wrap!(Region, fixed::Prop, Default); 47 | -------------------------------------------------------------------------------- /feather-ui/src/component/shape.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{Layout, leaf}; 5 | use crate::shaders::gen_uniform; 6 | use crate::{BASE_DPI, DriverState, SourceID, WindowStateMachine, layout}; 7 | use derive_where::derive_where; 8 | use std::borrow::Cow; 9 | use std::rc::Rc; 10 | use ultraviolet::Vec4; 11 | use wgpu::util::DeviceExt; 12 | 13 | #[derive_where(Clone)] 14 | pub struct Shape<'a, T: leaf::Padded + 'static> { 15 | pub id: std::rc::Rc, 16 | pub props: Rc, 17 | pub uniforms: [Vec4; 4], 18 | pub fragment: Cow<'a, str>, 19 | pub label: &'static str, 20 | } 21 | 22 | impl Shape<'_, T> { 23 | pub fn round_rect( 24 | id: std::rc::Rc, 25 | props: Rc, 26 | border: f32, 27 | blur: f32, 28 | corners: Vec4, 29 | fill: Vec4, 30 | outline: Vec4, 31 | ) -> Self { 32 | Self { 33 | id, 34 | props, 35 | uniforms: [Vec4::new(0.0, 0.0, border, blur), corners, fill, outline], 36 | fragment: Cow::Borrowed(include_str!("../shaders/RoundRect.wgsl")), 37 | label: "RoundRect FS", 38 | } 39 | } 40 | 41 | pub fn arc( 42 | id: std::rc::Rc, 43 | props: Rc, 44 | border: f32, 45 | blur: f32, 46 | arcs: Vec4, 47 | fill: Vec4, 48 | outline: Vec4, 49 | ) -> Self { 50 | Self { 51 | id, 52 | props, 53 | uniforms: [Vec4::new(0.0, 0.0, border, blur), arcs, fill, outline], 54 | fragment: Cow::Borrowed(include_str!("../shaders/Arc.wgsl")), 55 | label: "Arc FS", 56 | } 57 | } 58 | 59 | pub fn circle( 60 | id: std::rc::Rc, 61 | props: Rc, 62 | border: f32, 63 | blur: f32, 64 | radii: crate::Vec2, 65 | fill: Vec4, 66 | outline: Vec4, 67 | ) -> Self { 68 | Self { 69 | id, 70 | props, 71 | uniforms: [ 72 | Vec4::new(0.0, 0.0, border, blur), 73 | Vec4::new(radii.x, radii.y, 0.0, 0.0), 74 | fill, 75 | outline, 76 | ], 77 | fragment: Cow::Borrowed(include_str!("../shaders/Circle.wgsl")), 78 | label: "Circle FS", 79 | } 80 | } 81 | } 82 | 83 | impl crate::StateMachineChild for Shape<'_, T> { 84 | fn id(&self) -> std::rc::Rc { 85 | self.id.clone() 86 | } 87 | } 88 | 89 | impl super::Component for Shape<'_, T> 90 | where 91 | for<'a> &'a T: Into<&'a (dyn leaf::Padded + 'static)>, 92 | { 93 | fn layout( 94 | &self, 95 | state: &crate::StateManager, 96 | driver: &DriverState, 97 | window: &Rc, 98 | config: &wgpu::SurfaceConfiguration, 99 | ) -> Box> { 100 | let winstate: &WindowStateMachine = state.get(window).unwrap(); 101 | let dpi = winstate.state.as_ref().map(|x| x.dpi).unwrap_or(BASE_DPI); 102 | 103 | let shader_idx = 104 | driver 105 | .shader_cache 106 | .write() 107 | .register_shader(&driver.device, self.label, &self.fragment); 108 | let pipeline = 109 | driver 110 | .shader_cache 111 | .write() 112 | .standard_pipeline(&driver.device, shader_idx, config); 113 | 114 | let mvp = gen_uniform( 115 | driver, 116 | "MVP", 117 | crate::shaders::mat4_proj( 118 | 0.0, 119 | config.height as f32, 120 | config.width as f32, 121 | -(config.height as f32), 122 | 0.2, 123 | 10000.0, 124 | ) 125 | .as_byte_slice(), 126 | ); 127 | 128 | let posdim = driver 129 | .device 130 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 131 | label: Some("PosDim"), 132 | contents: Vec4::new(0.0, 0.0, 400.0, 200.0).as_byte_slice(), 133 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 134 | }); 135 | 136 | let dimborderblur = gen_uniform( 137 | driver, 138 | "DimBorderBlur", 139 | (self.uniforms[0] * Vec4::broadcast(dpi.x)).as_byte_slice(), 140 | ); 141 | let corners = gen_uniform( 142 | driver, 143 | "Corners", 144 | (self.uniforms[1] * Vec4::broadcast(dpi.x)).as_byte_slice(), 145 | ); 146 | let fill = gen_uniform(driver, "Fill", self.uniforms[2].as_byte_slice()); 147 | let outline = gen_uniform(driver, "Component", self.uniforms[3].as_byte_slice()); 148 | let buffers = [mvp, posdim, dimborderblur, corners, fill, outline]; 149 | let bindings: Vec = buffers 150 | .iter() 151 | .enumerate() 152 | .map(|(i, x)| wgpu::BindGroupEntry { 153 | binding: i as u32, 154 | resource: x.as_entire_binding(), 155 | }) 156 | .collect(); 157 | 158 | let bind_group = driver.device.create_bind_group(&wgpu::BindGroupDescriptor { 159 | layout: &pipeline.get_bind_group_layout(0), 160 | entries: &bindings, 161 | label: None, 162 | }); 163 | 164 | Box::new(layout::Node:: { 165 | props: self.props.clone(), 166 | children: Default::default(), 167 | id: Rc::downgrade(&self.id), 168 | renderable: Some(Rc::new_cyclic(|this| crate::render::standard::Pipeline { 169 | this: this.clone(), 170 | pipeline, 171 | group: bind_group, 172 | vertices: crate::shaders::default_vertex_buffer(driver), 173 | padding: self.props.padding().resolve(dpi), 174 | buffers, 175 | })), 176 | }) 177 | } 178 | } 179 | 180 | crate::gen_component_wrap!('a, Shape, leaf::Padded); 181 | -------------------------------------------------------------------------------- /feather-ui/src/component/text.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::layout::{self, Layout, leaf}; 5 | use crate::{DriverState, SourceID, WindowStateMachine, point_to_pixel}; 6 | use derive_where::derive_where; 7 | use std::cell::RefCell; 8 | use std::rc::Rc; 9 | 10 | #[derive(feather_macro::StateMachineChild)] 11 | #[derive_where(Clone)] 12 | pub struct Text { 13 | pub id: Rc, 14 | pub props: Rc, 15 | pub font_size: f32, 16 | pub line_height: f32, 17 | pub text: String, 18 | pub font: glyphon::FamilyOwned, 19 | pub color: glyphon::Color, 20 | pub weight: glyphon::Weight, 21 | pub style: glyphon::Style, 22 | pub wrap: glyphon::Wrap, 23 | } 24 | 25 | impl Default for Text { 26 | fn default() -> Self { 27 | Self { 28 | id: Default::default(), 29 | props: Default::default(), 30 | font_size: Default::default(), 31 | line_height: Default::default(), 32 | text: Default::default(), 33 | font: glyphon::FamilyOwned::SansSerif, 34 | color: glyphon::Color::rgba(255, 255, 255, 255), 35 | weight: Default::default(), 36 | style: Default::default(), 37 | wrap: glyphon::Wrap::None, 38 | } 39 | } 40 | } 41 | 42 | impl super::Component for Text 43 | where 44 | for<'a> &'a T: Into<&'a (dyn leaf::Padded + 'static)>, 45 | { 46 | fn layout( 47 | &self, 48 | state: &crate::StateManager, 49 | driver: &DriverState, 50 | window: &Rc, 51 | _: &wgpu::SurfaceConfiguration, 52 | ) -> Box> { 53 | let winstate: &WindowStateMachine = state.get(window).unwrap(); 54 | let winstate = winstate.state.as_ref().expect("No window state available"); 55 | let dpi = winstate.dpi; 56 | let mut font_system = driver.font_system.write(); 57 | let mut text_buffer = glyphon::Buffer::new( 58 | &mut font_system, 59 | glyphon::Metrics::new( 60 | point_to_pixel(self.font_size, dpi.x), 61 | point_to_pixel(self.line_height, dpi.x), 62 | ), 63 | ); 64 | 65 | text_buffer.set_wrap(&mut font_system, self.wrap); 66 | text_buffer.set_text( 67 | &mut font_system, 68 | &self.text, 69 | &glyphon::Attrs::new() 70 | .family(self.font.as_family()) 71 | .color(self.color) 72 | .weight(self.weight) 73 | .style(self.style), 74 | glyphon::Shaping::Advanced, 75 | ); 76 | 77 | let renderer = glyphon::TextRenderer::new( 78 | &mut winstate.atlas.borrow_mut(), 79 | &driver.device, 80 | wgpu::MultisampleState::default(), 81 | None, 82 | ); 83 | 84 | let render = Rc::new_cyclic(|this| crate::render::text::Pipeline { 85 | this: this.clone(), 86 | text_buffer: Rc::new(RefCell::new(Some(text_buffer))), 87 | renderer: renderer.into(), 88 | padding: self.props.padding().resolve(dpi).into(), 89 | atlas: winstate.atlas.clone(), 90 | viewport: winstate.viewport.clone(), 91 | }); 92 | Box::new(layout::text::Node:: { 93 | props: self.props.clone(), 94 | id: Rc::downgrade(&self.id), 95 | text_render: render.clone(), 96 | renderable: render.clone(), 97 | }) 98 | } 99 | } 100 | 101 | crate::gen_component_wrap!(Text, leaf::Padded); 102 | -------------------------------------------------------------------------------- /feather-ui/src/draw.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | -------------------------------------------------------------------------------- /feather-ui/src/input.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use enum_variant_type::EnumVariantType; 5 | use feather_macro::Dispatch; 6 | use ultraviolet::{Vec2, Vec3}; 7 | use winit::dpi::PhysicalPosition; 8 | 9 | use crate::DriverState; 10 | 11 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 12 | #[repr(u8)] 13 | pub enum TouchState { 14 | Start = 0, 15 | Move = 1, 16 | End = 2, 17 | } 18 | 19 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 20 | #[repr(u8)] 21 | pub enum MouseState { 22 | Down = 0, 23 | Up = 1, 24 | DblClick = 2, 25 | } 26 | 27 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 28 | #[repr(u16)] 29 | pub enum MouseButton { 30 | Left = (1 << 0), 31 | Right = (1 << 1), 32 | Middle = (1 << 2), 33 | Back = (1 << 3), 34 | Forward = (1 << 4), 35 | X1 = (1 << 5), 36 | X2 = (1 << 6), 37 | X3 = (1 << 7), 38 | X4 = (1 << 8), 39 | X5 = (1 << 9), 40 | X6 = (1 << 10), 41 | X7 = (1 << 11), 42 | X8 = (1 << 12), 43 | X9 = (1 << 13), 44 | X10 = (1 << 14), 45 | X11 = (1 << 15), 46 | } 47 | 48 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 49 | #[repr(u8)] 50 | pub enum ModifierKeys { 51 | Shift = 1, 52 | Control = 2, 53 | Alt = 4, 54 | Super = 8, 55 | Capslock = 16, 56 | Numlock = 32, 57 | Held = 64, 58 | } 59 | 60 | #[derive(Debug, Dispatch, EnumVariantType, Clone)] 61 | #[evt(derive(Clone), module = "raw_event")] 62 | pub enum RawEvent { 63 | Drag, // TBD, must be included here so RawEvent matches RawEventKind 64 | Drop { 65 | device_id: winit::event::DeviceId, 66 | pos: PhysicalPosition, 67 | }, 68 | Focus { 69 | acquired: bool, 70 | window: std::sync::Arc, // Allows setting IME mode for textboxes 71 | }, 72 | JoyAxis { 73 | device_id: winit::event::DeviceId, 74 | value: f64, 75 | axis: u32, 76 | }, 77 | JoyButton { 78 | device_id: winit::event::DeviceId, 79 | down: bool, 80 | button: u32, 81 | }, 82 | JoyOrientation { 83 | // 32 bytes 84 | device_id: winit::event::DeviceId, 85 | velocity: Vec3, 86 | rotation: Vec3, 87 | }, 88 | Key { 89 | // 48 bytes 90 | device_id: winit::event::DeviceId, 91 | physical_key: winit::keyboard::PhysicalKey, 92 | location: winit::keyboard::KeyLocation, 93 | down: bool, 94 | logical_key: winit::keyboard::Key, 95 | modifiers: u8, 96 | }, 97 | Mouse { 98 | // 24 bytes 99 | device_id: winit::event::DeviceId, 100 | state: MouseState, 101 | pos: PhysicalPosition, 102 | button: MouseButton, 103 | all_buttons: u16, 104 | modifiers: u8, 105 | }, 106 | MouseOn { 107 | device_id: winit::event::DeviceId, 108 | pos: PhysicalPosition, 109 | modifiers: u8, 110 | all_buttons: u16, 111 | driver: std::sync::Weak, // Allows setting our global cursor tracker 112 | }, 113 | MouseMove { 114 | device_id: winit::event::DeviceId, 115 | pos: PhysicalPosition, 116 | modifiers: u8, 117 | all_buttons: u16, 118 | driver: std::sync::Weak, // Allows setting our global cursor tracker 119 | }, 120 | MouseOff { 121 | device_id: winit::event::DeviceId, 122 | modifiers: u8, 123 | all_buttons: u16, 124 | driver: std::sync::Weak, // Allows setting our global cursor tracker 125 | }, 126 | MouseScroll { 127 | device_id: winit::event::DeviceId, 128 | state: TouchState, 129 | pos: PhysicalPosition, 130 | delta: Vec2, 131 | pixels: bool, // If true, delta is expressed in pixels 132 | }, 133 | Touch { 134 | // 48 bytes 135 | device_id: winit::event::DeviceId, 136 | index: u64, 137 | state: TouchState, 138 | pos: Vec3, 139 | angle: Vec2, 140 | pressure: f64, 141 | }, 142 | } 143 | 144 | static_assertions::const_assert!(size_of::() == 48); 145 | 146 | impl RawEvent { 147 | pub fn kind(&self) -> RawEventKind { 148 | self.into() 149 | } 150 | } 151 | 152 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 153 | #[repr(u64)] 154 | pub enum RawEventKind { 155 | Drag = (1 << 0), // This must start from 1 and perfectly match RawEvent to ensure the dispatch works correctly 156 | Drop = (1 << 1), 157 | Focus = (1 << 2), 158 | JoyAxis = (1 << 3), 159 | JoyButton = (1 << 4), 160 | JoyOrientation = (1 << 5), 161 | Key = (1 << 6), 162 | Mouse = (1 << 7), 163 | MouseOn = (1 << 8), 164 | MouseMove = (1 << 9), 165 | MouseOff = (1 << 10), 166 | MouseScroll = (1 << 11), 167 | Touch = (1 << 12), 168 | } 169 | 170 | impl From<&RawEvent> for RawEventKind { 171 | fn from(value: &RawEvent) -> Self { 172 | match value { 173 | RawEvent::Drag => RawEventKind::Drag, 174 | RawEvent::Drop { .. } => RawEventKind::Drop, 175 | RawEvent::Focus { .. } => RawEventKind::Focus, 176 | RawEvent::JoyAxis { .. } => RawEventKind::JoyAxis, 177 | RawEvent::JoyButton { .. } => RawEventKind::JoyButton, 178 | RawEvent::JoyOrientation { .. } => RawEventKind::JoyOrientation, 179 | RawEvent::Key { .. } => RawEventKind::Key, 180 | RawEvent::Mouse { .. } => RawEventKind::Mouse, 181 | RawEvent::MouseOn { .. } => RawEventKind::MouseOn, 182 | RawEvent::MouseMove { .. } => RawEventKind::MouseMove, 183 | RawEvent::MouseOff { .. } => RawEventKind::MouseOff, 184 | RawEvent::MouseScroll { .. } => RawEventKind::MouseScroll, 185 | RawEvent::Touch { .. } => RawEventKind::Touch, 186 | } 187 | } 188 | } 189 | 190 | impl From for TouchState { 191 | fn from(value: winit::event::TouchPhase) -> Self { 192 | match value { 193 | winit::event::TouchPhase::Started => TouchState::Start, 194 | winit::event::TouchPhase::Moved => TouchState::Move, 195 | winit::event::TouchPhase::Ended => TouchState::End, 196 | winit::event::TouchPhase::Cancelled => TouchState::End, 197 | } 198 | } 199 | } 200 | 201 | impl From for MouseButton { 202 | fn from(value: winit::event::MouseButton) -> Self { 203 | match value { 204 | winit::event::MouseButton::Left => MouseButton::Left, 205 | winit::event::MouseButton::Right => MouseButton::Right, 206 | winit::event::MouseButton::Middle => MouseButton::Middle, 207 | winit::event::MouseButton::Back => MouseButton::Back, 208 | winit::event::MouseButton::Forward => MouseButton::Forward, 209 | winit::event::MouseButton::Other(5) => MouseButton::X1, 210 | winit::event::MouseButton::Other(6) => MouseButton::X2, 211 | winit::event::MouseButton::Other(7) => MouseButton::X3, 212 | winit::event::MouseButton::Other(8) => MouseButton::X4, 213 | winit::event::MouseButton::Other(9) => MouseButton::X5, 214 | winit::event::MouseButton::Other(10) => MouseButton::X6, 215 | winit::event::MouseButton::Other(11) => MouseButton::X7, 216 | winit::event::MouseButton::Other(12) => MouseButton::X8, 217 | winit::event::MouseButton::Other(13) => MouseButton::X9, 218 | winit::event::MouseButton::Other(14) => MouseButton::X10, 219 | winit::event::MouseButton::Other(15) => MouseButton::X11, 220 | winit::event::MouseButton::Other(_) => panic!("Mouse button out of range"), 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /feather-ui/src/layout/base.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::{AbsRect, DAbsRect, DPoint, DRect, ZERO_DRECT}; 5 | use std::rc::Rc; 6 | 7 | #[macro_export] 8 | macro_rules! gen_from_to_dyn { 9 | ($idx:ident) => { 10 | impl<'a, T: $idx + 'static> From<&'a T> for &'a (dyn $idx + 'static) { 11 | fn from(value: &'a T) -> Self { 12 | return value; 13 | } 14 | } 15 | }; 16 | } 17 | 18 | pub trait Empty {} 19 | 20 | impl Empty for () {} 21 | impl RLimits for () {} 22 | impl Margin for () {} 23 | impl Order for () {} 24 | impl crate::layout::fixed::Child for () {} 25 | impl crate::layout::list::Child for () {} 26 | 27 | impl Empty for Rc {} 28 | 29 | impl Empty for DRect {} 30 | 31 | gen_from_to_dyn!(Empty); 32 | 33 | impl crate::layout::Desc for dyn Empty { 34 | type Props = dyn Empty; 35 | type Child = dyn Empty; 36 | type Children = (); 37 | 38 | fn stage<'a>( 39 | _: &Self::Props, 40 | mut outer_area: AbsRect, 41 | outer_limits: crate::AbsLimits, 42 | _: &Self::Children, 43 | id: std::rc::Weak, 44 | renderable: Option>, 45 | window: &mut crate::component::window::WindowState, 46 | ) -> Box { 47 | outer_area = super::nuetralize_unsized(outer_area); 48 | outer_area = super::limit_area(outer_area, outer_limits); 49 | 50 | Box::new(crate::layout::Concrete::new( 51 | renderable, 52 | outer_area, 53 | crate::rtree::Node::new(outer_area, None, Default::default(), id, window), 54 | Default::default(), 55 | )) 56 | } 57 | } 58 | 59 | //static SENTINEL: std::sync::LazyLock> = 60 | // std::sync::LazyLock::new(|| std::rc::Rc::new(())); 61 | 62 | pub trait Obstacles { 63 | fn obstacles(&self) -> &[DAbsRect]; 64 | } 65 | 66 | pub trait ZIndex { 67 | fn zindex(&self) -> i32 { 68 | 0 69 | } 70 | } 71 | 72 | // Padding is used so an element's actual area can be larger than the area it draws children inside (like text). 73 | pub trait Padding { 74 | fn padding(&self) -> &DAbsRect { 75 | &crate::ZERO_DABSRECT 76 | } 77 | } 78 | 79 | impl Padding for DRect {} 80 | 81 | // Relative to parent's area, but only ever used to determine spacing between child elements. 82 | pub trait Margin { 83 | fn margin(&self) -> &DRect { 84 | &ZERO_DRECT 85 | } 86 | } 87 | 88 | // Relative to child's assigned area (outer area) 89 | pub trait Area { 90 | fn area(&self) -> &DRect; 91 | } 92 | 93 | impl Area for DRect { 94 | fn area(&self) -> &DRect { 95 | self 96 | } 97 | } 98 | 99 | gen_from_to_dyn!(Area); 100 | 101 | // Relative to child's evaluated area (inner area) 102 | pub trait Anchor { 103 | fn anchor(&self) -> &DPoint { 104 | &crate::ZERO_DPOINT 105 | } 106 | } 107 | 108 | impl Anchor for DRect {} 109 | 110 | pub trait Limits { 111 | fn limits(&self) -> &crate::DLimits { 112 | &crate::DEFAULT_DLIMITS 113 | } 114 | } 115 | 116 | // Relative to parent's area 117 | pub trait RLimits { 118 | fn rlimits(&self) -> &crate::RelLimits { 119 | &crate::DEFAULT_RLIMITS 120 | } 121 | } 122 | 123 | pub trait Order { 124 | fn order(&self) -> i64 { 125 | 0 126 | } 127 | } 128 | 129 | pub trait Direction { 130 | fn direction(&self) -> crate::RowDirection { 131 | crate::RowDirection::LeftToRight 132 | } 133 | } 134 | 135 | impl Limits for DRect {} 136 | impl RLimits for DRect {} 137 | 138 | pub trait TextEdit { 139 | fn textedit(&self) -> &crate::text::Snapshot; 140 | } 141 | -------------------------------------------------------------------------------- /feather-ui/src/layout/domain_write.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::base::{Empty, RLimits}; 5 | use super::{Concrete, Desc, Layout, Renderable, Staged}; 6 | use crate::{AbsRect, CrossReferenceDomain, SourceID, render, rtree}; 7 | use std::marker::PhantomData; 8 | use std::rc::Rc; 9 | 10 | // A DomainWrite layout spawns a renderable that writes it's area to the target cross-reference domain 11 | pub trait Prop { 12 | fn domain(&self) -> Rc; 13 | } 14 | 15 | crate::gen_from_to_dyn!(Prop); 16 | 17 | impl Prop for Rc { 18 | fn domain(&self) -> Rc { 19 | self.clone() 20 | } 21 | } 22 | 23 | impl Empty for Rc {} 24 | impl RLimits for Rc {} 25 | impl super::fixed::Child for Rc {} 26 | 27 | impl Desc for dyn Prop { 28 | type Props = dyn Prop; 29 | type Child = dyn Empty; 30 | type Children = PhantomData>; 31 | 32 | fn stage<'a>( 33 | props: &Self::Props, 34 | mut outer_area: AbsRect, 35 | outer_limits: crate::AbsLimits, 36 | _: &Self::Children, 37 | id: std::rc::Weak, 38 | renderable: Option>, 39 | window: &mut crate::component::window::WindowState, 40 | ) -> Box { 41 | outer_area = super::nuetralize_unsized(outer_area); 42 | outer_area = super::limit_area(outer_area, outer_limits); 43 | 44 | Box::new(Concrete { 45 | area: outer_area, 46 | render: Some(Rc::new(render::domain::Write { 47 | id: id.clone(), 48 | domain: props.domain().clone(), 49 | base: renderable, 50 | })), 51 | rtree: rtree::Node::new(outer_area, None, Default::default(), id, window), 52 | children: Default::default(), 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /feather-ui/src/layout/fixed.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::{ 5 | Concrete, Desc, Layout, Renderable, Staged, base, check_unsized, check_unsized_abs, 6 | map_unsized_area, 7 | }; 8 | use crate::{AbsRect, ZERO_POINT, rtree}; 9 | use std::rc::Rc; 10 | 11 | pub trait Prop: base::Area + base::Anchor + base::Limits + base::ZIndex {} 12 | 13 | crate::gen_from_to_dyn!(Prop); 14 | 15 | pub trait Child: base::RLimits {} 16 | 17 | crate::gen_from_to_dyn!(Child); 18 | 19 | impl Child for crate::DRect {} 20 | 21 | impl Desc for dyn Prop { 22 | type Props = dyn Prop; 23 | type Child = dyn Child; 24 | type Children = im::Vector>>>; 25 | 26 | fn stage<'a>( 27 | props: &Self::Props, 28 | outer_area: AbsRect, 29 | outer_limits: crate::AbsLimits, 30 | children: &Self::Children, 31 | id: std::rc::Weak, 32 | renderable: Option>, 33 | window: &mut crate::component::window::WindowState, 34 | ) -> Box { 35 | // If we have an unsized outer_area, any sized object with relative dimensions must evaluate to 0 (or to the minimum limited size). An 36 | // unsized object can never have relative dimensions, as that creates a logic loop - instead it can only have a single relative anchor. 37 | // If both axes are sized, then all limits are applied as if outer_area was unsized, and children calculations are skipped. 38 | // 39 | // If we have an unsized outer_area and an unsized myarea.rel, then limits are applied as if outer_area was unsized, and furthermore, 40 | // they are reduced by myarea.abs.bottomright(), because that will be added on to the total area later, which will still be subject to size 41 | // limits, so we must anticipate this when calculating how much size the children will have available to them. This forces limits to be 42 | // true infinite numbers, so we can subtract finite amounts and still have infinity. We can't use infinity anywhere else, because infinity 43 | // times zero is NaN, so we cap certain calculations at f32::MAX 44 | // 45 | // If outer_area is sized and myarea.rel is zero or nonzero, all limits are applied normally and child calculations are skipped. 46 | // If outer_area is sized and myarea.rel is unsized, limits are applied normally, but are once again reduced by myarea.abs.bottomright() to 47 | // account for how the area calculations will interact with the limits later on. 48 | 49 | let limits = outer_limits + props.limits().resolve(window.dpi); 50 | let myarea = props.area().resolve(window.dpi); 51 | let (unsized_x, unsized_y) = check_unsized(myarea); 52 | 53 | // Check if any axis is unsized in a way that requires us to calculate baseline child sizes 54 | let evaluated_area = if unsized_x || unsized_y { 55 | // When an axis is unsized, we don't apply any limits to it, so we don't have to worry about 56 | // cases where the full evaluated area would invalidate the limit. 57 | let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits); 58 | let inner_area = AbsRect::from(inner_dim); 59 | // The area we pass to children must be independent of our own area, so it starts at 0,0 60 | let mut bottomright = ZERO_POINT; 61 | 62 | for child in children.iter() { 63 | let child_props = child.as_ref().unwrap().get_props(); 64 | let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits()); 65 | 66 | let stage = child 67 | .as_ref() 68 | .unwrap() 69 | .stage(inner_area, child_limit, window); 70 | bottomright = bottomright.max_by_component(stage.get_area().bottomright()); 71 | } 72 | 73 | let area = map_unsized_area(myarea, bottomright); 74 | 75 | // No need to cap this because unsized axis have now been resolved 76 | super::limit_area(area * crate::layout::nuetralize_unsized(outer_area), limits) 77 | } else { 78 | // If outer_area is unsized here, we nuetralize it when evaluating the relative coordinates. 79 | super::limit_area( 80 | myarea * crate::layout::nuetralize_unsized(outer_area), 81 | limits, 82 | ) 83 | }; 84 | 85 | let mut staging: im::Vector>> = im::Vector::new(); 86 | let mut nodes: im::Vector>> = im::Vector::new(); 87 | 88 | // If our parent just wants a size estimate, no need to layout children or render anything 89 | let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright()); 90 | if unsized_x || unsized_y { 91 | return Box::new(Concrete::new( 92 | None, 93 | evaluated_area, 94 | rtree::Node::new(evaluated_area, Some(props.zindex()), nodes, id, window), 95 | staging, 96 | )); 97 | } 98 | 99 | // We had to evaluate the full area first because our final area calculation can change the dimensions in 100 | // unsized cases. Thus, we calculate the final inner_area for the children from this evaluated area. 101 | let evaluated_dim = evaluated_area.dim(); 102 | 103 | let inner_area = AbsRect::from(evaluated_dim); 104 | 105 | for child in children.iter() { 106 | let child_props = child.as_ref().unwrap().get_props(); 107 | let child_limit = *child_props.rlimits() * evaluated_dim; 108 | 109 | let stage = child 110 | .as_ref() 111 | .unwrap() 112 | .stage(inner_area, child_limit, window); 113 | if let Some(node) = stage.get_rtree().upgrade() { 114 | nodes.push_back(Some(node)); 115 | } 116 | staging.push_back(Some(stage)); 117 | } 118 | 119 | // TODO: It isn't clear if the simple layout should attempt to handle children changing their estimated 120 | // sizes after the initial estimate. If we were to handle this, we would need to recalculate the unsized 121 | // axis with the new child results here, and repeat until it stops changing (we find the fixed point). 122 | // Because the performance implications are unclear, this might need to be relagated to a special layout. 123 | 124 | // Calculate the anchor using the final evaluated dimensions, after all unsized axis and limits are 125 | // calculated. However, we can only apply the anchor if the parent isn't unsized on that axis. 126 | let mut anchor = props.anchor().resolve(window.dpi) * evaluated_dim; 127 | let (unsized_outer_x, unsized_outer_y) = 128 | crate::layout::check_unsized_abs(outer_area.bottomright()); 129 | if unsized_outer_x { 130 | anchor.x = 0.0; 131 | } 132 | if unsized_outer_y { 133 | anchor.y = 0.0; 134 | } 135 | let evaluated_area = evaluated_area - anchor; 136 | 137 | Box::new(Concrete::new( 138 | renderable, 139 | evaluated_area, 140 | rtree::Node::new(evaluated_area, Some(props.zindex()), nodes, id, window), 141 | staging, 142 | )) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /feather-ui/src/layout/grid.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use ultraviolet::Vec2; 5 | 6 | use super::{ 7 | Concrete, Desc, Layout, Renderable, Staged, base, check_unsized, map_unsized_area, 8 | nuetralize_unsized, swap_axis, 9 | }; 10 | use crate::{ 11 | AbsRect, DPoint, DValue, MINUS_BOTTOMRIGHT, RowDirection, SourceID, UNSIZED_AXIS, rtree, 12 | }; 13 | use std::rc::Rc; 14 | 15 | // TODO: use sparse vectors here? Does that even make sense if rows require a default size of some kind? 16 | pub trait Prop: base::Area + base::Limits + base::Anchor + base::Padding { 17 | fn rows(&self) -> &[DValue]; 18 | fn columns(&self) -> &[DValue]; 19 | fn spacing(&self) -> DPoint; // Spacing is specified as (row, column) 20 | fn direction(&self) -> RowDirection; // Note that a "normal" grid is TopToBottom here by default, not LeftToRight 21 | } 22 | 23 | crate::gen_from_to_dyn!(Prop); 24 | 25 | pub trait Child: base::RLimits { 26 | /// (Row, Column) index of the item 27 | fn index(&self) -> (usize, usize); 28 | /// (Row, Column) span of the item, lets items span across multiple rows or columns. 29 | /// Minimum is (1,1), and the layout won't save you if you tell it to overlap items. 30 | fn span(&self) -> (usize, usize); 31 | } 32 | 33 | crate::gen_from_to_dyn!(Child); 34 | 35 | impl Desc for dyn Prop { 36 | type Props = dyn Prop; 37 | type Child = dyn Child; 38 | type Children = im::Vector>>>; 39 | 40 | fn stage<'a>( 41 | props: &Self::Props, 42 | outer_area: AbsRect, 43 | outer_limits: crate::AbsLimits, 44 | children: &Self::Children, 45 | id: std::rc::Weak, 46 | renderable: Option>, 47 | window: &mut crate::component::window::WindowState, 48 | ) -> Box { 49 | let mut limits = outer_limits + props.limits().resolve(window.dpi); 50 | let padding = props.padding().resolve(window.dpi); 51 | let myarea = props.area().resolve(window.dpi); 52 | let (unsized_x, unsized_y) = check_unsized(myarea); 53 | let allpadding = padding.topleft() + padding.bottomright(); 54 | let minmax = limits.0.as_array_mut(); 55 | if unsized_x { 56 | minmax[2] -= allpadding.x; 57 | minmax[0] -= allpadding.x; 58 | } 59 | if unsized_y { 60 | minmax[3] -= allpadding.y; 61 | minmax[1] -= allpadding.y; 62 | } 63 | 64 | let outer_safe = nuetralize_unsized(outer_area); 65 | let inner_dim = crate::AbsDim( 66 | super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits).0 67 | - padding.topleft() 68 | - padding.bottomright(), 69 | ); 70 | 71 | let yaxis = match props.direction() { 72 | RowDirection::LeftToRight | RowDirection::RightToLeft => false, 73 | RowDirection::TopToBottom | RowDirection::BottomToTop => true, 74 | }; 75 | 76 | let (outer_column, outer_row) = swap_axis(yaxis, outer_safe.dim().0); 77 | let (dpi_column, dpi_row) = swap_axis(yaxis, window.dpi); 78 | 79 | let spacing = props.spacing().resolve(Vec2::new(dpi_row, dpi_column)) 80 | * crate::AbsDim(Vec2::new(outer_row, outer_column)); 81 | let nrows = props.rows().len(); 82 | let ncolumns = props.columns().len(); 83 | 84 | let mut staging: im::Vector>> = im::Vector::new(); 85 | let mut nodes: im::Vector>> = im::Vector::new(); 86 | 87 | let evaluated_area = crate::alloca_array::((nrows + ncolumns) * 2, |x| { 88 | let (resolved, sizes) = x.split_at_mut(nrows + ncolumns); 89 | { 90 | let (rows, columns) = resolved.split_at_mut(nrows); 91 | 92 | // Fill our max calculation rows with NANs (this ensures max()/min() behave properly) 93 | sizes.fill(f32::NAN); 94 | 95 | let (maxrows, maxcolumns) = sizes.split_at_mut(nrows); 96 | 97 | // First we precalculate all row/column sizes that we can (if an outer axis is unsized, relative sizes are set to 0) 98 | for (i, row) in props.rows().iter().enumerate() { 99 | rows[i] = row.resolve(dpi_row).resolve(outer_row); 100 | } 101 | for (i, column) in props.columns().iter().enumerate() { 102 | columns[i] = column.resolve(dpi_column).resolve(outer_column); 103 | } 104 | 105 | // Then we go through all child elements so we can precalculate the maximum area of all rows and columns 106 | for child in children.iter() { 107 | let child_props = child.as_ref().unwrap().get_props(); 108 | let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits()); 109 | let (row, column) = child_props.index(); 110 | 111 | if rows[row] == UNSIZED_AXIS || columns[column] == UNSIZED_AXIS { 112 | let (w, h) = swap_axis(yaxis, Vec2::new(columns[column], rows[row])); 113 | let child_area = AbsRect::new(0.0, 0.0, w, h); 114 | 115 | let stage = child 116 | .as_ref() 117 | .unwrap() 118 | .stage(child_area, child_limit, window); 119 | let area = stage.get_area(); 120 | let (c, r) = swap_axis(yaxis, area.dim().0); 121 | maxrows[row] = maxrows[row].max(r); 122 | maxcolumns[column] = maxcolumns[column].max(c); 123 | } 124 | } 125 | } 126 | 127 | // Copy back our resolved row or column to any unsized ones 128 | for (i, size) in sizes.iter().enumerate() { 129 | if resolved[i] == UNSIZED_AXIS { 130 | resolved[i] = if size.is_nan() { 0.0 } else { *size }; 131 | } 132 | } 133 | let (rows, columns) = resolved.split_at_mut(nrows); 134 | let (x_used, y_used) = swap_axis( 135 | yaxis, 136 | Vec2::new( 137 | columns.iter().fold(0.0, |x, y| x + y) 138 | + (spacing.y * ncolumns.saturating_sub(1) as f32), 139 | rows.iter().fold(0.0, |x, y| x + y) 140 | + (spacing.x * nrows.saturating_sub(1) as f32), 141 | ), 142 | ); 143 | let area = map_unsized_area(myarea, Vec2::new(x_used, y_used)); 144 | 145 | // Calculate the offset to each row or column, without overwriting the size we stored in resolved 146 | let (row_offsets, column_offsets) = sizes.split_at_mut(nrows); 147 | let mut offset = 0.0; 148 | 149 | for (i, row) in rows.iter().enumerate() { 150 | row_offsets[i] = offset; 151 | offset += row + spacing.x; 152 | } 153 | 154 | offset = 0.0; 155 | for (i, column) in columns.iter().enumerate() { 156 | column_offsets[i] = offset; 157 | offset += column + spacing.y; 158 | } 159 | 160 | for child in children.iter() { 161 | let child_props = child.as_ref().unwrap().get_props(); 162 | let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits()); 163 | let (row, column) = child_props.index(); 164 | 165 | let (x, y) = swap_axis(yaxis, Vec2::new(column_offsets[column], row_offsets[row])); 166 | let (w, h) = swap_axis(yaxis, Vec2::new(columns[column], rows[row])); 167 | let child_area = AbsRect::new(x, y, x + w, y + h); 168 | 169 | let stage = child 170 | .as_ref() 171 | .unwrap() 172 | .stage(child_area, child_limit, window); 173 | if let Some(node) = stage.get_rtree().upgrade() { 174 | nodes.push_back(Some(node)); 175 | } 176 | staging.push_back(Some(stage)); 177 | } 178 | 179 | // No need to cap this because unsized axis have now been resolved 180 | let evaluated_area = AbsRect( 181 | super::limit_area(area * outer_safe, limits).0 + (padding.0 * MINUS_BOTTOMRIGHT), 182 | ); 183 | 184 | let anchor = props.anchor().resolve(window.dpi) * evaluated_area.dim(); 185 | evaluated_area - anchor 186 | }); 187 | 188 | Box::new(Concrete { 189 | area: evaluated_area, 190 | render: renderable, 191 | rtree: rtree::Node::new(evaluated_area, None, nodes, id, window), 192 | children: staging, 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /feather-ui/src/layout/leaf.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::base::Empty; 5 | use super::{Concrete, Desc, Layout, Renderable, Staged, base, map_unsized_area}; 6 | use crate::{AbsRect, DRect, SourceID, ZERO_POINT, rtree}; 7 | use std::marker::PhantomData; 8 | use std::rc::Rc; 9 | 10 | pub trait Prop: base::Area + base::Limits + base::Anchor {} 11 | 12 | crate::gen_from_to_dyn!(Prop); 13 | 14 | impl Prop for DRect {} 15 | 16 | // Actual leaves do not require padding, but a lot of raw elements do (text, shape, images, etc.) 17 | // This inherits Prop to allow elements to "extract" the padding for the rendering system for 18 | // when it doesn't affect layouts. 19 | pub trait Padded: Prop + base::Padding {} 20 | 21 | crate::gen_from_to_dyn!(Padded); 22 | 23 | impl Padded for DRect {} 24 | 25 | impl Desc for dyn Prop { 26 | type Props = dyn Prop; 27 | type Child = dyn Empty; 28 | type Children = PhantomData>; 29 | 30 | fn stage<'a>( 31 | props: &Self::Props, 32 | outer_area: AbsRect, 33 | outer_limits: crate::AbsLimits, 34 | _: &Self::Children, 35 | id: std::rc::Weak, 36 | renderable: Option>, 37 | window: &mut crate::component::window::WindowState, 38 | ) -> Box { 39 | let limits = outer_limits + props.limits().resolve(window.dpi); 40 | let evaluated_area = super::limit_area( 41 | map_unsized_area(props.area().resolve(window.dpi), ZERO_POINT) 42 | * super::nuetralize_unsized(outer_area), 43 | limits, 44 | ); 45 | 46 | let anchor = props.anchor().resolve(window.dpi) * evaluated_area.dim(); 47 | let evaluated_area = evaluated_area - anchor; 48 | 49 | Box::new(Concrete { 50 | area: evaluated_area, 51 | render: renderable, 52 | rtree: rtree::Node::new(evaluated_area, None, Default::default(), id, window), 53 | children: Default::default(), 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /feather-ui/src/layout/list.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use ultraviolet::Vec2; 5 | 6 | use super::{ 7 | Concrete, Desc, Layout, Renderable, Staged, base, check_unsized_abs, map_unsized_area, 8 | nuetralize_unsized, swap_axis, 9 | }; 10 | use crate::{AbsRect, RowDirection, SourceID, ZERO_POINT, rtree}; 11 | use std::rc::Rc; 12 | 13 | pub trait Prop: base::Area + base::Limits + base::Direction {} 14 | 15 | crate::gen_from_to_dyn!(Prop); 16 | 17 | pub trait Child: base::RLimits + base::Margin + base::Order {} 18 | 19 | crate::gen_from_to_dyn!(Child); 20 | 21 | impl Desc for dyn Prop { 22 | type Props = dyn Prop; 23 | type Child = dyn Child; 24 | // TODO: Make a sorted im::Vector that uses base::Order to order inserted children. 25 | type Children = im::Vector>>>; 26 | 27 | fn stage<'a>( 28 | props: &Self::Props, 29 | outer_area: AbsRect, 30 | outer_limits: crate::AbsLimits, 31 | children: &Self::Children, 32 | id: std::rc::Weak, 33 | renderable: Option>, 34 | window: &mut crate::component::window::WindowState, 35 | ) -> Box { 36 | // TODO: make insertion efficient by creating a RRB tree of list layout subnodes, in a similar manner to the r-tree nodes. 37 | 38 | let limits = outer_limits + props.limits().resolve(window.dpi); 39 | let myarea = props.area().resolve(window.dpi); 40 | //let (unsized_x, unsized_y) = super::check_unsized(*myarea); 41 | let dir = props.direction(); 42 | // For the purpose of calculating size, we only care about which axis we're distributing along 43 | let xaxis = match dir { 44 | RowDirection::LeftToRight | RowDirection::RightToLeft => true, 45 | RowDirection::TopToBottom | RowDirection::BottomToTop => false, 46 | }; 47 | 48 | // Even if both axis are sized, we have to precalculate the areas and margins anyway. 49 | let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits); 50 | let inner_area = AbsRect::from(inner_dim); 51 | let outer_safe = nuetralize_unsized(outer_area); 52 | // The inner_dim must preserve whether an axis is unsized, but the actual limits must be respected regardless. 53 | let (main_limit, _) = super::swap_axis(xaxis, inner_dim.0.min_by_component(limits.max())); 54 | 55 | // This should eventually be a persistent fold 56 | let mut areas: im::Vector> = im::Vector::new(); 57 | let mut aux_margins: im::Vector = im::Vector::new(); 58 | let mut cur = ZERO_POINT; 59 | let mut max_main = 0.0; 60 | let mut max_aux: f32 = 0.0; 61 | let mut prev_margin = f32::NAN; 62 | let mut aux_margin: f32 = 0.0; 63 | let mut aux_margin_bottom = f32::NAN; 64 | let mut prev_aux_margin = f32::NAN; 65 | 66 | for child in children.iter() { 67 | let child_props = child.as_ref().unwrap().get_props(); 68 | let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits()); 69 | let child_margin = child_props.margin().resolve(window.dpi) * outer_safe; 70 | 71 | let stage = child 72 | .as_ref() 73 | .unwrap() 74 | .stage(inner_area, child_limit, window); 75 | let area = stage.get_area(); 76 | 77 | let (margin_main, child_margin_aux) = super::swap_axis(xaxis, child_margin.topleft()); 78 | let (main, aux) = super::swap_axis(xaxis, area.dim().0); 79 | let mut margin = super::merge_margin(prev_margin, margin_main); 80 | // Have to add the margin here before we zero it 81 | areas.push_back(Some((area, margin))); 82 | 83 | if !prev_margin.is_nan() && cur.x + main + margin > main_limit { 84 | max_main = cur.x.max(max_main); 85 | cur.x = 0.0; 86 | let aux_merge = super::merge_margin(prev_aux_margin, aux_margin); 87 | aux_margins.push_back(aux_merge); 88 | cur.y += max_aux + aux_merge; 89 | max_aux = 0.0; 90 | margin = 0.0; 91 | aux_margin = 0.0; 92 | prev_aux_margin = aux_margin_bottom; 93 | aux_margin_bottom = f32::NAN; 94 | } 95 | 96 | cur.x += main + margin; 97 | aux_margin = aux_margin.max(child_margin_aux); 98 | max_aux = max_aux.max(aux); 99 | 100 | let (margin, child_margin_aux) = super::swap_axis(xaxis, child_margin.bottomright()); 101 | prev_margin = margin; 102 | aux_margin_bottom = aux_margin_bottom.max(child_margin_aux); 103 | } 104 | 105 | // Final bounds calculations 106 | max_main = cur.x.max(max_main); 107 | let aux_merge = super::merge_margin(prev_aux_margin, aux_margin); 108 | aux_margins.push_back(aux_merge); 109 | cur.y += max_aux + aux_margin; 110 | let area = map_unsized_area(myarea, Vec2::new(max_main, cur.y)); 111 | 112 | // No need to cap this because unsized axis have now been resolved 113 | let evaluated_area = super::limit_area(area * outer_safe, limits); 114 | 115 | let mut staging: im::Vector>> = im::Vector::new(); 116 | let mut nodes: im::Vector>> = im::Vector::new(); 117 | 118 | // If our parent is asking for a size estimation along the expansion axis, no need to layout the children 119 | // TODO: Double check this assumption is true 120 | let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright()); 121 | if (unsized_x && xaxis) || (unsized_y && !xaxis) { 122 | return Box::new(Concrete { 123 | area: evaluated_area, 124 | render: None, 125 | rtree: rtree::Node::new(evaluated_area, None, nodes, id, window), 126 | children: staging, 127 | }); 128 | } 129 | 130 | let evaluated_dim = evaluated_area.dim(); 131 | let mut cur = match dir { 132 | RowDirection::LeftToRight | RowDirection::TopToBottom => ZERO_POINT, 133 | RowDirection::RightToLeft => Vec2::new(evaluated_dim.0.x, 0.0), 134 | RowDirection::BottomToTop => Vec2::new(0.0, evaluated_dim.0.y), 135 | }; 136 | let mut maxaux: f32 = 0.0; 137 | aux_margins.pop_front(); 138 | let mut aux_margin = aux_margins.pop_front().unwrap_or_default(); 139 | 140 | for (i, child) in children.iter().enumerate() { 141 | let child = child.as_ref().unwrap(); 142 | let (area, margin) = areas[i].unwrap(); 143 | let dim = area.dim().0; 144 | let (_, aux) = swap_axis(xaxis, dim); 145 | 146 | match dir { 147 | RowDirection::RightToLeft => { 148 | if cur.x - dim.x - margin < 0.0 { 149 | cur.y += maxaux + aux_margin; 150 | aux_margin = aux_margins.pop_front().unwrap_or_default(); 151 | maxaux = 0.0; 152 | cur.x = evaluated_dim.0.x - dim.x; 153 | } else { 154 | cur.x -= dim.x + margin 155 | } 156 | } 157 | RowDirection::BottomToTop => { 158 | if cur.y - dim.y - margin < 0.0 { 159 | cur.x += maxaux + aux_margin; 160 | aux_margin = aux_margins.pop_front().unwrap_or_default(); 161 | maxaux = 0.0; 162 | cur.y = evaluated_dim.0.y - dim.y; 163 | } else { 164 | cur.y -= dim.y + margin 165 | } 166 | } 167 | RowDirection::LeftToRight => { 168 | if cur.x + dim.x + margin > evaluated_dim.0.x { 169 | cur.y += maxaux + aux_margin; 170 | aux_margin = aux_margins.pop_front().unwrap_or_default(); 171 | maxaux = 0.0; 172 | cur.x = 0.0; 173 | } else { 174 | cur.x += margin; 175 | } 176 | } 177 | RowDirection::TopToBottom => { 178 | if cur.y + dim.y + margin > evaluated_dim.0.y { 179 | cur.x += maxaux + aux_margin; 180 | aux_margin = aux_margins.pop_front().unwrap_or_default(); 181 | maxaux = 0.0; 182 | cur.y = 0.0; 183 | } else { 184 | cur.y += margin; 185 | } 186 | } 187 | }; 188 | 189 | let child_area = area + cur; 190 | 191 | match dir { 192 | RowDirection::LeftToRight => cur.x += dim.x, 193 | RowDirection::TopToBottom => cur.y += dim.y, 194 | _ => (), 195 | }; 196 | maxaux = maxaux.max(aux); 197 | 198 | let child_limit = *child.get_props().rlimits() * evaluated_dim; 199 | 200 | let stage = child.stage(child_area, child_limit, window); 201 | if let Some(node) = stage.get_rtree().upgrade() { 202 | nodes.push_back(Some(node)); 203 | } 204 | staging.push_back(Some(stage)); 205 | } 206 | 207 | Box::new(Concrete { 208 | area: evaluated_area, 209 | render: renderable, 210 | rtree: rtree::Node::new(evaluated_area, None, nodes, id, window), 211 | children: staging, 212 | }) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /feather-ui/src/layout/root.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::{Desc, Layout, Renderable, Staged, base}; 5 | use crate::{AbsDim, AbsRect, DEFAULT_LIMITS}; 6 | use std::rc::Rc; 7 | 8 | // The root node represents some area on the screen that contains a feather layout. Later this will turn 9 | // into an absolute bounding volume. There can be multiple root nodes, each mapping to a different window. 10 | pub trait Prop { 11 | fn dim(&self) -> &crate::AbsDim; 12 | } 13 | 14 | crate::gen_from_to_dyn!(Prop); 15 | 16 | impl Prop for AbsDim { 17 | fn dim(&self) -> &crate::AbsDim { 18 | self 19 | } 20 | } 21 | 22 | impl Desc for dyn Prop { 23 | type Props = dyn Prop; 24 | type Child = dyn base::Empty; 25 | type Children = Box>; 26 | 27 | fn stage<'a>( 28 | props: &Self::Props, 29 | _: AbsRect, 30 | _: crate::AbsLimits, 31 | child: &Self::Children, 32 | _: std::rc::Weak, 33 | _: Option>, 34 | window: &mut crate::component::window::WindowState, 35 | ) -> Box { 36 | // We bypass creating our own node here because we can never have a nonzero topleft corner, so our node would be redundant. 37 | child.stage((*props.dim()).into(), DEFAULT_LIMITS, window) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /feather-ui/src/layout/text.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use std::rc::Rc; 5 | 6 | use derive_where::derive_where; 7 | 8 | use crate::{AbsRect, SourceID, render, rtree}; 9 | 10 | use super::{Layout, check_unsized, leaf, limit_area}; 11 | 12 | #[derive_where(Clone)] 13 | pub struct Node { 14 | pub id: std::rc::Weak, 15 | pub props: Rc, 16 | pub text_render: Rc, 17 | pub renderable: Rc, 18 | } 19 | 20 | impl Layout for Node { 21 | fn get_props(&self) -> &T { 22 | &self.props 23 | } 24 | fn stage<'a>( 25 | &self, 26 | outer_area: AbsRect, 27 | outer_limits: crate::AbsLimits, 28 | window: &mut crate::component::window::WindowState, 29 | ) -> Box { 30 | let mut limits = self.props.limits().resolve(window.dpi) + outer_limits; 31 | let myarea = self.props.area().resolve(window.dpi); 32 | let (unsized_x, unsized_y) = check_unsized(myarea); 33 | let padding = self.props.padding().resolve(window.dpi); 34 | let allpadding = myarea.bottomright().abs() + padding.topleft() + padding.bottomright(); 35 | let minmax = limits.0.as_array_mut(); 36 | if unsized_x { 37 | minmax[2] -= allpadding.x; 38 | minmax[0] -= allpadding.x; 39 | } 40 | if unsized_y { 41 | minmax[3] -= allpadding.y; 42 | minmax[1] -= allpadding.y; 43 | } 44 | 45 | let mut evaluated_area = limit_area( 46 | super::cap_unsized(myarea * crate::layout::nuetralize_unsized(outer_area)), 47 | limits, 48 | ); 49 | 50 | let (limitx, limity) = { 51 | let max = limits.max(); 52 | ( 53 | max.x.is_finite().then_some(max.x), 54 | max.y.is_finite().then_some(max.y), 55 | ) 56 | }; 57 | 58 | let mut text_binding = self.text_render.text_buffer.borrow_mut(); 59 | let text_buffer = text_binding.as_mut().unwrap(); 60 | let driver = window.driver.clone(); 61 | let mut font_system = driver.font_system.write(); 62 | 63 | let dim = evaluated_area.dim(); 64 | text_buffer.set_size( 65 | &mut font_system, 66 | if unsized_x { limitx } else { Some(dim.0.x) }, 67 | if unsized_y { limity } else { Some(dim.0.y) }, 68 | ); 69 | 70 | text_buffer.shape_until_scroll(&mut font_system, false); 71 | 72 | // If we have indeterminate area, calculate the size 73 | if unsized_x || unsized_y { 74 | let mut h = 0.0; 75 | let mut w: f32 = 0.0; 76 | for run in text_buffer.layout_runs() { 77 | w = w.max(run.line_w); 78 | h += run.line_height; 79 | } 80 | 81 | // Apply adjusted limits to inner size calculation 82 | w = w.max(limits.min().x).min(limits.max().x); 83 | h = h.max(limits.min().y).min(limits.max().y); 84 | let ltrb = evaluated_area.0.as_array_mut(); 85 | if unsized_x { 86 | ltrb[2] = ltrb[0] + w + allpadding.x; 87 | } 88 | if unsized_y { 89 | ltrb[3] = ltrb[1] + h + allpadding.y; 90 | } 91 | }; 92 | 93 | // We always return the full area so we can correctly capture input, but we need 94 | // to pass our padding to our renderer so it can pad the text while setting the 95 | // clip rect to the full area 96 | self.text_render.padding.set(padding); 97 | 98 | evaluated_area = crate::layout::apply_anchor( 99 | evaluated_area, 100 | outer_area, 101 | self.props.anchor().resolve(window.dpi) * evaluated_area.dim(), 102 | ); 103 | 104 | Box::new(crate::layout::Concrete::new( 105 | Some(self.renderable.clone()), 106 | evaluated_area, 107 | rtree::Node::new( 108 | evaluated_area, 109 | None, 110 | Default::default(), 111 | self.id.clone(), 112 | window, 113 | ), 114 | Default::default(), 115 | )) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /feather-ui/src/propbag.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | use std::collections::HashMap; 4 | 5 | use crate::DEFAULT_RLIMITS; 6 | 7 | #[derive(Default)] 8 | pub struct PropBag { 9 | props: HashMap>, 10 | } 11 | 12 | #[allow(dead_code)] 13 | impl PropBag { 14 | pub fn new() -> Self { 15 | Default::default() 16 | } 17 | pub fn contains(&self, element: PropBagElement) -> bool { 18 | self.props.contains_key(&element) 19 | } 20 | fn get_value(&self, e: PropBagElement) -> T { 21 | if let Some(t) = self.props.get(&e) { 22 | *t.downcast_ref::().unwrap() 23 | } else { 24 | Default::default() 25 | } 26 | } 27 | fn set_value(&mut self, e: PropBagElement, v: T) -> Option { 28 | self.props 29 | .insert(e, Box::new(v)) 30 | .map(|x| *x.downcast().unwrap()) 31 | } 32 | } 33 | 34 | macro_rules! gen_prop_bag_base { 35 | ($prop:path, $name:ident, $setter:ident, $t:ty, $default:expr) => { 36 | impl $prop for PropBag { 37 | fn $name(&self) -> &$t { 38 | if let Some(v) = self.props.get(&PropBagElement::$name) { 39 | v.downcast_ref().expect(concat!( 40 | stringify!($name), 41 | " in PropBag was the wrong type!" 42 | )) 43 | } else { 44 | $default 45 | } 46 | } 47 | } 48 | impl PropBag { 49 | #[allow(dead_code)] 50 | pub fn $setter(&mut self, v: $t) -> Option<$t> { 51 | self.props 52 | .insert(PropBagElement::$name, Box::new(v)) 53 | .map(|x| { 54 | *x.downcast().expect(concat!( 55 | stringify!($name), 56 | " in PropBag was the wrong type!" 57 | )) 58 | }) 59 | } 60 | } 61 | }; 62 | } 63 | 64 | macro_rules! gen_prop_bag_value_clone { 65 | ($prop:path, $name:ident, $setter:ident, $t:ty, $default:expr) => { 66 | impl $prop for PropBag { 67 | fn $name(&self) -> $t { 68 | if let Some(v) = self.props.get(&PropBagElement::$name) { 69 | (*v.downcast_ref::<$t>().expect(concat!( 70 | stringify!($name), 71 | " in PropBag was the wrong type!" 72 | ))) 73 | .clone() 74 | } else { 75 | $default 76 | } 77 | } 78 | } 79 | impl PropBag { 80 | #[allow(dead_code)] 81 | pub fn $setter(&mut self, v: $t) -> Option<$t> { 82 | self.props 83 | .insert(PropBagElement::$name, Box::new(v)) 84 | .map(|x| { 85 | *x.downcast().expect(concat!( 86 | stringify!($name), 87 | " in PropBag was the wrong type!" 88 | )) 89 | }) 90 | } 91 | } 92 | }; 93 | } 94 | 95 | macro_rules! gen_prop_bag_all { 96 | ($prop:path, $name:ident, $setter:ident, $ty:ty, $default:expr) => (gen_prop_bag_base!($prop, $name, $setter, $ty, $default);); 97 | ($prop:path, $name:ident, $setter:ident, $ty:ty, $default:expr, $($props:path, $names:ident, $setters:ident, $types:ty, $defaults:expr),+) => ( 98 | gen_prop_bag_base!($prop, $name, $setter, $ty, $default); 99 | gen_prop_bag_all!($($props, $names, $setters, $types, $defaults),+); 100 | ) 101 | } 102 | 103 | macro_rules! gen_prop_bag { 104 | ($($props:path, $names:ident, $setters:ident, $types:ty, $defaults:expr),+) => ( 105 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 106 | #[allow(non_camel_case_types)] 107 | #[repr(u16)] 108 | pub enum PropBagElement { 109 | domain, 110 | zindex, 111 | obstacles, 112 | direction, 113 | wrap, 114 | justify, 115 | align, 116 | order, 117 | grow, 118 | shrink, 119 | basis, 120 | $($names),+ 121 | } 122 | gen_prop_bag_all!($($props, $names, $setters, $types, $defaults),+); 123 | ) 124 | } 125 | 126 | gen_prop_bag_value_clone!(crate::layout::base::Order, order, set_order, i64, 0); 127 | gen_prop_bag_value_clone!(crate::layout::base::ZIndex, zindex, set_zindex, i32, 0); 128 | gen_prop_bag_value_clone!( 129 | crate::layout::base::Direction, 130 | direction, 131 | set_direction, 132 | crate::RowDirection, 133 | crate::RowDirection::LeftToRight 134 | ); 135 | gen_prop_bag_value_clone!( 136 | crate::layout::domain_write::Prop, 137 | domain, 138 | set_domain, 139 | std::rc::Rc, 140 | panic!("PropBag didn't have domain!") 141 | ); 142 | 143 | impl crate::layout::base::Obstacles for PropBag { 144 | fn obstacles(&self) -> &[crate::DAbsRect] { 145 | // We have to be careful here because the actual stored type is a Vec<>, not a slice. 146 | self.props 147 | .get(&PropBagElement::obstacles) 148 | .expect("PropBag didn't have obstacles") 149 | .downcast_ref::>() 150 | .expect("obstacles in PropBag was the wrong type!") 151 | } 152 | } 153 | 154 | impl PropBag { 155 | #[allow(dead_code)] 156 | pub fn set_obstacles(&mut self, v: &[crate::DAbsRect]) -> Option> { 157 | self.props 158 | .insert(PropBagElement::zindex, Box::new(v.to_vec())) 159 | .map(move |x| { 160 | *x.downcast() 161 | .expect("obstacles in PropBag was the wrong type!") 162 | }) 163 | } 164 | } 165 | 166 | #[rustfmt::skip] 167 | gen_prop_bag!( 168 | crate::layout::base::Area, area, set_area, crate::DRect, panic!("No area set and no default available!"), 169 | crate::layout::base::Padding, padding, set_padding, crate::DAbsRect, &crate::ZERO_DABSRECT, 170 | crate::layout::base::Margin, margin, set_margin, crate::DRect, &crate::ZERO_DRECT, 171 | crate::layout::base::Limits, limits, set_limits, crate::DLimits, &crate::DEFAULT_DLIMITS, 172 | crate::layout::base::RLimits, rlimits, set_rlimits, crate::RelLimits, &DEFAULT_RLIMITS, 173 | crate::layout::base::Anchor, anchor, set_anchor, crate::DPoint, &crate::ZERO_DPOINT, 174 | crate::layout::root::Prop, dim, set_dim, crate::AbsDim, panic!("No dim set and no default available!") 175 | ); 176 | 177 | impl crate::layout::base::Empty for PropBag {} 178 | impl crate::layout::leaf::Prop for PropBag {} 179 | impl crate::layout::fixed::Prop for PropBag {} 180 | impl crate::layout::fixed::Child for PropBag {} 181 | impl crate::layout::list::Child for PropBag {} 182 | impl crate::layout::list::Prop for PropBag {} 183 | impl crate::layout::leaf::Padded for PropBag {} 184 | 185 | impl crate::layout::flex::Prop for PropBag { 186 | fn wrap(&self) -> bool { 187 | self.get_value(PropBagElement::wrap) 188 | } 189 | 190 | fn justify(&self) -> crate::layout::flex::FlexJustify { 191 | self.get_value(PropBagElement::justify) 192 | } 193 | 194 | fn align(&self) -> crate::layout::flex::FlexJustify { 195 | self.get_value(PropBagElement::align) 196 | } 197 | } 198 | 199 | impl crate::layout::flex::Child for PropBag { 200 | fn grow(&self) -> f32 { 201 | self.get_value(PropBagElement::grow) 202 | } 203 | 204 | fn shrink(&self) -> f32 { 205 | self.get_value(PropBagElement::shrink) 206 | } 207 | 208 | fn basis(&self) -> crate::DValue { 209 | self.get_value(PropBagElement::basis) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /feather-ui/src/render/chain.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use std::rc::Rc; 5 | 6 | pub struct Pipeline(pub [Rc; N]); 7 | 8 | impl super::Renderable for Pipeline { 9 | fn render( 10 | &self, 11 | area: crate::AbsRect, 12 | driver: &crate::DriverState, 13 | ) -> im::Vector { 14 | self.0.iter().flat_map(|x| x.render(area, driver)).collect() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /feather-ui/src/render/domain/line.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::shaders::{Vertex, to_bytes}; 5 | use crate::{CrossReferenceDomain, DriverState, RenderLambda, SourceID}; 6 | use std::rc::Rc; 7 | use wgpu::util::DeviceExt; 8 | 9 | pub struct Pipeline { 10 | pub this: std::rc::Weak, 11 | pub pipeline: wgpu::RenderPipeline, 12 | pub group: wgpu::BindGroup, 13 | pub domain: Rc, 14 | pub start: Rc, 15 | pub end: Rc, 16 | pub _buffers: [wgpu::Buffer; 3], 17 | } 18 | 19 | impl super::Renderable for Pipeline { 20 | fn render( 21 | &self, 22 | _: crate::AbsRect, 23 | driver: &DriverState, 24 | ) -> im::Vector { 25 | // TODO: This needs to be deferred until the end of the pipeline and then re-inserted back into place to remove dependency cycles 26 | let start = self.domain.get_area(&self.start).unwrap_or_default(); 27 | let end = self.domain.get_area(&self.end).unwrap_or_default(); 28 | 29 | // We have to put this in an RC because it needs to be cloneable across multiple render passes 30 | let verts = Rc::new( 31 | driver 32 | .device 33 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 34 | label: Some("LineVertices"), 35 | contents: to_bytes(&[ 36 | Vertex { 37 | pos: ((start.topleft() + start.bottomright()) * 0.5).into(), 38 | }, 39 | Vertex { 40 | pos: ((end.topleft() + end.bottomright()) * 0.5).into(), 41 | }, 42 | ]), 43 | usage: wgpu::BufferUsages::VERTEX, 44 | }), 45 | ); 46 | 47 | let weak = self.this.clone(); 48 | let mut result = im::Vector::new(); 49 | result.push_back(Some(Box::new(move |pass: &mut wgpu::RenderPass| { 50 | if let Some(this) = weak.upgrade() { 51 | pass.set_vertex_buffer(0, verts.slice(..)); 52 | pass.set_bind_group(0, &this.group, &[]); 53 | pass.set_pipeline(&this.pipeline); 54 | pass.draw( 55 | //0..(this.vertices.size() as u32 / size_of::() as u32), 56 | 0..2, 57 | 0..1, 58 | ); 59 | } 60 | }) as Box)); 61 | result 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /feather-ui/src/render/domain/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use super::Renderable; 5 | use crate::{AbsRect, CrossReferenceDomain, SourceID}; 6 | use std::rc::Rc; 7 | 8 | pub mod line; 9 | 10 | pub struct Write { 11 | pub(crate) id: std::rc::Weak, 12 | pub(crate) domain: Rc, 13 | pub(crate) base: Option>, 14 | } 15 | 16 | impl Renderable for Write { 17 | fn render( 18 | &self, 19 | area: AbsRect, 20 | driver: &crate::DriverState, 21 | ) -> im::Vector { 22 | if let Some(idref) = self.id.upgrade() { 23 | self.domain.write_area(idref, area); 24 | } 25 | 26 | self.base 27 | .as_ref() 28 | .map(|x| x.render(area, driver)) 29 | .unwrap_or_default() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /feather-ui/src/render/line.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::shaders::{Vertex, to_bytes}; 5 | use crate::{DriverState, RenderLambda}; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | use ultraviolet::Vec2; 9 | use wgpu::util::DeviceExt; 10 | 11 | pub struct Pipeline { 12 | pub this: std::rc::Weak, 13 | pub pipeline: wgpu::RenderPipeline, 14 | pub group: wgpu::BindGroup, 15 | pub pos: RefCell<(Vec2, Vec2)>, 16 | pub _buffers: [wgpu::Buffer; 3], 17 | } 18 | 19 | impl super::Renderable for Pipeline { 20 | fn render( 21 | &self, 22 | _: crate::AbsRect, 23 | driver: &DriverState, 24 | ) -> im::Vector { 25 | // We have to put this in an RC because it needs to be cloneable across multiple render passes 26 | let (start, end) = *self.pos.borrow(); 27 | let verts = Rc::new( 28 | driver 29 | .device 30 | .create_buffer_init(&wgpu::util::BufferInitDescriptor { 31 | label: Some("LineVertices"), 32 | contents: to_bytes(&[Vertex { pos: start.into() }, Vertex { pos: end.into() }]), 33 | usage: wgpu::BufferUsages::VERTEX, 34 | }), 35 | ); 36 | 37 | let weak = self.this.clone(); 38 | let mut result = im::Vector::new(); 39 | result.push_back(Some(Box::new(move |pass: &mut wgpu::RenderPass| { 40 | if let Some(this) = weak.upgrade() { 41 | pass.set_vertex_buffer(0, verts.slice(..)); 42 | pass.set_bind_group(0, &this.group, &[]); 43 | pass.set_pipeline(&this.pipeline); 44 | pass.draw( 45 | //0..(this.vertices.size() as u32 / size_of::() as u32), 46 | 0..2, 47 | 0..1, 48 | ); 49 | } 50 | }) as Box)); 51 | result 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /feather-ui/src/render/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use crate::{AbsRect, DriverState}; 5 | 6 | pub mod chain; 7 | pub mod domain; 8 | pub mod line; 9 | pub mod standard; 10 | pub mod text; 11 | 12 | pub trait Renderable { 13 | fn render(&self, area: AbsRect, driver: &DriverState) -> im::Vector; 14 | } 15 | -------------------------------------------------------------------------------- /feather-ui/src/render/standard.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use ultraviolet::Vec4; 5 | 6 | use crate::{DriverState, RenderLambda}; 7 | 8 | pub struct Pipeline { 9 | pub this: std::rc::Weak, 10 | pub pipeline: wgpu::RenderPipeline, 11 | pub group: wgpu::BindGroup, 12 | pub vertices: wgpu::Buffer, 13 | pub buffers: [wgpu::Buffer; 6], 14 | pub padding: crate::AbsRect, 15 | } 16 | 17 | impl super::Renderable for Pipeline { 18 | fn render( 19 | &self, 20 | area: crate::AbsRect, 21 | driver: &DriverState, 22 | ) -> im::Vector { 23 | driver.queue.write_buffer( 24 | &self.buffers[1], 25 | 0, 26 | Vec4::new( 27 | area.topleft().x + self.padding.topleft().x, 28 | area.topleft().y + self.padding.topleft().y, 29 | area.bottomright().x - area.topleft().x - self.padding.bottomright().x, 30 | area.bottomright().y - area.topleft().y - self.padding.bottomright().y, 31 | ) 32 | .as_byte_slice(), 33 | ); 34 | 35 | let weak = self.this.clone(); 36 | let mut result = im::Vector::new(); 37 | result.push_back(Some(Box::new(move |pass: &mut wgpu::RenderPass| { 38 | if let Some(this) = weak.upgrade() { 39 | pass.set_vertex_buffer(0, this.vertices.slice(..)); 40 | pass.set_bind_group(0, &this.group, &[]); 41 | pass.set_pipeline(&this.pipeline); 42 | pass.draw( 43 | //0..(this.vertices.size() as u32 / size_of::() as u32), 44 | 0..4, 45 | 0..1, 46 | ); 47 | } 48 | }) as Box)); 49 | result 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /feather-ui/src/render/text.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | 7 | use glyphon::Viewport; 8 | 9 | use crate::{AbsRect, RenderLambda}; 10 | 11 | pub struct Pipeline { 12 | pub this: std::rc::Weak, 13 | pub renderer: RefCell, 14 | pub text_buffer: Rc>>, 15 | pub padding: std::cell::Cell, 16 | pub atlas: Rc>, 17 | pub viewport: Rc>, 18 | } 19 | 20 | impl super::Renderable for Pipeline { 21 | fn render( 22 | &self, 23 | area: AbsRect, 24 | driver: &crate::DriverState, 25 | ) -> im::Vector { 26 | let mut font_system = driver.font_system.write(); 27 | let padding = self.padding.get(); 28 | 29 | self.renderer 30 | .borrow_mut() 31 | .prepare( 32 | &driver.device, 33 | &driver.queue, 34 | &mut font_system, 35 | &mut self.atlas.borrow_mut(), 36 | &self.viewport.borrow(), 37 | [glyphon::TextArea { 38 | buffer: self.text_buffer.borrow().as_ref().unwrap(), 39 | left: area.topleft().x + padding.topleft().x, 40 | top: area.topleft().y + padding.topleft().y, 41 | scale: 1.0, 42 | bounds: glyphon::TextBounds { 43 | left: area.topleft().x as i32, 44 | top: area.topleft().y as i32, 45 | right: area.bottomright().x as i32, 46 | bottom: area.bottomright().y as i32, 47 | }, 48 | default_color: glyphon::Color::rgb(255, 255, 255), 49 | custom_glyphs: &[], 50 | }], 51 | &mut driver.swash_cache.write(), 52 | ) 53 | .unwrap(); 54 | 55 | let mut result = im::Vector::new(); 56 | let weak = self.this.clone(); 57 | result.push_back(Some(Box::new(move |pass: &mut wgpu::RenderPass| { 58 | if let Some(this) = weak.upgrade() { 59 | this.renderer 60 | .borrow() 61 | .render(&this.atlas.borrow(), &this.viewport.borrow(), pass) 62 | .unwrap(); 63 | } 64 | }) as Box)); 65 | result 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Arc.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | in vec2 pos; 3 | layout(binding=1) uniform vec4 PosDim; 4 | layout(binding=2) uniform vec4 DimBorderBlur; 5 | layout(binding=3) uniform vec4 Corners; 6 | layout(binding=4) uniform vec4 Fill; 7 | layout(binding=5) uniform vec4 Outline; 8 | out vec4 fragColor; 9 | 10 | const float PI=3.14159265359; 11 | 12 | float linearstep(float low,float high,float x){return clamp((x-low)/(high-low),0.,1.);} 13 | vec2 rotate(vec2 p,float a){return vec2(p.x*cos(a)+p.y*sin(a),p.x*sin(a)-p.y*cos(a));} 14 | vec4 sRGB(vec4 linearRGB){return vec4(1.055*pow(linearRGB.rgb,vec3(1./2.4)-.055),linearRGB.a);} 15 | vec4 linearRGB(vec4 sRGB){return vec4(pow((sRGB.rgb+.055)/1.055,vec3(2.4)),sRGB.a);} 16 | void main() 17 | { 18 | float l=(PosDim.z+PosDim.w)*.5; 19 | vec2 uv=(pos*2.)-1.; 20 | float width=fwidth(pos.x); 21 | float w1=(1.+DimBorderBlur.w)*width; 22 | 23 | float border=(DimBorderBlur.z/l)*2.;// double because UV is in range [-1,1], not [0,1] 24 | float t=.50-(Corners.z/l)+w1*1.5; 25 | // We have to compensate for needing to do smoothstep starting from 0, which combined with abs() 26 | // acts as a ceil() function, creating one extra half pixel. 27 | float r=1.-t+w1; 28 | 29 | // SDF for circle 30 | float d0=abs(length(uv)-r)-t+border; 31 | float d1=abs(length(uv)-r)-t; 32 | 33 | // SDF for lines that make up arc 34 | vec2 omega1=rotate(uv,Corners.x-Corners.y); 35 | vec2 omega2=rotate(uv,Corners.x+Corners.y); 36 | float d; 37 | 38 | // TODO: This cannot deal with non-integer circle radii, but it might be generalizable to those cases. 39 | if(abs(-omega1.y)+abs(omega2.y)PI*.5){ 42 | d=max(-omega1.y,omega2.y); 43 | }else{ 44 | d=min(-omega1.y,omega2.y); 45 | } 46 | 47 | // Compensate for blur so the circle is still full or empty at 2pi and 0. 48 | d+=(clamp(Corners.y/PI,0.,1.)-.5)*2.*(DimBorderBlur.w*width)+border; 49 | 50 | float d2=d-border+w1; 51 | float d3=min(d,omega1.x+Corners.y)+w1; 52 | 53 | // Merge results of both SDFs 54 | float s=linearstep(-w1,w1,min(-d0,d2)-w1); 55 | float alpha=linearstep(-w1,w1,min(-d1,d3)-w1); 56 | 57 | // Output to screen 58 | fragColor=vec4(Fill.rgb,1)*Fill.a*s+vec4(Outline.rgb,1)*Outline.a*clamp(alpha-s,0.,1.); 59 | } 60 | 61 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Arc.wgsl: -------------------------------------------------------------------------------- 1 | struct FragmentOutput { 2 | @location(0) fragColor: vec4, 3 | } 4 | 5 | const PI: f32 = 3.1415927f; 6 | 7 | var pos_1: vec2; 8 | @group(0) @binding(1) 9 | var PosDim: vec4; 10 | @group(0) @binding(2) 11 | var DimBorderBlur: vec4; 12 | @group(0) @binding(3) 13 | var Corners: vec4; 14 | @group(0) @binding(4) 15 | var Fill: vec4; 16 | @group(0) @binding(5) 17 | var Outline: vec4; 18 | var fragColor: vec4; 19 | 20 | fn linearstep(low: f32, high: f32, x: f32) -> f32 { 21 | var low_1: f32; 22 | var high_1: f32; 23 | var x_1: f32; 24 | 25 | low_1 = low; 26 | high_1 = high; 27 | x_1 = x; 28 | let _e14 = x_1; 29 | let _e15 = low_1; 30 | let _e17 = high_1; 31 | let _e18 = low_1; 32 | let _e23 = x_1; 33 | let _e24 = low_1; 34 | let _e26 = high_1; 35 | let _e27 = low_1; 36 | return clamp(((_e23 - _e24) / (_e26 - _e27)), 0f, 1f); 37 | } 38 | 39 | fn rotate(p: vec2, a: f32) -> vec2 { 40 | var p_1: vec2; 41 | var a_1: f32; 42 | 43 | p_1 = p; 44 | a_1 = a; 45 | let _e12 = p_1; 46 | let _e15 = a_1; 47 | let _e18 = p_1; 48 | let _e21 = a_1; 49 | let _e25 = p_1; 50 | let _e28 = a_1; 51 | let _e31 = p_1; 52 | let _e34 = a_1; 53 | return vec2(((_e12.x * cos(_e15)) + (_e18.y * sin(_e21))), ((_e25.x * sin(_e28)) - (_e31.y * cos(_e34)))); 54 | } 55 | 56 | fn sRGB(linearRGB: vec4) -> vec4 { 57 | var linearRGB_1: vec4; 58 | 59 | linearRGB_1 = linearRGB; 60 | let _e11 = linearRGB_1; 61 | let _e25 = linearRGB_1; 62 | let _e40 = (1.055f * pow(_e25.xyz, vec3(0.36166665f, 0.36166665f, 0.36166665f))); 63 | let _e41 = linearRGB_1; 64 | return vec4(_e40.x, _e40.y, _e40.z, _e41.w); 65 | } 66 | 67 | fn linearRGB_2(sRGB_1: vec4) -> vec4 { 68 | var sRGB_2: vec4; 69 | 70 | sRGB_2 = sRGB_1; 71 | let _e10 = sRGB_2; 72 | let _e20 = sRGB_2; 73 | let _e30 = pow(((_e20.xyz + vec3(0.055f)) / vec3(1.055f)), vec3(2.4f)); 74 | let _e31 = sRGB_2; 75 | return vec4(_e30.x, _e30.y, _e30.z, _e31.w); 76 | } 77 | 78 | fn main_1() { 79 | var l: f32; 80 | var uv: vec2; 81 | var width: f32; 82 | var w1_: f32; 83 | var border: f32; 84 | var t: f32; 85 | var r: f32; 86 | var d0_: f32; 87 | var d1_: f32; 88 | var omega1_: vec2; 89 | var omega2_: vec2; 90 | var d: f32; 91 | var d2_: f32; 92 | var d3_: f32; 93 | var s: f32; 94 | var alpha: f32; 95 | 96 | let _e8 = PosDim; 97 | let _e10 = PosDim; 98 | l = ((_e8.z + _e10.w) * 0.5f); 99 | let _e16 = pos_1; 100 | uv = ((_e16 * 2f) - vec2(1f)); 101 | let _e23 = pos_1; 102 | let _e25 = pos_1; 103 | let _e27 = fwidth(_e25.x); 104 | width = _e27; 105 | let _e30 = DimBorderBlur; 106 | let _e33 = width; 107 | w1_ = ((1f + _e30.w) * _e33); 108 | let _e36 = DimBorderBlur; 109 | let _e38 = l; 110 | border = ((_e36.z / _e38) * 2f); 111 | let _e44 = Corners; 112 | let _e46 = l; 113 | let _e49 = w1_; 114 | t = ((0.5f - (_e44.z / _e46)) + (_e49 * 1.5f)); 115 | let _e55 = t; 116 | let _e57 = w1_; 117 | r = ((1f - _e55) + _e57); 118 | let _e61 = uv; 119 | let _e63 = r; 120 | let _e66 = uv; 121 | let _e68 = r; 122 | let _e71 = t; 123 | let _e73 = border; 124 | d0_ = ((abs((length(_e66) - _e68)) - _e71) + _e73); 125 | let _e77 = uv; 126 | let _e79 = r; 127 | let _e82 = uv; 128 | let _e84 = r; 129 | let _e87 = t; 130 | d1_ = (abs((length(_e82) - _e84)) - _e87); 131 | let _e91 = Corners; 132 | let _e93 = Corners; 133 | let _e96 = uv; 134 | let _e97 = Corners; 135 | let _e99 = Corners; 136 | let _e102 = rotate(_e96, (_e97.x - _e99.y)); 137 | omega1_ = _e102; 138 | let _e105 = Corners; 139 | let _e107 = Corners; 140 | let _e110 = uv; 141 | let _e111 = Corners; 142 | let _e113 = Corners; 143 | let _e116 = rotate(_e110, (_e111.x + _e113.y)); 144 | omega2_ = _e116; 145 | let _e119 = omega1_; 146 | let _e122 = omega1_; 147 | let _e126 = omega2_; 148 | let _e128 = omega2_; 149 | let _e132 = width; 150 | if ((abs(-(_e122.y)) + abs(_e128.y)) < _e132) { 151 | { 152 | let _e134 = Corners; 153 | let _e141 = width; 154 | d = ((((_e134.y / PI) - 0.5f) * 2f) * _e141); 155 | } 156 | } else { 157 | let _e143 = Corners; 158 | if (_e143.y > 1.5707964f) { 159 | { 160 | let _e149 = omega1_; 161 | let _e152 = omega2_; 162 | let _e154 = omega1_; 163 | let _e157 = omega2_; 164 | d = max(-(_e154.y), _e157.y); 165 | } 166 | } else { 167 | { 168 | let _e160 = omega1_; 169 | let _e163 = omega2_; 170 | let _e165 = omega1_; 171 | let _e168 = omega2_; 172 | d = min(-(_e165.y), _e168.y); 173 | } 174 | } 175 | } 176 | let _e171 = d; 177 | let _e172 = Corners; 178 | let _e177 = Corners; 179 | let _e187 = DimBorderBlur; 180 | let _e189 = width; 181 | let _e192 = border; 182 | d = (_e171 + ((((clamp((_e177.y / PI), 0f, 1f) - 0.5f) * 2f) * (_e187.w * _e189)) + _e192)); 183 | let _e195 = d; 184 | let _e196 = border; 185 | let _e198 = w1_; 186 | d2_ = ((_e195 - _e196) + _e198); 187 | let _e202 = omega1_; 188 | let _e204 = Corners; 189 | let _e207 = d; 190 | let _e208 = omega1_; 191 | let _e210 = Corners; 192 | let _e214 = w1_; 193 | d3_ = (min(_e207, (_e208.x + _e210.y)) + _e214); 194 | let _e217 = w1_; 195 | let _e220 = d0_; 196 | let _e223 = d0_; 197 | let _e225 = d2_; 198 | let _e227 = w1_; 199 | let _e229 = w1_; 200 | let _e231 = w1_; 201 | let _e232 = d0_; 202 | let _e235 = d0_; 203 | let _e237 = d2_; 204 | let _e239 = w1_; 205 | let _e241 = linearstep(-(_e229), _e231, (min(-(_e235), _e237) - _e239)); 206 | s = _e241; 207 | let _e243 = w1_; 208 | let _e246 = d1_; 209 | let _e249 = d1_; 210 | let _e251 = d3_; 211 | let _e253 = w1_; 212 | let _e255 = w1_; 213 | let _e257 = w1_; 214 | let _e258 = d1_; 215 | let _e261 = d1_; 216 | let _e263 = d3_; 217 | let _e265 = w1_; 218 | let _e267 = linearstep(-(_e255), _e257, (min(-(_e261), _e263) - _e265)); 219 | alpha = _e267; 220 | let _e269 = Fill; 221 | let _e270 = _e269.xyz; 222 | let _e277 = Fill; 223 | let _e280 = s; 224 | let _e282 = Outline; 225 | let _e283 = _e282.xyz; 226 | let _e290 = Outline; 227 | let _e293 = alpha; 228 | let _e294 = s; 229 | let _e298 = alpha; 230 | let _e299 = s; 231 | fragColor = (((vec4(_e270.x, _e270.y, _e270.z, 1f) * _e277.w) * _e280) + ((vec4(_e283.x, _e283.y, _e283.z, 1f) * _e290.w) * clamp((_e298 - _e299), 0f, 1f))); 232 | return; 233 | } 234 | 235 | @fragment 236 | fn main(@location(0) pos: vec2) -> FragmentOutput { 237 | pos_1 = pos; 238 | main_1(); 239 | let _e19 = fragColor; 240 | return FragmentOutput(_e19); 241 | } 242 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Circle.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | in vec2 pos; 3 | layout(binding=1) uniform vec4 PosDim; 4 | layout(binding=2) uniform vec4 DimBorderBlur; 5 | layout(binding=3) uniform vec4 Corners; 6 | layout(binding=4) uniform vec4 Fill; 7 | layout(binding=5) uniform vec4 Outline; 8 | out vec4 fragColor; 9 | const float PI = 3.14159265359; 10 | 11 | float linearstep(float low, float high, float x) { return clamp((x - low) / (high - low), 0.0, 1.0); } 12 | void main() 13 | { 14 | float l = (PosDim.z+PosDim.w) * 0.5; 15 | vec2 uv = (pos*2.0) - 1.0; 16 | float w1 = (1.0 + DimBorderBlur.w)*fwidth(pos.x); 17 | 18 | float border = (DimBorderBlur.z / l) * 2.0; // double because UV is in range [-1,1], not [0,1] 19 | float t = 0.50 - (Corners.x / l); 20 | // We have to compensate for needing to do smoothstep starting from 0, which combined with abs() 21 | // acts as a ceil() function, creating one extra half pixel. 22 | float r = 1.0 - t - w1; 23 | 24 | // SDF for circle 25 | float inner = (Corners.y / l) * 2.0; 26 | float d0 = abs(length(uv) - r + (border*0.5) - (inner*0.5)) - t + (border*0.5) + (inner*0.5); 27 | float d1 = abs(length(uv) - r) - t; 28 | float s = pow(linearstep(w1*2.0, 0.0, d0), 2.2); 29 | float alpha = pow(linearstep(w1*2.0, 0.0, d1), 2.2); 30 | 31 | // Output to screen 32 | fragColor = (vec4(Fill.rgb, 1)*Fill.a*s) + (vec4(Outline.rgb, 1)*Outline.a*clamp(alpha - s, 0.0, 1.0)); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Circle.wgsl: -------------------------------------------------------------------------------- 1 | struct FragmentOutput { 2 | @location(0) fragColor: vec4, 3 | } 4 | 5 | const PI: f32 = 3.1415927f; 6 | 7 | var pos_1: vec2; 8 | @group(0) @binding(1) 9 | var PosDim: vec4; 10 | @group(0) @binding(2) 11 | var DimBorderBlur: vec4; 12 | @group(0) @binding(3) 13 | var Corners: vec4; 14 | @group(0) @binding(4) 15 | var Fill: vec4; 16 | @group(0) @binding(5) 17 | var Outline: vec4; 18 | var fragColor: vec4; 19 | 20 | fn linearstep(low: f32, high: f32, x: f32) -> f32 { 21 | var low_1: f32; 22 | var high_1: f32; 23 | var x_1: f32; 24 | 25 | low_1 = low; 26 | high_1 = high; 27 | x_1 = x; 28 | let _e14 = x_1; 29 | let _e15 = low_1; 30 | let _e17 = high_1; 31 | let _e18 = low_1; 32 | let _e23 = x_1; 33 | let _e24 = low_1; 34 | let _e26 = high_1; 35 | let _e27 = low_1; 36 | return clamp(((_e23 - _e24) / (_e26 - _e27)), 0f, 1f); 37 | } 38 | 39 | fn main_1() { 40 | var l: f32; 41 | var uv: vec2; 42 | var w1_: f32; 43 | var border: f32; 44 | var t: f32; 45 | var r: f32; 46 | var inner: f32; 47 | var d0_: f32; 48 | var d1_: f32; 49 | var s: f32; 50 | var alpha: f32; 51 | 52 | let _e8 = PosDim; 53 | let _e10 = PosDim; 54 | l = ((_e8.z + _e10.w) * 0.5f); 55 | let _e16 = pos_1; 56 | uv = ((_e16 * 2f) - vec2(1f)); 57 | let _e24 = DimBorderBlur; 58 | let _e27 = pos_1; 59 | let _e29 = pos_1; 60 | let _e31 = fwidth(_e29.x); 61 | w1_ = ((1f + _e24.w) * _e31); 62 | let _e34 = DimBorderBlur; 63 | let _e36 = l; 64 | border = ((_e34.z / _e36) * 2f); 65 | let _e42 = Corners; 66 | let _e44 = l; 67 | t = (0.5f - (_e42.x / _e44)); 68 | let _e49 = t; 69 | let _e51 = w1_; 70 | r = ((1f - _e49) - _e51); 71 | let _e54 = Corners; 72 | let _e56 = l; 73 | inner = ((_e54.y / _e56) * 2f); 74 | let _e62 = uv; 75 | let _e64 = r; 76 | let _e66 = border; 77 | let _e70 = inner; 78 | let _e75 = uv; 79 | let _e77 = r; 80 | let _e79 = border; 81 | let _e83 = inner; 82 | let _e88 = t; 83 | let _e90 = border; 84 | let _e94 = inner; 85 | d0_ = (((abs((((length(_e75) - _e77) + (_e79 * 0.5f)) - (_e83 * 0.5f))) - _e88) + (_e90 * 0.5f)) + (_e94 * 0.5f)); 86 | let _e100 = uv; 87 | let _e102 = r; 88 | let _e105 = uv; 89 | let _e107 = r; 90 | let _e110 = t; 91 | d1_ = (abs((length(_e105) - _e107)) - _e110); 92 | let _e113 = w1_; 93 | let _e118 = w1_; 94 | let _e122 = d0_; 95 | let _e123 = linearstep((_e118 * 2f), 0f, _e122); 96 | let _e125 = w1_; 97 | let _e130 = w1_; 98 | let _e134 = d0_; 99 | let _e135 = linearstep((_e130 * 2f), 0f, _e134); 100 | s = pow(_e135, 2.2f); 101 | let _e139 = w1_; 102 | let _e144 = w1_; 103 | let _e148 = d1_; 104 | let _e149 = linearstep((_e144 * 2f), 0f, _e148); 105 | let _e151 = w1_; 106 | let _e156 = w1_; 107 | let _e160 = d1_; 108 | let _e161 = linearstep((_e156 * 2f), 0f, _e160); 109 | alpha = pow(_e161, 2.2f); 110 | let _e165 = Fill; 111 | let _e166 = _e165.xyz; 112 | let _e173 = Fill; 113 | let _e176 = s; 114 | let _e178 = Outline; 115 | let _e179 = _e178.xyz; 116 | let _e186 = Outline; 117 | let _e189 = alpha; 118 | let _e190 = s; 119 | let _e194 = alpha; 120 | let _e195 = s; 121 | fragColor = (((vec4(_e166.x, _e166.y, _e166.z, 1f) * _e173.w) * _e176) + ((vec4(_e179.x, _e179.y, _e179.z, 1f) * _e186.w) * clamp((_e194 - _e195), 0f, 1f))); 122 | return; 123 | } 124 | 125 | @fragment 126 | fn main(@location(0) pos: vec2) -> FragmentOutput { 127 | pos_1 = pos; 128 | main_1(); 129 | let _e19 = fragColor; 130 | return FragmentOutput(_e19); 131 | } 132 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Image.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | in vec2 uv; 3 | in vec4 color; 4 | out vec4 fragColor; 5 | 6 | uniform sampler2D texture; 7 | 8 | void main() 9 | { 10 | vec4 c = texture2D(texture, uv); 11 | fragColor = vec4(c.rgb * color.rgb * vec3(color.a,color.a,color.a), c.a*color.a); 12 | //fragColor = vec4(uv.x, uv.y, 0, 1); 13 | } 14 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Image.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 440 2 | layout(binding=0) uniform mat4 MVP; 3 | in vec4 vPosUV; 4 | in vec4 vColor; 5 | out vec2 uv; 6 | out vec4 color; 7 | 8 | void main() 9 | { 10 | gl_Position = MVP * vec4(vPosUV.xy, 0, 1); 11 | uv = vPosUV.zw; 12 | color = vColor; 13 | } 14 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Line.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | layout(binding=2) uniform vec4 Color; 3 | in vec2 vLineCenter; 4 | out vec4 fragColor; 5 | 6 | void main(void) 7 | { 8 | vec4 col = Color; 9 | float d = length(vLineCenter-gl_FragCoord.xy); 10 | float LineWidth = 1.0; 11 | col.a *= smoothstep(0.0, 1.0, LineWidth-d); 12 | fragColor = vec4(col.rgb*col.a, col.a); 13 | } 14 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Line.frag.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(2) 2 | var Color: vec4f; 3 | 4 | @fragment 5 | fn main(@location(0) vLineCenter: vec2f, @builtin(position) gl_FragCoord: vec4f) -> @location(0) vec4f { 6 | var col: vec4f = Color; 7 | let d = length(vLineCenter - gl_FragCoord.xy); 8 | let LineWidth = 1.1f; 9 | col.a *= smoothstep(0.0f, 1.0f, LineWidth - d); 10 | return vec4f(col.rgb*col.a, col.a); 11 | } -------------------------------------------------------------------------------- /feather-ui/src/shaders/Line.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | layout(binding=0) uniform mat4 MVP; 3 | layout(binding=1) uniform vec2 ViewPort; //Width and Height of the viewport 4 | in vec2 vPos; 5 | out vec2 vLineCenter; 6 | 7 | void main(void) 8 | { 9 | vec4 pp = MVP * vec4(vPos.xy, 0, 1); 10 | gl_Position = pp; 11 | vLineCenter = 0.5*(pp.xy + vec2(1, 1))*ViewPort; 12 | } 13 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Line.vert.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @location(0) vLineCenter: vec2f, 3 | @builtin(position) gl_Position: vec4f, 4 | } 5 | 6 | @group(0) @binding(0) 7 | var MVP: mat4x4; 8 | @group(0) @binding(1) 9 | var ViewPort: vec2; 10 | 11 | @vertex 12 | fn main(@location(0) vPos: vec2f) -> VertexOutput { 13 | let pp = (MVP * vec4f(vPos.x, vPos.y, 0f, 1f)); 14 | let vLineCenter = ((0.5f * (pp.xy + vec2f(1f, 1f))) * ViewPort); 15 | return VertexOutput(vLineCenter, pp); 16 | } 17 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/RoundRect.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | in vec2 pos; 3 | layout(binding=1) uniform vec4 PosDim; 4 | layout(binding=2) uniform vec4 DimBorderBlur; 5 | layout(binding=3) uniform vec4 Corners; 6 | layout(binding=4) uniform vec4 Fill; 7 | layout(binding=5) uniform vec4 Outline; 8 | out vec4 fragColor; 9 | 10 | float linearstep(float low, float high, float x) { return clamp((x - low) / (high - low), 0.0, 1.0); } 11 | float rectangle(vec2 samplePosition, vec2 halfSize, vec4 edges) { 12 | float edge = 20.0; 13 | if(samplePosition.x > 0.0) 14 | edge = (samplePosition.y < 0.0) ? edges.y : edges.z; 15 | else 16 | edge = (samplePosition.y < 0.0) ? edges.x : edges.w; 17 | 18 | vec2 componentWiseEdgeDistance = abs(samplePosition) - halfSize + vec2(edge); 19 | float outsideDistance = length(max(componentWiseEdgeDistance, 0.0)); 20 | float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0.0); 21 | return outsideDistance + insideDistance - edge; 22 | } 23 | 24 | void main() 25 | { 26 | // Ideally we would get DPI for both height and width, but for now we just assume DPI isn't weird 27 | float w = fwidth(PosDim.z*pos.x) * 0.5 * (1.0 + DimBorderBlur.w); 28 | vec2 uv = (pos * PosDim.zw) - (PosDim.zw * 0.5); 29 | 30 | float dist = rectangle(uv, PosDim.zw * 0.5, Corners); 31 | float alpha = linearstep(w, -w, dist); 32 | float s = linearstep(w, -w, dist + DimBorderBlur.z); 33 | 34 | // Output to screen 35 | //fragColor = vec4(dist,dist,dist,1); 36 | fragColor = (vec4(Fill.rgb, 1)*Fill.a*s) + (vec4(Outline.rgb, 1)*Outline.a*clamp(alpha - s, 0.0, 1.0)); 37 | } 38 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/RoundRect.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(1) 2 | var PosDim: vec4f; 3 | @group(0) @binding(2) 4 | var DimBorderBlur: vec4f; 5 | @group(0) @binding(3) 6 | var Corners: vec4f; 7 | @group(0) @binding(4) 8 | var Fill: vec4f; 9 | @group(0) @binding(5) 10 | var Outline: vec4f; 11 | 12 | fn linearstep(low: f32, high: f32, x: f32) -> f32 { return clamp((x - low) / (high - low), 0.0f, 1.0f); } 13 | 14 | fn rectangle(samplePosition: vec2f, halfSize: vec2f, edges: vec4f) -> f32 { 15 | var edge: f32 = 20.0f; 16 | if(samplePosition.x > 0.0f) { 17 | edge = select(edges.z, edges.y, samplePosition.y < 0.0f); 18 | } else { 19 | edge = select(edges.w, edges.x, samplePosition.y < 0.0f); 20 | } 21 | 22 | let componentWiseEdgeDistance = abs(samplePosition) - halfSize + vec2f(edge); 23 | let outsideDistance = length(max(componentWiseEdgeDistance, vec2f(0.0f))); 24 | let insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0.0f); 25 | return outsideDistance + insideDistance - edge; 26 | } 27 | 28 | @fragment 29 | fn main(@location(0) pos: vec2f) -> @location(0) vec4f { 30 | // Ideally we would get DPI for both height and width, but for now we just assume DPI isn't weird 31 | let w = fwidth(PosDim.z*pos.x) * 0.5f * (1.0f + DimBorderBlur.w); 32 | let uv = (pos * PosDim.zw) - (PosDim.zw * 0.5f); 33 | 34 | let dist = rectangle(uv, PosDim.zw * 0.5f, Corners); 35 | let alpha = linearstep(w, -w, dist); 36 | let s = linearstep(w, -w, dist + DimBorderBlur.z); 37 | 38 | return (vec4f(Fill.rgb, 1f)*Fill.a*s) + (vec4f(Outline.rgb, 1f)*Outline.a*clamp(alpha - s, 0.0f, 1.0f)); 39 | } 40 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Triangle.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | in vec2 pos; 3 | layout(binding=1) uniform vec4 PosDim; 4 | layout(binding=2) uniform vec4 DimBorderBlur; 5 | layout(binding=3) uniform vec4 Corners; 6 | layout(binding=4) uniform vec4 Fill; 7 | layout(binding=5) uniform vec4 Outline; 8 | out vec4 fragColor; 9 | 10 | float linearstep(float low, float high, float x) { return clamp((x - low) / (high - low), 0.0, 1.0); } 11 | float linetopoint(vec2 p1, vec2 p2, vec2 p) 12 | { 13 | vec2 n = p2 - p1; 14 | n = vec2(n.y, -n.x); 15 | return dot(normalize(n), p1 - p); 16 | } 17 | void main() 18 | { 19 | vec2 d = PosDim.zw; 20 | vec2 p = pos * d + vec2(-0.5,0.5); 21 | vec4 c = Corners; 22 | vec2 p2 = vec2(c.w*d.x, 0.0); 23 | float r1 = linetopoint(p2, vec2(0.0, d.y), p); 24 | float r2 = -linetopoint(p2, d, p); 25 | float r = max(r1, r2); 26 | r = max(r, p.y - d.y); 27 | 28 | // Ideally we would get DPI for both height and width, but for now we just assume DPI isn't weird 29 | float w = fwidth(p.x) * (1.0 + DimBorderBlur.w); 30 | float s = 1.0 - linearstep(1.0 - DimBorderBlur.z - w*2.0, 1.0 - DimBorderBlur.z - w, r); 31 | float alpha = linearstep(1.0 - w, 1.0 - w*2.0, r); 32 | fragColor = (vec4(Fill.rgb, 1.0)*Fill.a*s) + (vec4(Outline.rgb, 1.0)*Outline.a*clamp(alpha - s,0.0,1.0)); 33 | } 34 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/Triangle.wgsl: -------------------------------------------------------------------------------- 1 | struct FragmentOutput { 2 | @location(0) fragColor: vec4, 3 | } 4 | 5 | var pos_1: vec2; 6 | @group(0) @binding(1) 7 | var PosDim: vec4; 8 | @group(0) @binding(2) 9 | var DimBorderBlur: vec4; 10 | @group(0) @binding(3) 11 | var Corners: vec4; 12 | @group(0) @binding(4) 13 | var Fill: vec4; 14 | @group(0) @binding(5) 15 | var Outline: vec4; 16 | var fragColor: vec4; 17 | 18 | fn linearstep(low: f32, high: f32, x: f32) -> f32 { 19 | var low_1: f32; 20 | var high_1: f32; 21 | var x_1: f32; 22 | 23 | low_1 = low; 24 | high_1 = high; 25 | x_1 = x; 26 | let _e13 = x_1; 27 | let _e14 = low_1; 28 | let _e16 = high_1; 29 | let _e17 = low_1; 30 | let _e22 = x_1; 31 | let _e23 = low_1; 32 | let _e25 = high_1; 33 | let _e26 = low_1; 34 | return clamp(((_e22 - _e23) / (_e25 - _e26)), 0f, 1f); 35 | } 36 | 37 | fn linetopoint(p1_: vec2, p2_: vec2, p: vec2) -> f32 { 38 | var p1_1: vec2; 39 | var p2_1: vec2; 40 | var p_1: vec2; 41 | var n: vec2; 42 | 43 | p1_1 = p1_; 44 | p2_1 = p2_; 45 | p_1 = p; 46 | let _e13 = p2_1; 47 | let _e14 = p1_1; 48 | n = (_e13 - _e14); 49 | let _e17 = n; 50 | let _e19 = n; 51 | n = vec2(_e17.y, -(_e19.x)); 52 | let _e24 = n; 53 | let _e26 = p1_1; 54 | let _e27 = p_1; 55 | let _e30 = n; 56 | let _e32 = p1_1; 57 | let _e33 = p_1; 58 | return dot(normalize(_e30), (_e32 - _e33)); 59 | } 60 | 61 | fn main_1() { 62 | var d: vec2; 63 | var p_2: vec2; 64 | var c: vec4; 65 | var dist: vec2; 66 | var p2_2: vec2; 67 | var r1_: f32; 68 | var r2_: f32; 69 | var r: f32; 70 | var w: f32; 71 | var s: f32; 72 | var alpha: f32; 73 | 74 | let _e7 = PosDim; 75 | d = _e7.zw; 76 | let _e10 = pos_1; 77 | let _e11 = d; 78 | p_2 = ((_e10 * _e11) + vec2(-0.5f, 0.5f)); 79 | let _e19 = Corners; 80 | c = _e19; 81 | let _e22 = c; 82 | let _e24 = d; 83 | p2_2 = vec2((_e22.w * _e24.x), 0f); 84 | let _e32 = d; 85 | let _e36 = p2_2; 86 | let _e38 = d; 87 | let _e41 = p_2; 88 | let _e42 = linetopoint(_e36, vec2(0f, _e38.y), _e41); 89 | r1_ = _e42; 90 | let _e47 = p2_2; 91 | let _e48 = d; 92 | let _e49 = p_2; 93 | let _e50 = linetopoint(_e47, _e48, _e49); 94 | r2_ = -(_e50); 95 | let _e55 = r1_; 96 | let _e56 = r2_; 97 | r = max(_e55, _e56); 98 | let _e60 = p_2; 99 | let _e62 = d; 100 | let _e65 = r; 101 | let _e66 = p_2; 102 | let _e68 = d; 103 | r = max(_e65, (_e66.y - _e68.y)); 104 | let _e72 = p_2; 105 | let _e74 = p_2; 106 | let _e76 = fwidth(_e74.x); 107 | let _e78 = DimBorderBlur; 108 | w = (_e76 * (1f + _e78.w)); 109 | let _e85 = DimBorderBlur; 110 | let _e88 = w; 111 | let _e93 = DimBorderBlur; 112 | let _e96 = w; 113 | let _e100 = DimBorderBlur; 114 | let _e103 = w; 115 | let _e108 = DimBorderBlur; 116 | let _e111 = w; 117 | let _e113 = r; 118 | let _e114 = linearstep(((1f - _e100.z) - (_e103 * 2f)), ((1f - _e108.z) - _e111), _e113); 119 | s = (1f - _e114); 120 | let _e118 = w; 121 | let _e121 = w; 122 | let _e127 = w; 123 | let _e130 = w; 124 | let _e134 = r; 125 | let _e135 = linearstep((1f - _e127), (1f - (_e130 * 2f)), _e134); 126 | alpha = _e135; 127 | let _e137 = Fill; 128 | let _e138 = _e137.xyz; 129 | let _e144 = Fill; 130 | let _e147 = s; 131 | let _e149 = Outline; 132 | let _e150 = _e149.xyz; 133 | let _e156 = Outline; 134 | let _e159 = alpha; 135 | let _e160 = s; 136 | let _e164 = alpha; 137 | let _e165 = s; 138 | fragColor = (((vec4(_e138.x, _e138.y, _e138.z, 1f) * _e144.w) * _e147) + ((vec4(_e150.x, _e150.y, _e150.z, 1f) * _e156.w) * clamp((_e164 - _e165), 0f, 1f))); 139 | return; 140 | } 141 | 142 | @fragment 143 | fn main(@location(0) pos: vec2) -> FragmentOutput { 144 | pos_1 = pos; 145 | main_1(); 146 | let _e17 = fragColor; 147 | return FragmentOutput(_e17); 148 | } 149 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/standard.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 450 2 | layout(binding=0) uniform mat4 MVP; 3 | layout(binding=1) uniform vec4 PosDim; 4 | in vec2 vPos; 5 | out vec2 pos; 6 | 7 | void main() 8 | { 9 | pos = vPos.xy; 10 | mat4 move; 11 | move[0] = vec4(PosDim.z, 0,0,0); 12 | move[1] = vec4(0, PosDim.w,0,0); 13 | move[2] = vec4(0, 0,1,0); 14 | move[3] = vec4(PosDim.x + PosDim.z*0.5, PosDim.y + PosDim.w*0.5,0,1); 15 | gl_Position = MVP * move * vec4(pos.x - 0.5, pos.y - 0.5, 1, 1); 16 | } 17 | -------------------------------------------------------------------------------- /feather-ui/src/shaders/standard.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) 2 | var MVP: mat4x4f; 3 | @group(0) @binding(1) 4 | var PosDim: vec4f; 5 | 6 | struct VertexOutput { 7 | @location(0) pos: vec2f, 8 | @builtin(position) gl_Position: vec4f, 9 | } 10 | 11 | @vertex 12 | fn main(@location(0) vPos: vec2f) -> VertexOutput { 13 | var mv: mat4x4f; 14 | mv[0] = vec4f(PosDim.z, 0f,0f,0f); 15 | mv[1] = vec4f(0f, PosDim.w,0f,0f); 16 | mv[2] = vec4f(0f, 0f, 1f, 0f); 17 | mv[3] = vec4f(PosDim.x + PosDim.z*0.5f, PosDim.y + PosDim.w*0.5f,0f,1f); 18 | let outpos = MVP * mv * vec4(vPos.x - 0.5f, vPos.y - 0.5f, 1f, 1f); 19 | 20 | return VertexOutput(vPos.xy, outpos); 21 | } 22 | -------------------------------------------------------------------------------- /feather-ui/src/text.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | use std::sync::atomic::{AtomicUsize, Ordering}; 7 | 8 | use smallvec::SmallVec; 9 | use unicode_segmentation::GraphemeCursor; 10 | 11 | #[derive(Default, Debug)] 12 | pub struct EditObj { 13 | pub(crate) text: RefCell, 14 | count: AtomicUsize, 15 | cursor: AtomicUsize, 16 | select: AtomicUsize, // If there's a selection, this is different from cursor and points at the end. Can be less than cursor. 17 | } 18 | 19 | impl Clone for EditObj { 20 | fn clone(&self) -> Self { 21 | Self { 22 | text: self.text.clone(), 23 | count: self.count.load(Ordering::Relaxed).into(), 24 | cursor: self.cursor.load(Ordering::Relaxed).into(), 25 | select: self.select.load(Ordering::Relaxed).into(), 26 | } 27 | } 28 | } 29 | 30 | impl EditObj { 31 | pub fn new(text: String, cursor: (usize, usize)) -> Self { 32 | Self { 33 | text: text.into(), 34 | count: 0.into(), 35 | cursor: cursor.0.into(), 36 | select: cursor.1.into(), 37 | } 38 | } 39 | pub fn get_content(&self) -> std::cell::Ref<'_, String> { 40 | self.text.borrow() 41 | } 42 | pub fn set_content(&self, content: &str) { 43 | *self.text.borrow_mut() = content.into(); 44 | self.count.fetch_add(1, Ordering::Release); 45 | } 46 | pub fn edit( 47 | &self, 48 | multisplice: &[(std::ops::Range, String)], 49 | ) -> SmallVec<[(std::ops::Range, String); 1]> { 50 | let old: SmallVec<[(std::ops::Range, String); 1]> = if multisplice.len() == 1 { 51 | let (range, replace) = &multisplice[0]; 52 | let old = self.text.borrow()[range.clone()].to_string(); 53 | self.text.borrow_mut().replace_range(range.clone(), replace); 54 | [(range.start..replace.len(), old)].into() 55 | } else { 56 | // To preserve the validity of the ranges, we have to assemble the string piecewise 57 | let mut undo = SmallVec::new(); 58 | let mut last = 0; 59 | let s = { 60 | let mut pieces: Vec<&str> = Vec::new(); 61 | let txt = self.text.borrow(); 62 | for (range, replace) in multisplice { 63 | pieces.push(&txt[last..range.start]); 64 | pieces.push(replace); 65 | undo.push((range.start..replace.len(), txt[range.clone()].to_string())); 66 | last = range.end; 67 | } 68 | 69 | pieces.push(&txt[last..]); 70 | pieces.join("") 71 | }; 72 | self.text.replace(s); 73 | undo 74 | }; 75 | self.count.fetch_add(1, Ordering::Release); 76 | old 77 | } 78 | 79 | pub fn get_cursor(&self) -> (usize, usize) { 80 | ( 81 | self.cursor.load(Ordering::Relaxed), 82 | self.select.load(Ordering::Relaxed), 83 | ) 84 | } 85 | 86 | pub fn set_cursor(&self, cursor: usize) { 87 | self.cursor.store(cursor, Ordering::Release); 88 | self.select.store(cursor, Ordering::Release); 89 | self.count.fetch_add(1, Ordering::Release); 90 | } 91 | pub fn set_selection(&self, cursor: (usize, usize)) { 92 | self.cursor.store(cursor.0, Ordering::Release); 93 | self.select.store(cursor.1, Ordering::Release); 94 | self.count.fetch_add(1, Ordering::Release); 95 | } 96 | 97 | pub(crate) fn next_grapheme(&self, byte_offset: usize) -> usize { 98 | let txt = self.text.borrow(); 99 | let mut cursor = GraphemeCursor::new(byte_offset, txt.len(), true); 100 | assert!(cursor.is_boundary(&txt, 0).is_ok_and(|v| v)); 101 | match cursor.next_boundary(&txt, 0) { 102 | Ok(Some(x)) => x, 103 | Ok(None) => txt.len(), 104 | Err(_) => byte_offset, 105 | } 106 | } 107 | 108 | pub(crate) fn prev_grapheme(&self, byte_offset: usize) -> usize { 109 | let txt = self.text.borrow(); 110 | let mut cursor = GraphemeCursor::new(byte_offset, txt.len(), true); 111 | assert!(cursor.is_boundary(&txt, 0).is_ok_and(|v| v)); 112 | match cursor.prev_boundary(&txt, 0) { 113 | Ok(Some(x)) => x, 114 | Ok(None) => 0, 115 | Err(_) => byte_offset, 116 | } 117 | } 118 | } 119 | 120 | #[derive(Default, Debug)] 121 | pub struct Snapshot { 122 | pub(crate) obj: Rc, 123 | count: usize, 124 | } 125 | 126 | impl Snapshot { 127 | pub fn get(&self) -> &EditObj { 128 | &self.obj 129 | } 130 | } 131 | 132 | // Ensures each clone gets a fresh snapshot to capture changes 133 | impl Clone for Snapshot { 134 | fn clone(&self) -> Self { 135 | Self { 136 | obj: self.obj.clone(), 137 | count: self.obj.count.load(Ordering::Acquire), 138 | } 139 | } 140 | } 141 | 142 | impl Eq for Snapshot {} 143 | impl PartialEq for Snapshot { 144 | fn eq(&self, other: &Self) -> bool { 145 | self.count == other.count && Rc::ptr_eq(&self.obj, &other.obj) 146 | } 147 | } 148 | 149 | impl PartialOrd for Snapshot { 150 | fn partial_cmp(&self, other: &Self) -> Option { 151 | if Rc::ptr_eq(&self.obj, &other.obj) { 152 | self.count.partial_cmp(&other.count) 153 | } else { 154 | None 155 | } 156 | } 157 | } 158 | 159 | impl From> for Snapshot { 160 | fn from(value: Rc) -> Self { 161 | Self { 162 | obj: value.clone(), 163 | count: value.count.load(std::sync::atomic::Ordering::Acquire), 164 | } 165 | } 166 | } 167 | 168 | impl From for Snapshot { 169 | fn from(value: EditObj) -> Self { 170 | let value = Rc::new(value); 171 | Self { 172 | obj: value.clone(), 173 | count: value.count.load(std::sync::atomic::Ordering::Acquire), 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "advisory-db": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1747256151, 7 | "narHash": "sha256-9G1MB6X/mMPDJ/YSEoyk2YSdDM6geQeum8TD67pBwEY=", 8 | "owner": "rustsec", 9 | "repo": "advisory-db", 10 | "rev": "982c2320aa55b3095110a0b0eadd446d83be45f9", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "rustsec", 15 | "repo": "advisory-db", 16 | "type": "github" 17 | } 18 | }, 19 | "crane": { 20 | "locked": { 21 | "lastModified": 1747260204, 22 | "narHash": "sha256-KUb6MFWc2DYeTCmcEkrBrrqhxAgO6NHZh5qQKwsjG6I=", 23 | "owner": "ipetkov", 24 | "repo": "crane", 25 | "rev": "7f85510df37247c86a0c44032f49aa18292ee11f", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "ipetkov", 30 | "repo": "crane", 31 | "type": "github" 32 | } 33 | }, 34 | "flake-utils": { 35 | "inputs": { 36 | "systems": "systems" 37 | }, 38 | "locked": { 39 | "lastModified": 1731533236, 40 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 41 | "owner": "numtide", 42 | "repo": "flake-utils", 43 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "type": "github" 50 | } 51 | }, 52 | "nixpkgs": { 53 | "locked": { 54 | "lastModified": 1747209494, 55 | "narHash": "sha256-fLise+ys+bpyjuUUkbwqo5W/UyIELvRz9lPBPoB0fbM=", 56 | "owner": "NixOS", 57 | "repo": "nixpkgs", 58 | "rev": "5d736263df906c5da72ab0f372427814de2f52f8", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "NixOS", 63 | "ref": "nixos-24.11", 64 | "repo": "nixpkgs", 65 | "type": "github" 66 | } 67 | }, 68 | "nixpkgs_2": { 69 | "locked": { 70 | "lastModified": 1744536153, 71 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 72 | "owner": "NixOS", 73 | "repo": "nixpkgs", 74 | "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "NixOS", 79 | "ref": "nixpkgs-unstable", 80 | "repo": "nixpkgs", 81 | "type": "github" 82 | } 83 | }, 84 | "root": { 85 | "inputs": { 86 | "advisory-db": "advisory-db", 87 | "crane": "crane", 88 | "flake-utils": "flake-utils", 89 | "nixpkgs": "nixpkgs", 90 | "rust-overlay": "rust-overlay" 91 | } 92 | }, 93 | "rust-overlay": { 94 | "inputs": { 95 | "nixpkgs": "nixpkgs_2" 96 | }, 97 | "locked": { 98 | "lastModified": 1747363019, 99 | "narHash": "sha256-N4dwkRBmpOosa4gfFkFf/LTD8oOcNkAyvZ07JvRDEf0=", 100 | "owner": "oxalica", 101 | "repo": "rust-overlay", 102 | "rev": "0e624f2b1972a34be1a9b35290ed18ea4b419b6f", 103 | "type": "github" 104 | }, 105 | "original": { 106 | "owner": "oxalica", 107 | "repo": "rust-overlay", 108 | "type": "github" 109 | } 110 | }, 111 | "systems": { 112 | "locked": { 113 | "lastModified": 1681028828, 114 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 115 | "owner": "nix-systems", 116 | "repo": "default", 117 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "nix-systems", 122 | "repo": "default", 123 | "type": "github" 124 | } 125 | } 126 | }, 127 | "root": "root", 128 | "version": 7 129 | } 130 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # SPDX-FileCopyrightText: 2025 Fundament Software SPC 3 | 4 | { 5 | inputs = { 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 7 | rust-overlay.url = "github:oxalica/rust-overlay"; 8 | flake-utils.url = "github:numtide/flake-utils"; 9 | 10 | crane.url = "github:ipetkov/crane"; 11 | advisory-db = { 12 | url = "github:rustsec/advisory-db"; 13 | flake = false; 14 | }; 15 | }; 16 | 17 | outputs = 18 | inputs@{ self 19 | , flake-utils 20 | , nixpkgs 21 | , rust-overlay 22 | , crane 23 | , advisory-db 24 | , ... 25 | }: 26 | flake-utils.lib.eachSystem [ flake-utils.lib.system.x86_64-linux ] (system: 27 | let 28 | overlays = [ (import rust-overlay) ]; 29 | pkgs = import nixpkgs { inherit system overlays; }; 30 | 31 | rust-custom-toolchain = (pkgs.rust-bin.stable.latest.default.override { 32 | extensions = [ 33 | "rust-src" 34 | "rustfmt" 35 | "llvm-tools-preview" 36 | "rust-analyzer-preview" 37 | ]; 38 | }); 39 | impureDrivers = [ 40 | "/run/opengl-driver" # impure deps on specific GPU, mesa, vulkan loader, radv, nvidia proprietary etc 41 | ]; 42 | gfxDeps = [ 43 | pkgs.xorg.libxcb 44 | pkgs.xorg.libX11 45 | pkgs.xorg.libXcursor 46 | pkgs.xorg.libXrandr 47 | pkgs.xorg.libXi 48 | pkgs.libxkbcommon 49 | pkgs.wayland 50 | pkgs.fontconfig # you probably need this? unless you're ignoring system font config entirely 51 | # pkgs.libGL/U # should be in /run/opengl-driver? 52 | pkgs.pkg-config # let things detect packages at build time 53 | # some toolkits use these for dialogs - probably not relevant for feather? 54 | # pkgs.kdialog 55 | # pkgs.yad 56 | pkgs.vulkan-loader 57 | #pkgs.libglvnd 58 | ]; 59 | in 60 | rec { 61 | devShells.default = 62 | (pkgs.mkShell.override { stdenv = pkgs.llvmPackages.stdenv; }) { 63 | buildInputs = with pkgs; [ openssl pkg-config dotnet-sdk ] ++ gfxDeps; 64 | 65 | nativeBuildInputs = with pkgs; [ 66 | # get current rust toolchain defaults (this includes clippy and rustfmt) 67 | rust-custom-toolchain 68 | 69 | cargo-edit 70 | ]; 71 | 72 | #LD_LIBRARY_PATH = pkgs.lib.strings.concatMapStringsSep ":" toString (with pkgs; [ xorg.libX11 xorg.libXcursor xorg.libXi (libxkbcommon + "/lib") (vulkan-loader + "/lib") libglvnd ]); 73 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (impureDrivers ++ gfxDeps); 74 | # fetch with cli instead of native 75 | CARGO_NET_GIT_FETCH_WITH_CLI = "true"; 76 | RUST_BACKTRACE = 1; 77 | RUSTFLAGS = "-C linker=clang -C link-arg=-fuse-ld=${pkgs.mold}/bin/mold -C link-arg=-flto=thin"; 78 | }; 79 | 80 | checks = 81 | let 82 | craneLib = 83 | (inputs.crane.mkLib pkgs).overrideToolchain rust-custom-toolchain; 84 | commonArgs = { 85 | src = ./.; 86 | buildInputs = with pkgs; [ pkg-config openssl zlib ]; 87 | strictDeps = true; 88 | version = "0.1.0"; 89 | stdenv = pkgs: pkgs.stdenvAdapters.useMoldLinker pkgs.llvmPackages_15.stdenv; 90 | CARGO_BUILD_RUSTFLAGS = "-C linker=clang -C link-arg=-fuse-ld=${pkgs.mold}/bin/mold -C link-arg=-flto=thin"; 91 | }; 92 | pname = "feather-checks"; 93 | 94 | cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { 95 | inherit pname; 96 | }); 97 | build-tests = craneLib.buildPackage (commonArgs // { 98 | inherit cargoArtifacts pname; 99 | cargoTestExtraArgs = "--no-run"; 100 | }); 101 | in 102 | { 103 | inherit build-tests; 104 | 105 | # Run clippy (and deny all warnings) on the crate source, 106 | # again, reusing the dependency artifacts from above. 107 | # 108 | # Note that this is done as a separate derivation so that 109 | # we can block the CI if there are issues here, but not 110 | # prevent downstream consumers from building our crate by itself. 111 | feather-clippy = craneLib.cargoClippy (commonArgs // { 112 | inherit cargoArtifacts; 113 | pname = "${pname}-clippy"; 114 | cargoClippyExtraArgs = "-- --deny warnings"; 115 | }); 116 | 117 | # Check formatting 118 | feather-fmt = craneLib.cargoFmt (commonArgs // { 119 | pname = "${pname}-fmt"; 120 | }); 121 | 122 | # Audit dependencies 123 | feather-audit = craneLib.cargoAudit (commonArgs // { 124 | pname = "${pname}-audit"; 125 | advisory-db = inputs.advisory-db; 126 | cargoAuditExtraArgs = "--ignore RUSTSEC-2020-0071"; 127 | }); 128 | 129 | # We can't run tests during nix flake check because it might not have a graphical device. 130 | }; 131 | }); 132 | } 133 | --------------------------------------------------------------------------------