├── .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