├── .cargo └── config.toml ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── assets ├── animations.css ├── buttons.css ├── dynamic.css ├── fonts │ ├── Kenney Space.ttf │ ├── OFL.txt │ ├── Poppins-Regular.ttf │ └── Poppins-SemiBold.ttf ├── game_menu.css ├── grid.css ├── panel-border-010.png ├── panel-border-030.png └── tailwind-colors.css ├── benches ├── animations.css ├── basic.css ├── flair_benches.rs └── transitions.css ├── bin └── flamegraph.rs ├── crates ├── bevy_flair_core │ ├── Cargo.toml │ └── src │ │ ├── component_property.rs │ │ ├── lib.rs │ │ ├── property_map.rs │ │ ├── property_value.rs │ │ ├── reflect_value.rs │ │ ├── registry.rs │ │ ├── static_type_info.rs │ │ └── sub_properties.rs ├── bevy_flair_css_parser │ ├── Cargo.toml │ └── src │ │ ├── calc.rs │ │ ├── error.rs │ │ ├── error_codes.rs │ │ ├── imports_parser.rs │ │ ├── lib.rs │ │ ├── loader.rs │ │ ├── parser.rs │ │ ├── reflect.rs │ │ ├── reflect │ │ ├── assets.rs │ │ ├── color.rs │ │ ├── enums.rs │ │ ├── grid.rs │ │ ├── image.rs │ │ ├── text.rs │ │ └── ui.rs │ │ ├── shorthand.rs │ │ ├── utils.rs │ │ └── vars.rs └── bevy_flair_style │ ├── Cargo.toml │ └── src │ ├── animations │ ├── curves.rs │ ├── easing.rs │ ├── mod.rs │ └── reflect.rs │ ├── builder.rs │ ├── components.rs │ ├── css_selector │ ├── element.rs │ ├── error.rs │ ├── mod.rs │ └── testing.rs │ ├── layers.rs │ ├── lib.rs │ ├── media_selector.rs │ ├── snapshots │ ├── bevy_flair_style__tests__append_child_after_initial_spawn.snap │ ├── bevy_flair_style__tests__append_grand_children_after_initial_spawn.snap │ ├── bevy_flair_style__tests__spawn_many_children.snap │ └── bevy_flair_style__tests__spawn_many_children_and_grandchildren.snap │ ├── style_sheet.rs │ ├── systems.rs │ ├── testing.rs │ └── vars.rs ├── examples ├── animations.rs ├── buttons.rs ├── dynamic.rs ├── game_menu.rs └── grid.rs ├── src └── lib.rs └── tests ├── all_properties.rs ├── common └── mod.rs ├── css ├── _import_1.css ├── _import_2.css ├── _imported_layers.css ├── all_properties.css ├── imports.css ├── layers.css ├── media_queries.css └── selectors.css ├── imports.rs ├── layers.rs ├── media_queries.rs └── selectors.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = "wasm-server-runner" -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | # Run cargo check --workspace --all-targets -- -D warnings 14 | check: 15 | strategy: 16 | matrix: 17 | include: 18 | - name: Linux x86_64 19 | os: ubuntu-latest 20 | target: x86_64-unknown-linux-gnu 21 | - name: Wasm 22 | os: ubuntu-latest 23 | target: wasm32-unknown-unknown 24 | - name: MacOS aarch64 25 | os: macos-14 26 | target: aarch64-apple-darwin 27 | 28 | name: Check ${{ matrix.name }} 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v4 33 | - name: Cache 34 | uses: actions/cache@v3 35 | with: 36 | path: | 37 | ~/.cargo/bin/ 38 | ~/.cargo/registry/index/ 39 | ~/.cargo/registry/cache/ 40 | ~/.cargo/git/db/ 41 | target/ 42 | key: ${{ runner.os }}-${{ matrix.target }}-cargo-check-${{ hashFiles('**/Cargo.toml') }} 43 | - name: Install stable toolchain 44 | uses: dtolnay/rust-toolchain@stable 45 | with: 46 | targets: ${{ matrix.target }} 47 | - name: Install ubuntu deps 48 | if: ${{ matrix.os == 'ubuntu-latest' }} 49 | run: sudo apt-get install -y libudev-dev 50 | - name: cargo check 51 | run: cargo check --workspace --target ${{ matrix.target }} 52 | - name: cargo check --release 53 | run: cargo check --release --workspace --target ${{ matrix.target }} 54 | 55 | # Run cargo clippy --workspace --all-targets -- -D warnings 56 | clippy: 57 | name: Clippy 58 | needs: check 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout sources 62 | uses: actions/checkout@v4 63 | - name: Cache 64 | uses: actions/cache@v3 65 | with: 66 | path: | 67 | ~/.cargo/bin/ 68 | ~/.cargo/registry/index/ 69 | ~/.cargo/registry/cache/ 70 | ~/.cargo/git/db/ 71 | target/ 72 | key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.toml') }} 73 | - name: Install stable toolchain 74 | uses: dtolnay/rust-toolchain@stable 75 | with: 76 | components: clippy 77 | - name: Install ubuntu deps 78 | run: sudo apt-get install -y libudev-dev 79 | - name: cargo clippy 80 | run: cargo clippy --workspace --all-targets -- -D warnings 81 | - name: cargo clippy --release 82 | run: cargo clippy --release --workspace --all-targets -- -D warnings 83 | 84 | # Run cargo fmt --all -- --check 85 | fmt: 86 | name: Format 87 | runs-on: ubuntu-latest 88 | steps: 89 | - name: Checkout sources 90 | uses: actions/checkout@v4 91 | - name: Install stable toolchain 92 | uses: dtolnay/rust-toolchain@stable 93 | with: 94 | components: rustfmt 95 | - name: cargo fmt 96 | run: cargo fmt --all -- --check 97 | 98 | # Run cargo doc --workspace --no-deps --examples --all-features --document-private-items 99 | doc: 100 | name: Doc check 101 | runs-on: ubuntu-latest 102 | steps: 103 | - name: Checkout sources 104 | uses: actions/checkout@v4 105 | - name: Install stable toolchain 106 | uses: dtolnay/rust-toolchain@stable 107 | - name: Install ubuntu deps 108 | run: sudo apt-get install -y libudev-dev 109 | - name: Run cargo doc 110 | run: cargo doc --workspace --no-deps --examples --all-features --document-private-items 111 | env: 112 | RUSTDOCFLAGS: "-D warnings" 113 | 114 | # Run cargo test --workspace 115 | test: 116 | name: Test 117 | runs-on: ubuntu-latest 118 | steps: 119 | - name: Checkout sources 120 | uses: actions/checkout@v4 121 | - name: Cache 122 | uses: actions/cache@v3 123 | with: 124 | path: | 125 | ~/.cargo/bin/ 126 | ~/.cargo/registry/index/ 127 | ~/.cargo/registry/cache/ 128 | ~/.cargo/git/db/ 129 | target/ 130 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 131 | - name: Install stable toolchain 132 | uses: dtolnay/rust-toolchain@stable 133 | - name: Install ubuntu deps 134 | run: sudo apt-get install -y libudev-dev 135 | - name: cargo test 136 | run: cargo test --workspace 137 | # Run cargo check --all-targets using MSRV 138 | check-msrv: 139 | runs-on: ubuntu-latest 140 | needs: check 141 | steps: 142 | - uses: actions/checkout@v4 143 | - uses: actions/cache@v4 144 | with: 145 | path: | 146 | ~/.cargo/bin/ 147 | ~/.cargo/registry/index/ 148 | ~/.cargo/registry/cache/ 149 | ~/.cargo/git/db/ 150 | target/ 151 | key: ${{ runner.os }}-cargo-check-msrv-${{ hashFiles('**/Cargo.toml') }} 152 | - uses: dtolnay/rust-toolchain@stable 153 | - name: get MSRV 154 | id: msrv 155 | run: | 156 | msrv=`cargo metadata --no-deps --format-version 1 | jq --raw-output '.packages[] | select(.name=="bevy_flair") | .rust_version'` 157 | echo "msrv=$msrv" >> $GITHUB_OUTPUT 158 | - uses: dtolnay/rust-toolchain@master 159 | with: 160 | toolchain: ${{ steps.msrv.outputs.msrv }} 161 | - name: Install ubuntu deps 162 | run: sudo apt-get install -y libudev-dev 163 | - name: cargo check 164 | run: cargo check --all-targets 165 | - name: cargo check --release 166 | run: cargo check --release --all-targets 167 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | #.idea/ 22 | 23 | # Added by cargo 24 | 25 | /target 26 | 27 | rust-toolchain.toml 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3] - 16-May-2025 9 | 10 | ### Added 11 | 12 | - Support for `@media` queries. 13 | - The following properties are supported: `prefers-color-scheme`, `width`, `height`, `resolution`, `aspect-ratio`. 14 | - Support for the css properties: 15 | - `grid` 16 | - `gap` 17 | - `grid-template` 18 | - `place-items` 19 | - `text-shadow` 20 | - `overflow-clip-margin` 21 | - `aspect-ratio` 22 | - `line-height` 23 | - `font-smooth` 24 | - `text-align` 25 | - `-bevy-line-break` 26 | - Adding transition events. New events are emitted when a transition starts, ends or gets replaced. 27 | - Support for `initial` property value. 28 | - Things like `flex: initial` should work. 29 | 30 | ### Fixed 31 | - Some properties were not supporting `inherit`. Now all properties do support it. 32 | 33 | ### Changed 34 | - `bevy_flair` doesn't depend on `bevy` crate, now it depends directly on individual crates, like `bevy_ui`. 35 | - Color interpolation happens in Oklab color space, independently of the defined color space. 36 | - Removed `SimpleSelector`. Use `CssSelector`. 37 | - All non-standard css properties are prefixed with `-bevy`. 38 | - For example `background-image-mode` now is called `-bevy-image-mode` 39 | 40 | ## [0.2] - 30-Apr-2025 41 | 42 | ### Added 43 | 44 | - Support for inherited properties (e.g. `color`, `font-family` are inherited by default). 45 | - Fancy selectors like `:not()`, `:has()`, `:is()` and `:where()`. 46 | - Import other stylesheets using `@import`. 47 | - Nested selectors are supported. 48 | - You can add `&:hover { .. }` inside a selector and it will work. 49 | - Support for custom properties using `var()`. 50 | - Fallback is currently not supported 51 | - Basic support for calc expressions using `calc()`. 52 | - This is currently limited by Bevy support of mixing different types. For example, this cannot not work currently: `calc(100% - 20px)`. 53 | - Currently, is valuable to do calculations using vars. For example: `calc(var(--spacing) * 2)`. 54 | - Support for some shorthand properties not previously supported like `border` or `flex`. 55 | 56 | ### Fixed 57 | - `:nth-child()` type selectors are correctly re-calculated when a sibling is added. 58 | 59 | ### Changed 60 | - Support for Bevy 0.16 61 | 62 | ## [0.1] - 24-Jan-2025 63 | 64 | Bevy Flair enables developers to style Bevy UI interfaces using familiar CSS syntax. 65 | 66 | With Bevy Flair, you can define the appearance and layout of Bevy UI components efficiently, leveraging the simplicity and power of CSS. 67 | 68 | ## Added 69 | 70 | - Use CSS assets to apply format to any Bevy UI element. 71 | - Inherited stylesheets. Just specify the stylesheet using [`NodeStyleSheet`] in the root node, and all children will inherit the same stylesheet. 72 | - Support for css reloading. Just edit your .css files and the styles will be re-applied on the fly. This is one of the main advantages over specifying the styles directly. 73 | - Almost All existing UI components and properties are supported: 74 | - All [`Node`] properties are supported. 75 | - Components [`BorderColor`], [`BackgroundColor`], [`BorderRadius`], [`Outline`], [`BoxShadow`] and [`ZIndex`] 76 | are supported, and inserted automatically when the corresponding property is used (e.g. using `background-color: red` will automatically insert the [`BackgroundColor`] component ) 77 | - Use of non-standard css to support [`ImageNode`] (e.g: `image-texture: url("panel-border-030.png")`, `image-mode: sliced(20.0px)`). 78 | - Color parsing. (e.g. `red`,`#ff0000`,`rgb(255 0 0)`,`hsl(0 100% 50% / 50%)`,`oklch(40.1% 0.123 21.57)`) 79 | - Most common css selectors works by default (Thanks to [selectors] crate). 80 | - `:root` selector 81 | - `#id` selector. Works by using the [`Name`] component 82 | - `.class` selector. Works by using the [`ClassList`] component 83 | - `Type` selector. Works by using the [`TrackTypeNameComponentPlugin`] plugin configured by default to track: [`Node`], [`Button`], [`Label`] and [`Text`], 84 | - `:hover`, `:active` and `:focus` pseudo class selectors. `:hover` and `:active` are automatically tracked for buttons. 85 | - `:nth-child(_)`, `:first-child` works just fine. e.g: `:nth_child(2n + 1)` 86 | - Most common css selector combinators (Thanks to [selectors] crate): 87 | - Descendant: `ul.my-things li`. 88 | - Child: `ul.my-things > li`. 89 | - Next sibling: `img + p`. 90 | - Subsequent-sibling: `img ~ p`. 91 | - Font loading support using [`@font-face`]. 92 | - Animated property changes using [`transition`]. 93 | - Custom animations using [`@keyframes`]. 94 | - Different stylesheets per subtree. With the use of a different [`NodeStyleSheet`] per subtree. It's even possible to not apply any style for a given subtree. 95 | - Supports for custom properties. (Example TBA) 96 | - Supports for custom parsing. (Example TBA) 97 | 98 | [`Node`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.Node.html 99 | [`ImageNode`]: https://docs.rs/bevy/0.15.1/bevy/ui/widget/struct.ImageNode.html 100 | [`Button`]: https://docs.rs/bevy/0.15.1/bevy/ui/widget/struct.Button.html 101 | [`Label`]: https://docs.rs/bevy/0.15.1/bevy/ui/widget/struct.Label.html 102 | [`Text`]: https://docs.rs/bevy/0.15.1/bevy/ui/widget/struct.Text.html 103 | [`Name`]: https://docs.rs/bevy/0.15.1/bevy/core/struct.Name.html 104 | [`BorderColor`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.BorderColor.html 105 | [`BackgroundColor`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.BackgroundColor.html 106 | [`BorderRadius`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.BorderRadius.html 107 | [`Outline`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.Outline.html 108 | [`BoxShadow`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.BoxShadow.html 109 | [`ZIndex`]: https://docs.rs/bevy/0.15.1/bevy/ui/struct.ZIndex.html 110 | [`ClassList`]: https://docs.rs/bevy_flair/latest/bevy_flair/style/components/struct.ClassList.html 111 | [`TrackTypeNameComponentPlugin`]: https://docs.rs/bevy_flair/latest/bevy_flair/style/struct.TrackTypeNameComponentPlugin.html 112 | [`NodeStyleSheet`]: https://docs.rs/bevy_flair/latest/bevy_flair/style/components/enum.NodeStyleSheet.html 113 | [selectors]: https://crates.io/crates/selectors 114 | [`transition`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transition 115 | [`@keyframes`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes 116 | [`@font-face`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_flair" 3 | version = "0.3.0" 4 | edition = "2024" 5 | description = "Bevy UI styling using CSS" 6 | license = "MIT OR Apache-2.0" 7 | categories = ["games", "game-development"] 8 | keywords = ["style", "animations", "bevy", "css"] 9 | repository = "https://github.com/eckz/bevy_flair" 10 | rust-version = "1.85.0" 11 | 12 | [lib] 13 | bench = false 14 | 15 | [workspace] 16 | members = ["crates/*"] 17 | resolver = "2" 18 | 19 | [workspace.dependencies] 20 | bevy_app = { version = "0.16.0" } 21 | bevy_asset = { version = "0.16.0" } 22 | bevy_color = { version = "0.16.0" } 23 | bevy_ecs = { version = "0.16.0" } 24 | bevy_image = { version = "0.16.0" } 25 | bevy_input_focus = { version = "0.16.0" } 26 | bevy_math = { version = "0.16.0" } 27 | bevy_reflect = { version = "0.16.0" } 28 | bevy_render = { version = "0.16.0" } 29 | bevy_scene = { version = "0.16.0" } 30 | bevy_text = { version = "0.16.0" } 31 | bevy_time = { version = "0.16.0" } 32 | bevy_ui = { version = "0.16.0" } 33 | bevy_utils = { version = "0.16.0" } 34 | bevy_window = { version = "0.16.0" } 35 | 36 | bevy_flair_core = { path = "crates/bevy_flair_core", version = "0.3.0" } 37 | bevy_flair_style = { path = "crates/bevy_flair_style", version = "0.3.0" } 38 | bevy_flair_css_parser = { path = "crates/bevy_flair_css_parser", version = "0.3.0" } 39 | 40 | cssparser = { version = "0.35" } 41 | cssparser-color = { version = "0.3" } 42 | selectors = { version = "0.28" } 43 | 44 | [dependencies] 45 | bevy_app = { workspace = true } 46 | bevy_asset = { workspace = true } 47 | bevy_ecs = { workspace = true } 48 | 49 | bevy_flair_core = { workspace = true } 50 | bevy_flair_style = { workspace = true } 51 | bevy_flair_css_parser = { workspace = true } 52 | 53 | [dev-dependencies] 54 | criterion = { version = "0.5", default-features = false, features = [ "plotters", "cargo_bench_support","html_reports" ] } 55 | bevy = { version = "0.16", default-features = false, features = [ 56 | "std", 57 | "bevy_ui", 58 | "bevy_text", 59 | "bevy_log", 60 | "bevy_picking", 61 | "bevy_input_focus", 62 | "bevy_ui_picking_backend", 63 | "bevy_winit", 64 | "bevy_gilrs", 65 | "png", 66 | "bevy_dev_tools", 67 | "default_font", 68 | "x11" 69 | ] } 70 | 71 | [features] 72 | default = [] 73 | 74 | [[bench]] 75 | name = "flair_benches" 76 | harness = false 77 | 78 | [profile.dev.package."*"] 79 | opt-level = 1 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Erick Z 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | - AnimationEvents 3 | - TransitionEvents 4 | - animation-play-state: `paused | running` 5 | - Error if an animation contains < 2 keyframes 6 | - :hover / :active does not work on any element, only Buttons 7 | - `@layer` 8 | -------------------------------------------------------------------------------- /assets/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes box-animation { 2 | from { 3 | width: 15vw; 4 | height: 15vh; 5 | } 6 | 7 | to { 8 | width: 40vw; 9 | height: 40vh; 10 | } 11 | } 12 | 13 | @keyframes red-animation { 14 | from { 15 | background-color: rgb(15%, 15%, 15%); 16 | } 17 | 18 | to { 19 | background-color: red; 20 | } 21 | } 22 | 23 | @keyframes green-animation { 24 | from { 25 | background-color: rgb(15%, 15%, 15%); 26 | } 27 | 28 | to { 29 | background-color: green; 30 | } 31 | } 32 | 33 | @keyframes blue-animation { 34 | from { 35 | background-color: rgb(15%, 15%, 15%); 36 | } 37 | 38 | to { 39 | background-color: blue; 40 | } 41 | } 42 | 43 | @keyframes yellow-animation { 44 | from { 45 | background-color: rgb(15%, 15%, 15%); 46 | } 47 | 48 | to { 49 | background-color: yellow; 50 | } 51 | } 52 | 53 | :root { 54 | width: 100%; 55 | height: 100%; 56 | } 57 | 58 | .box { 59 | position: absolute; 60 | width: 20vw; 61 | height: 20vh; 62 | border: 4px black; 63 | } 64 | 65 | .box:nth-child(1) { 66 | left: 5vw; 67 | top: 5vh; 68 | 69 | animation: 3s ease-in-out 5 red-animation; 70 | } 71 | 72 | .box:nth-child(2) { 73 | right: 5vw; 74 | top: 5vh; 75 | 76 | animation: 3s ease-in-out 4 alternate-reverse blue-animation; 77 | } 78 | 79 | 80 | .box:nth-child(3) { 81 | left: 5vw; 82 | bottom: 5vh; 83 | 84 | animation: 3s linear infinite alternate yellow-animation; 85 | } 86 | 87 | .box:nth-child(4) { 88 | right: 5vw; 89 | bottom: 5vh; 90 | 91 | animation: 3s steps(5, end) infinite alternate green-animation; 92 | } 93 | 94 | .box.animated { 95 | animation: 3s ease infinite alternate box-animation; 96 | } -------------------------------------------------------------------------------- /assets/buttons.css: -------------------------------------------------------------------------------- 1 | @import "tailwind-colors.css"; 2 | 3 | @font-face { 4 | font-family: "Poppins-Regular"; 5 | src: url("fonts/Poppins-Regular.ttf"); 6 | } 7 | 8 | /* Define our own variables */ 9 | :root { 10 | --background-color: var(--color-zinc-50); 11 | --text-color: var(--color-gray-800); 12 | --font-size: 35px; 13 | 14 | --button-bg: var(--color-zinc-100); 15 | --button-border: var(--color-zinc-400); 16 | 17 | --button-hover-bg: var(--color-indigo-400); 18 | --button-hover-border: var(--color-indigo-600); 19 | --button-hover-text: var(--color-white); 20 | 21 | --button-active-bg: var(--color-red-400); 22 | --button-active-border: var(--color-red-600); 23 | 24 | --button-border-radius: 5px; 25 | } 26 | 27 | @media (prefers-color-scheme: dark) { 28 | :root { 29 | --background-color: var(--color-neutral-800); 30 | --text-color: var(--color-gray-100); 31 | 32 | --button-bg: var(--color-neutral-600); 33 | --button-border: var(--color-neutral-900); 34 | } 35 | } 36 | 37 | :root { 38 | display: flex; 39 | width: 100%; 40 | height: 100%; 41 | align-items: center; 42 | justify-content: center; 43 | flex-direction: column; 44 | 45 | font-family: "Poppins-Regular"; 46 | font-size: var(--font-size); 47 | color: var(--text-color); 48 | background-color: var(--background-color); 49 | 50 | transition: background-color 0.3s; 51 | } 52 | 53 | Button { 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | border-width: 5px; 58 | margin: 15px 0; 59 | 60 | width: 200px; 61 | max-width: 90%; 62 | height: 65px; 63 | 64 | @media (min-width: 600px) { 65 | width: 300px; 66 | } 67 | 68 | @media (min-width: 800px) and (min-height: 600px) { 69 | width: 400px; 70 | height: 80px; 71 | } 72 | 73 | background-color: var(--button-bg); 74 | border-color: var(--button-border); 75 | border-radius: var(--button-border-radius); 76 | 77 | transition: border-radius 0.6s ease-in-out, border-color 0.4s, background-color 0.4s, outline-color 0.4s, width 0.6s, height 0.6s; 78 | 79 | outline: initial; 80 | 81 | &:hover { 82 | background-color: var(--button-hover-bg); 83 | border-color: var(--button-hover-border); 84 | } 85 | 86 | &:active { 87 | background-color: var(--button-active-bg); 88 | border-color: var(--button-active-border); 89 | } 90 | &:hover, &:active { 91 | color: var(--button-hover-text); 92 | border-radius: calc(var(--button-border-radius) + 5px); 93 | } 94 | 95 | &:focus-visible { 96 | outline: 3px red; 97 | } 98 | 99 | Text { 100 | transition: color 0.4s; 101 | } 102 | } 103 | 104 | Button.dark-light-button { 105 | position: absolute; 106 | top: 25px; 107 | right: 25px; 108 | margin: 0; 109 | 110 | width: 80px; 111 | height: 40px; 112 | 113 | font-size: calc(var(--font-size) * 0.6); 114 | border-width: 3px; 115 | 116 | &:hover, &:active { 117 | color: var(--text-color); 118 | background-color: var(--button-bg); 119 | border-color: var(--button-border); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /assets/dynamic.css: -------------------------------------------------------------------------------- 1 | @import "tailwind-colors.css"; 2 | 3 | @font-face { 4 | font-family: "Poppins-Regular"; 5 | src: url("fonts/Poppins-Regular.ttf"); 6 | } 7 | 8 | :root { 9 | --background-color: var(--color-gray-100); 10 | --text-color: var(--color-gray-900); 11 | --font-size: 27px; 12 | } 13 | 14 | :root { 15 | display: flex; 16 | width: 100%; 17 | height: 100%; 18 | 19 | padding-top: 20vh; 20 | 21 | align-items: center; 22 | justify-content: start; 23 | flex-direction: column; 24 | 25 | font-family: "Poppins-Regular"; 26 | font-size: var(--font-size); 27 | color: var(--text-color); 28 | background-color: var(--background-color); 29 | 30 | & > :first-child { 31 | font-size: calc(var(--font-size) * 0.5); 32 | } 33 | } 34 | 35 | Input { 36 | display: flex; 37 | align-items: center; 38 | justify-content: start; 39 | 40 | width: 400px; 41 | height: 60px; 42 | padding: 0 20px; 43 | border: 2px var(--color-gray-950); 44 | border-radius: 20px; 45 | 46 | font-size: calc(var(--font-size) * 0.8); 47 | 48 | transition: border-color 0.3s; 49 | 50 | &:focus-visible { 51 | border-color: var(--color-red-600); 52 | } 53 | } 54 | 55 | .items-container { 56 | display: flex; 57 | 58 | align-items: center; 59 | justify-content: center; 60 | flex-direction: column; 61 | 62 | margin-top: 5px; 63 | 64 | width: 350px; 65 | height: auto; 66 | padding: 5px 20px; 67 | 68 | border: 2px transparent; 69 | } 70 | 71 | .no-items-text { 72 | display: none; 73 | } 74 | 75 | .items-container:empty ~ .no-items-text { 76 | display: flex; 77 | } 78 | 79 | .item { 80 | display: flex; 81 | align-items: start; 82 | 83 | width: 100%; 84 | 85 | &:nth-child(odd) .item-text { 86 | background-color: var(--color-gray-100); 87 | } 88 | &:nth-child(even) .item-text { 89 | background-color: var(--color-gray-300); 90 | } 91 | 92 | &:first-child .item-text { 93 | border-top-width: 2px; 94 | border-top-left-radius: 10px; 95 | border-top-right-radius: 10px; 96 | } 97 | 98 | &:last-child .item-text { 99 | border-bottom-width: 2px; 100 | border-bottom-left-radius: 10px; 101 | border-bottom-right-radius: 10px; 102 | } 103 | 104 | .item-text { 105 | flex: 1; 106 | border-width: 0 2px; 107 | border-color: var(--color-gray-900); 108 | margin-right: 15px; 109 | border-radius: 0; 110 | 111 | padding: 4px 15px; 112 | } 113 | 114 | .remove-button { 115 | display: flex; 116 | align-items: center; 117 | justify-content: center; 118 | align-self: center; 119 | 120 | height: 26px; 121 | width: 26px; 122 | 123 | transition: border-color 0.3s; 124 | 125 | color: var(--color-gray-700); 126 | border: 1px var(--color-gray-700); 127 | border-radius: 100%; 128 | font-size: calc(var(--font-size) * 0.7); 129 | 130 | &:focus-visible { 131 | border-color: var(--color-red-600); 132 | } 133 | } 134 | } 135 | 136 | 137 | -------------------------------------------------------------------------------- /assets/fonts/Kenney Space.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eckz/bevy_flair/bcd25912092cfe012b44babd93d2ada8b036782a/assets/fonts/Kenney Space.ttf -------------------------------------------------------------------------------- /assets/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /assets/fonts/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eckz/bevy_flair/bcd25912092cfe012b44babd93d2ada8b036782a/assets/fonts/Poppins-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eckz/bevy_flair/bcd25912092cfe012b44babd93d2ada8b036782a/assets/fonts/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /assets/game_menu.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Kenney Space"; 3 | src: url("fonts/Kenney Space.ttf"); 4 | } 5 | 6 | @keyframes borders-animation { 7 | 0%, 50%, 100% { 8 | margin-top: 0; 9 | width: 250px; 10 | height: 60px; 11 | } 12 | 13 | 25% { 14 | margin-top: -5px; 15 | width: 270px; 16 | height: 70px; 17 | } 18 | } 19 | 20 | :root { 21 | font-family: "Kenney Space"; 22 | 23 | width: 100%; 24 | height: 100%; 25 | background-color: #1f2730; 26 | 27 | --floating-borders-top: 100px; 28 | --floating-borders-spacing: 90px; 29 | } 30 | 31 | :root, #game_menu { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | } 36 | 37 | #menu_title { 38 | font-size: 38px; 39 | color: #d68a4c; 40 | } 41 | 42 | #game_menu { 43 | width: auto; 44 | height: auto; 45 | padding: 25px 90px 40px; 46 | 47 | background-color: rgb(39, 58, 66); 48 | 49 | flex-direction: column; 50 | 51 | border-radius: 2px; 52 | 53 | background-image: url("panel-border-030.png"); 54 | -bevy-image-mode: sliced(20.0px); 55 | } 56 | 57 | Button { 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | 62 | margin-top: 30px; 63 | background-color: transparent; 64 | 65 | font-size: 20px; 66 | color: rgb(20, 20, 21); 67 | &:focus { 68 | color: #E6E6E6; 69 | } 70 | 71 | Text { 72 | transition: color 0.6s; 73 | } 74 | } 75 | 76 | Button, #floating_borders { 77 | width: 250px; 78 | height: 60px; 79 | } 80 | 81 | #floating_borders { 82 | display: flex; 83 | position: absolute; 84 | top: var(--floating-borders-top); 85 | transition: top 0.6s; 86 | background-image: url("panel-border-010.png"); 87 | -bevy-image-mode: sliced(20.0px); 88 | } 89 | 90 | Button:nth-child(2):focus ~ #floating_borders { 91 | top: calc(var(--floating-borders-top) + var(--floating-borders-spacing) * 0); 92 | } 93 | 94 | Button:nth-child(3):focus ~ #floating_borders { 95 | top: calc(var(--floating-borders-top) + var(--floating-borders-spacing) * 1); 96 | } 97 | 98 | Button:nth-child(4):focus ~ #floating_borders { 99 | top: calc(var(--floating-borders-top) + var(--floating-borders-spacing) * 2); 100 | } 101 | 102 | Button:nth-child(5):focus ~ #floating_borders { 103 | top: calc(var(--floating-borders-top) + var(--floating-borders-spacing) * 3); 104 | } 105 | 106 | Button:hover ~ #floating_borders { 107 | animation: 2.0s linear infinite borders-animation; 108 | } 109 | 110 | -------------------------------------------------------------------------------- /assets/grid.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | display: grid; 4 | width: 100%; 5 | height: 100%; 6 | 7 | grid: repeat(50, 1fr) / repeat(50, 1fr); 8 | grid-auto-flow: row; 9 | 10 | gap: 2px; 11 | padding: 5px; 12 | } 13 | 14 | :root > * { 15 | border-color: white; 16 | border-radius: 3px; 17 | background-color: aqua; 18 | transition: background-color 5.0s; 19 | 20 | &:hover { 21 | background-color: blueviolet; 22 | transition: background-color 0.3s; 23 | } 24 | } -------------------------------------------------------------------------------- /assets/panel-border-010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eckz/bevy_flair/bcd25912092cfe012b44babd93d2ada8b036782a/assets/panel-border-010.png -------------------------------------------------------------------------------- /assets/panel-border-030.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eckz/bevy_flair/bcd25912092cfe012b44babd93d2ada8b036782a/assets/panel-border-030.png -------------------------------------------------------------------------------- /benches/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes animation { 2 | from { 3 | width: 20px; 4 | } 5 | 6 | to { 7 | width: 300px; 8 | } 9 | } 10 | 11 | :root { 12 | display: flex; 13 | flex-direction: column; 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | :root > * { 19 | width: 20px; 20 | height: 20px; 21 | background-color: black; 22 | } 23 | 24 | .animated { 25 | animation: 0.2s ease infinite alternate animation; 26 | } -------------------------------------------------------------------------------- /benches/basic.css: -------------------------------------------------------------------------------- 1 | :root { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | :root > * { 9 | width: 20px; 10 | height: 20px; 11 | background-color: black; 12 | } -------------------------------------------------------------------------------- /benches/flair_benches.rs: -------------------------------------------------------------------------------- 1 | use bevy::asset::io::memory::Dir; 2 | use bevy::prelude::*; 3 | 4 | use bevy::ecs::system::RunSystemOnce; 5 | use bevy::time::TimeUpdateStrategy; 6 | use bevy::time::common_conditions::on_timer; 7 | use bevy_flair::prelude::*; 8 | use std::sync::LazyLock; 9 | use std::time::Duration; 10 | 11 | criterion::criterion_group!(benches, benchmarks); 12 | criterion::criterion_main!(benches); 13 | 14 | static ASSETS_DIR: LazyLock = LazyLock::new(|| { 15 | let dir = Dir::new("assets".into()); 16 | dir.insert_asset("basic.css".as_ref(), include_bytes!("basic.css")); 17 | dir.insert_asset( 18 | "transitions.css".as_ref(), 19 | include_bytes!("transitions.css"), 20 | ); 21 | dir.insert_asset("animations.css".as_ref(), include_bytes!("animations.css")); 22 | dir 23 | }); 24 | 25 | fn test_app() -> App { 26 | use bevy::asset::io::memory::MemoryAssetReader; 27 | use bevy::asset::io::{AssetSource, AssetSourceId}; 28 | let mut app = App::new(); 29 | 30 | app.register_asset_source( 31 | AssetSourceId::Default, 32 | AssetSource::build().with_reader(move || { 33 | Box::new(MemoryAssetReader { 34 | root: ASSETS_DIR.clone(), 35 | }) 36 | }), 37 | ); 38 | 39 | app.add_plugins(( 40 | bevy::time::TimePlugin, 41 | TaskPoolPlugin { 42 | task_pool_options: TaskPoolOptions::with_num_threads(1), 43 | }, 44 | AssetPlugin::default(), 45 | FlairPlugin, 46 | )); 47 | app.finish(); 48 | app 49 | } 50 | 51 | fn run_n_frames(num_frames: usize) -> impl Fn(App) { 52 | move |mut app: App| { 53 | for _ in 0..num_frames { 54 | app.update(); 55 | } 56 | } 57 | } 58 | 59 | fn default_button() -> impl Bundle { 60 | (Button, children![Text::new("Button")]) 61 | } 62 | 63 | fn spawn_root_with_n_buttons( 64 | style_sheet: &'static str, 65 | n: u32, 66 | mut button_spawner: F, 67 | ) -> impl FnMut(Commands, Res) 68 | where 69 | B: Bundle, 70 | F: FnMut() -> B, 71 | { 72 | move |mut commands, asset_server| { 73 | commands 74 | .spawn(( 75 | Node::default(), 76 | NodeStyleSheet::new(asset_server.load(style_sheet)), 77 | )) 78 | .with_children(|root| { 79 | for _ in 0..n { 80 | root.spawn(button_spawner()); 81 | } 82 | }); 83 | } 84 | } 85 | 86 | fn spawn_n_roots(style_sheet: &'static str, n: u32) -> impl FnMut(Commands, Res) { 87 | move |mut commands, asset_server| { 88 | let style = asset_server.load(style_sheet); 89 | for _ in 0..n { 90 | commands.spawn((Node::default(), NodeStyleSheet::new(style.clone()))); 91 | } 92 | } 93 | } 94 | 95 | fn benchmarks(c: &mut criterion::Criterion) { 96 | let mut group = c.benchmark_group("n_buttons"); 97 | group.warm_up_time(Duration::from_millis(200)); 98 | group.sample_size(30); 99 | 100 | for n_buttons in [512, 4096, 16384, 32768] { 101 | group.bench_with_input( 102 | criterion::BenchmarkId::from_parameter(n_buttons), 103 | &n_buttons, 104 | |b, &n_buttons| { 105 | b.iter_batched( 106 | || { 107 | let mut app = test_app(); 108 | app.update(); 109 | app.world_mut() 110 | .run_system_once(spawn_root_with_n_buttons( 111 | "basic.css", 112 | n_buttons, 113 | default_button, 114 | )) 115 | .expect("Error on initial spawn"); 116 | app 117 | }, 118 | run_n_frames(60), 119 | criterion::BatchSize::LargeInput, 120 | ); 121 | }, 122 | ); 123 | } 124 | group.finish(); 125 | 126 | let mut group = c.benchmark_group("n_roots"); 127 | group.warm_up_time(Duration::from_millis(200)); 128 | group.sample_size(30); 129 | 130 | for n_roots in [512, 4096, 16384, 32768] { 131 | group.bench_with_input( 132 | criterion::BenchmarkId::from_parameter(n_roots), 133 | &n_roots, 134 | |b, &n_roots| { 135 | b.iter_batched( 136 | || { 137 | let mut app = test_app(); 138 | app.update(); 139 | app.world_mut() 140 | .run_system_once(spawn_n_roots("basic.css", n_roots)) 141 | .expect("Error on initial spawn"); 142 | app 143 | }, 144 | run_n_frames(60), 145 | criterion::BatchSize::LargeInput, 146 | ); 147 | }, 148 | ); 149 | } 150 | group.finish(); 151 | 152 | let mut group = c.benchmark_group("n_transitions"); 153 | group.warm_up_time(Duration::from_secs(1)); 154 | 155 | for n_buttons in [512, 2048, 8192] { 156 | group.bench_with_input( 157 | criterion::BenchmarkId::from_parameter(n_buttons), 158 | &n_buttons, 159 | |b, &n_buttons| { 160 | b.iter_batched( 161 | || { 162 | fn toggle_class(mut query: Query<&mut ClassList, With