├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── bevy_cobweb_ui_derive │ ├── Cargo.toml │ └── src │ │ ├── inner.rs │ │ └── lib.rs ├── cobweb_asset_format │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cob │ │ ├── cob.rs │ │ ├── cob_commands.rs │ │ ├── cob_defs.rs │ │ ├── cob_import.rs │ │ ├── cob_manifest.rs │ │ ├── cob_scenes.rs │ │ ├── mod.rs │ │ └── references.rs │ │ ├── data │ │ ├── cob_file.rs │ │ ├── cob_fill.rs │ │ ├── cob_generics.rs │ │ ├── cob_loadable.rs │ │ ├── cob_scene_layer.rs │ │ ├── de │ │ │ ├── cob_builtin.rs │ │ │ ├── cob_enum_variant.rs │ │ │ ├── cob_loadable.rs │ │ │ ├── cob_number.rs │ │ │ ├── cob_value.rs │ │ │ ├── containers.rs │ │ │ └── mod.rs │ │ ├── defs │ │ │ ├── cob_constant.rs │ │ │ ├── cob_scene_macro.rs │ │ │ ├── cob_value_group.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── ser │ │ │ ├── cob_loadable.rs │ │ │ ├── cob_value.rs │ │ │ └── mod.rs │ │ └── value │ │ │ ├── cob_array.rs │ │ │ ├── cob_bool.rs │ │ │ ├── cob_builtin.rs │ │ │ ├── cob_enum.rs │ │ │ ├── cob_map.rs │ │ │ ├── cob_none.rs │ │ │ ├── cob_number.rs │ │ │ ├── cob_string.rs │ │ │ ├── cob_tuple.rs │ │ │ ├── cob_value.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── parsing │ │ ├── error.rs │ │ ├── identifiers.rs │ │ ├── mod.rs │ │ ├── recursion.rs │ │ └── span.rs │ │ ├── raw_serializer.rs │ │ └── resolver │ │ ├── cob_resolver.rs │ │ ├── constants_resolver.rs │ │ ├── mod.rs │ │ ├── scene_macros_resolver.rs │ │ └── utils.rs ├── sickle_macros │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── style_commands.rs ├── sickle_math │ ├── Cargo.toml │ └── src │ │ ├── ease.rs │ │ ├── lerp.rs │ │ └── lib.rs └── sickle_ui_scaffold │ ├── Cargo.toml │ └── src │ ├── attributes │ ├── custom_attrs.rs │ ├── dynamic_style.rs │ ├── dynamic_style_attribute.rs │ ├── mod.rs │ ├── pseudo_state.rs │ ├── style_animation.rs │ ├── traits.rs │ └── ui_context.rs │ ├── builder_ext │ ├── container.rs │ └── mod.rs │ ├── flux_interaction.rs │ ├── lib.rs │ ├── ui_builder.rs │ ├── ui_commands.rs │ ├── ui_style │ ├── attribute.rs │ ├── builder.rs │ ├── generated.rs │ ├── manual.rs │ ├── mod.rs │ └── style.rs │ └── ui_utils.rs ├── examples ├── calculator │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── checkbox │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── counter │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── cursors │ ├── Cargo.toml │ ├── assets │ │ ├── cursor.png │ │ └── main.cob │ └── src │ │ └── main.rs ├── editor_demo │ ├── Cargo.toml │ ├── assets │ │ ├── editor_ext.cob │ │ └── main.cob │ └── src │ │ ├── editor_ext.rs │ │ ├── main.rs │ │ ├── orbiter.rs │ │ └── rng.rs ├── fonts │ ├── Cargo.toml │ ├── assets │ │ ├── fonts │ │ │ ├── LibreBaskerville-Bold.ttf │ │ │ ├── LibreBaskerville-Italic.ttf │ │ │ ├── LibreBaskerville-Regular.ttf │ │ │ └── OFL.txt │ │ └── main.cob │ └── src │ │ └── main.rs ├── game_menu │ ├── Cargo.toml │ ├── README.md │ ├── assets │ │ ├── images │ │ │ ├── example_280_220.png │ │ │ └── example_280_220_fr_FR.png │ │ ├── locales │ │ │ ├── en-US │ │ │ │ ├── main.ftl.ron │ │ │ │ └── text.ftl │ │ │ └── fr-FR │ │ │ │ ├── main.ftl.ron │ │ │ │ └── text.ftl │ │ └── main.caf.json │ └── src │ │ └── main.rs ├── hello_world │ ├── Cargo.toml │ ├── assets │ │ └── main.cobweb │ └── src │ │ └── main.rs ├── help_text │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── localization │ ├── Cargo.toml │ ├── assets │ │ ├── images │ │ │ ├── example_280_220.png │ │ │ ├── example_280_220_de_DE.png │ │ │ └── example_280_220_fr_FR.png │ │ ├── locales │ │ │ ├── de-DE │ │ │ │ ├── main.ftl.ron │ │ │ │ └── text.ftl │ │ │ ├── en-US │ │ │ │ ├── main.ftl.ron │ │ │ │ └── text.ftl │ │ │ └── fr-FR │ │ │ │ ├── main.ftl.ron │ │ │ │ └── text.ftl │ │ └── main.cob │ └── src │ │ └── main.rs ├── radio_buttons │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── scroll │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs ├── slider │ ├── Cargo.toml │ ├── assets │ │ └── main.cob │ └── src │ │ └── main.rs └── tooltip │ ├── Cargo.toml │ ├── assets │ └── main.cob │ └── src │ └── main.rs ├── rustfmt.toml ├── src ├── assets_ext │ ├── ASSETS_EXT.md │ ├── audio.rs │ ├── fonts.rs │ ├── images.rs │ ├── mod.rs │ ├── plugin.rs │ └── texture_atlases.rs ├── bevy_ext │ ├── cursor.rs │ ├── fonts.rs │ ├── mod.rs │ ├── picking.rs │ ├── plugin.rs │ └── texture_atlases.rs ├── builtin │ ├── assets │ │ ├── fonts │ │ │ ├── FiraSans-Bold.ttf │ │ │ ├── FiraSans-BoldItalic.ttf │ │ │ ├── FiraSans-Italic.ttf │ │ │ ├── FiraSans-Medium.ttf │ │ │ ├── FiraSans-MediumItalic.ttf │ │ │ ├── FiraSans-Regular.ttf │ │ │ ├── FiraSansCondensed-Bold.ttf │ │ │ ├── FiraSansCondensed-BoldItalic.ttf │ │ │ ├── FiraSansCondensed-Italic.ttf │ │ │ ├── FiraSansCondensed-Regular.ttf │ │ │ ├── LICENSE-2.0.txt │ │ │ ├── OFL.txt │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── plugin.rs │ ├── colors │ │ ├── COLORS.md │ │ ├── basic.cob │ │ ├── colors.cob │ │ ├── css.cob │ │ ├── mod.rs │ │ ├── plugin.rs │ │ └── tailwind.cob │ ├── mod.rs │ ├── plugin.rs │ └── widgets │ │ ├── README.md │ │ ├── checkbox │ │ ├── mod.rs │ │ └── widget.rs │ │ ├── mod.rs │ │ ├── plugin.rs │ │ ├── radio_button │ │ ├── mod.rs │ │ └── widget.rs │ │ ├── scroll │ │ ├── mod.rs │ │ └── widget.rs │ │ ├── slider │ │ ├── mod.rs │ │ └── widget.rs │ │ └── tooltip │ │ ├── mod.rs │ │ └── widget.rs ├── editor │ ├── EDITOR.md │ ├── build.rs │ ├── death_signal.rs │ ├── editor.rs │ ├── editor_commands.rs │ ├── editor_events.rs │ ├── editor_stack.rs │ ├── hash_registry.rs │ ├── mod.rs │ ├── plugin.rs │ ├── template │ │ ├── frame.cob │ │ └── mod.rs │ ├── utils.rs │ ├── widget_interop.rs │ └── widget_registry.rs ├── lib.rs ├── loading │ ├── LOADING.md │ ├── app_load_ext.rs │ ├── cache │ │ ├── cob_asset_cache.rs │ │ ├── commands_buffer.rs │ │ ├── manifest_map.rs │ │ ├── mod.rs │ │ ├── plugin.rs │ │ └── scene_buffer.rs │ ├── cob_asset_loader.rs │ ├── extract │ │ ├── cob_extract.rs │ │ ├── extract_commands.rs │ │ ├── extract_defs.rs │ │ ├── extract_import.rs │ │ ├── extract_manifest.rs │ │ ├── extract_scenes.rs │ │ ├── mod.rs │ │ ├── reflected_loadable.rs │ │ └── utils.rs │ ├── load_ext.rs │ ├── load_progress.rs │ ├── loadable.rs │ ├── mod.rs │ ├── plugin.rs │ └── scene │ │ ├── mod.rs │ │ ├── scene_builder.rs │ │ ├── scene_handle_error │ │ ├── scene_handle_error.rs │ │ └── spawn_scene_ext.rs ├── localization │ ├── LOCALIZATION.md │ ├── ftl_bundle.rs │ ├── locale.rs │ ├── localization_manifest.rs │ ├── localization_set.rs │ ├── localized_text.rs │ ├── mod.rs │ ├── plugin.rs │ ├── relocalize_tracker.rs │ └── text_localizer.rs ├── plugin.rs ├── react_ext │ ├── mod.rs │ ├── plugin.rs │ ├── reactor_ext.rs │ └── utils.rs ├── sickle_ext │ ├── SICKLE.md │ ├── builder_ext.rs │ ├── control.rs │ ├── control_loadable_registration.rs │ ├── control_loadables.rs │ ├── control_map.rs │ ├── control_traits.rs │ ├── interaction_ext.rs │ ├── mod.rs │ ├── node_attributes.rs │ ├── plugin.rs │ ├── pseudo_states_ext.rs │ └── react_ext.rs ├── tools │ ├── hierarchy_utils.rs │ ├── mod.rs │ ├── plugin.rs │ ├── text_editor.rs │ └── type_name.rs └── ui_bevy │ ├── UI_BEVY.md │ ├── mod.rs │ ├── plugin.rs │ └── ui_ext │ ├── component_wrappers.rs │ ├── image_node.rs │ ├── mod.rs │ ├── node_field_wrappers.rs │ ├── node_wrappers.rs │ ├── opacity.rs │ ├── plugin.rs │ ├── text.rs │ └── text_rendering.rs └── tests └── test ├── cob ├── cob_commands.rs ├── cob_constants.rs ├── cob_fill.rs ├── cob_import.rs ├── cob_manifest.rs ├── cob_scene_macros.rs ├── cob_scenes.rs ├── helpers │ ├── mod.rs │ ├── serde_types.rs │ ├── test_fns.rs │ └── utils.rs ├── mod.rs └── serde.rs ├── common └── mod.rs ├── mod.rs └── type_name.rs /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | inputs: 2 | toolchain: 3 | description: rust toolchain to use 4 | required: false 5 | default: stable 6 | toolchain-components: 7 | description: additional rust toolchain components to install 8 | required: false 9 | key: 10 | description: extra info to use as cache key 11 | required: true 12 | runs: 13 | using: composite 14 | steps: 15 | - uses: actions/cache@v4 16 | with: 17 | path: | 18 | ~/.cargo/bin/ 19 | ~/.cargo/registry/index 20 | ~/.cargo/registry/cache 21 | ~/.cargo/git/db 22 | target/ 23 | key: ${{ runner.os }}-cargo-${{ inputs.toolchain }}-${{ inputs.key }}-${{ hashFiles('Cargo.toml') }} 24 | restore-keys: | 25 | ${{ runner.os }}-cargo-${{ inputs.toolchain }}-${{ inputs.key }}- 26 | ${{ runner.os }}-cargo-${{ inputs.toolchain }}- 27 | - uses: dtolnay/rust-toolchain@master 28 | with: 29 | toolchain: ${{ inputs.toolchain }} 30 | components: ${{ inputs.toolchain-components }} 31 | - name: Install Dependencies (Linux) 32 | if: ${{ runner.os == 'linux' }} 33 | shell: bash 34 | run: > 35 | sudo apt-get update && 36 | sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | ci: 16 | name: CI 17 | # needs: [smoke, test, docs, rustfmt, clippy] 18 | needs: [smoke, test, docs, rustfmt] 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Done 22 | run: exit 0 23 | 24 | test: 25 | needs: smoke 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | os: [ubuntu-latest, windows-latest, macos-latest] 30 | rust: [stable, beta, nightly] 31 | runs-on: ${{ matrix.os }} 32 | continue-on-error: ${{ matrix.rust != 'stable' }} 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: ./.github/actions/setup 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | key: test 39 | - run: cargo test --features=dev 40 | 41 | docs: 42 | name: Docs 43 | needs: smoke 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: ./.github/actions/setup 48 | with: 49 | key: doc 50 | - run: cargo doc --no-deps --document-private-items --features=dev 51 | name: check documentation 52 | 53 | rustfmt: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: ./.github/actions/setup 58 | with: 59 | toolchain: nightly 60 | toolchain-components: rustfmt 61 | key: fmt 62 | - run: cargo fmt --check 63 | name: check formatting 64 | 65 | # clippy: 66 | # needs: smoke 67 | # runs-on: ubuntu-latest 68 | # steps: 69 | # - uses: actions/checkout@v4 70 | # - uses: ./.github/actions/setup 71 | # with: 72 | # toolchain-components: clippy 73 | # key: clippy 74 | # - run: cargo clippy --all-targets --features=dev -- -D warnings 75 | 76 | smoke: 77 | name: Quick Check 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v4 81 | - uses: ./.github/actions/setup 82 | with: 83 | key: check 84 | - run: cargo check --all-targets --features=dev 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .vscode 4 | .notes 5 | */.DS_Store 6 | 7 | rust-toolchain.toml 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_cobweb_ui" 3 | description = "UI framework for the bevy game engine" 4 | authors = ["koe "] 5 | version = "0.18.0" 6 | edition = "2021" 7 | keywords = ["gamedev", "reactive", "ui"] 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/UkoeHB/bevy_cobweb_ui" 11 | 12 | [workspace] 13 | members = ["examples/*", "crates/*"] 14 | exclude = ["examples/*"] 15 | 16 | [lib] 17 | path = "src/lib.rs" 18 | doctest = false 19 | test = false 20 | 21 | [[test]] 22 | name = "tests" 23 | path = "tests/test/mod.rs" 24 | doctest = false 25 | 26 | [package.metadata.docs.rs] 27 | all-features = true 28 | rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] 29 | 30 | [features] 31 | # Default features. 32 | default = ["colors", "widgets", "firasans_default"] 33 | 34 | # Enables built-in colors. 35 | colors = [] 36 | 37 | # Auto-registers FiraSans font variants. This adds ~4.7mb to binary sizes. 38 | firasans = [] 39 | # Sets FiraSans Medium as the default font. 40 | # If not enabled, the default font will be Bevy's default font. 41 | firasans_default = ["firasans"] 42 | 43 | # Enables built-in widgets. 44 | widgets = ["colors"] 45 | 46 | # Enables the built-in experimental COB editor. 47 | editor = ["hot_reload", "colors", "widgets", "dep:foldhash"] 48 | 49 | # Enables Serialize/Deserialize on some built-in types. 50 | serde = ["bevy/serialize"] 51 | 52 | # Enables hot-reloading. Note that embedded files are not hot-reloaded by default due to perf 53 | # issues (see https://github.com/bevyengine/bevy/issues/17430). You'll need bevy's 54 | # `embedded_watcher` feature. 55 | hot_reload = ["bevy/file_watcher", "bevy/multi_threaded"] 56 | 57 | # Dev features. Enables hot-reloading. 58 | dev = ["hot_reload", "bevy/dynamic_linking"] 59 | 60 | [dependencies] 61 | bevy.workspace = true 62 | bevy_cobweb.workspace = true 63 | dyn-clone.workspace = true 64 | nom.workspace = true 65 | serde.workspace = true 66 | smol_str.workspace = true 67 | smallvec.workspace = true 68 | thiserror.workspace = true 69 | tracing.workspace = true 70 | wasm-timer = { version = "0.2" } 71 | 72 | # Localization deps 73 | sys-locale = { version = "0.3" } 74 | fluent = { version = "0.16" } 75 | fluent-langneg = { version = "0.14" } 76 | fluent_content = { version = "0.0.5" } 77 | intl-memoizer = { version = "0.5" } 78 | ron = { version = "0.8" } 79 | serde_yaml = { version = "0.9" } 80 | unic-langid = { version = "0.9" } 81 | 82 | # Editor deps 83 | foldhash = { version = "0.1.3", optional = true } 84 | 85 | # Local sub-crates 86 | bevy_cobweb_ui_derive = { path = "crates/bevy_cobweb_ui_derive", version = "0.3.0" } 87 | cob_sickle_math = { path = "crates/sickle_math", version = "0.7.0" } 88 | cob_sickle_macros = { path = "crates/sickle_macros", version = "0.7.0" } 89 | cob_sickle_ui_scaffold = { path = "crates/sickle_ui_scaffold", version = "0.7.0" } 90 | cobweb_asset_format = { path = "crates/cobweb_asset_format", version = "0.2.0", default-features = false, features = ["full"] } 91 | 92 | [workspace.dependencies] 93 | bevy_cobweb = { version = "0.17.0" } 94 | #bevy_cobweb = { git = "https://github.com/UkoeHB/bevy_cobweb", rev = "19c66ab" } 95 | dyn-clone = { version = "1.0" } 96 | nom = { version = "7.1" } 97 | nom_locate = { version = "4.2" } 98 | serde = { version = "1.0", features = ["derive"] } 99 | smol_str = { version = "0.2" } # Locked to Bevy's smol_str version. 100 | smallvec = { version = "1.13" } 101 | thiserror = { version = "1.0" } 102 | tracing = { version = "0.1" } 103 | 104 | [workspace.dependencies.bevy] 105 | version = "0.16" 106 | default-features = false 107 | features = [ 108 | "std", 109 | "async_executor", 110 | "bevy_asset", 111 | "bevy_audio", 112 | "bevy_color", 113 | "bevy_text", 114 | "bevy_state", 115 | "bevy_log", 116 | "bevy_picking", 117 | "bevy_ui", 118 | "bevy_ui_picking_backend", 119 | "bevy_winit", 120 | "custom_cursor", 121 | "default_font", 122 | "png", 123 | "x11", 124 | "wav" 125 | ] 126 | 127 | [dev-dependencies] 128 | bevy = { version = "0.16", default-features = false, features = [ 129 | "serialize", 130 | 131 | # AssetEvents for AudioSource are not registered if no audio types are included 132 | # this causes some systems to panic because of missing SystemParam 133 | "wav", 134 | ] } 135 | tracing-subscriber = { version = "0.3" } 136 | 137 | [profile.dev.package."*"] 138 | opt-level = 2 139 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 koe 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 | -------------------------------------------------------------------------------- /crates/bevy_cobweb_ui_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_cobweb_ui_derive" 3 | description = "Derives for bevy_cobweb_ui" 4 | version = "0.3.0" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | quote = { version = "1.0" } 13 | syn = { version = "2.0" } 14 | proc-macro2 = "1.0" 15 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cobweb_asset_format" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "COB definition with parsing and ser/de." 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/UkoeHB/bevy_cobweb_ui" 8 | 9 | [features] 10 | default = ["full"] 11 | 12 | # Enables the standard Cob file format with sections. Disable this if you only want to parse raw CobValues. 13 | full = ["builtin"] 14 | 15 | # Enables builtin values. 16 | builtin = ["dep:bevy"] 17 | 18 | [dependencies] 19 | bevy = { version = "0.16", default-features = false, features = ["bevy_ui", "bevy_color"], optional = true } 20 | nom = { workspace = true } 21 | nom_locate = { workspace = true } 22 | serde = { workspace = true } 23 | smol_str = { workspace = true } 24 | smallvec = { workspace = true } 25 | tracing = { workspace = true } 26 | 27 | derive_more = { version = "2.0", features = ["from"] } 28 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/cob/cob_commands.rs: -------------------------------------------------------------------------------- 1 | use nom::bytes::complete::tag; 2 | use nom::Parser; 3 | 4 | use crate::prelude::*; 5 | 6 | //------------------------------------------------------------------------------------------------------------------- 7 | 8 | /// Commands are parsed as loadables. 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub struct CobCommandEntry(pub CobLoadable); 11 | 12 | impl CobCommandEntry 13 | { 14 | pub fn write_to(&self, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 15 | { 16 | self.0.write_to(writer) 17 | } 18 | 19 | pub fn try_parse(fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 20 | { 21 | let starts_newline = fill.ends_with_newline(); 22 | let check_newline = || -> Result<(), SpanError> { 23 | if !starts_newline { 24 | tracing::warn!("command entry doesn't start on a new line at {}", get_location(content).as_str()); 25 | return Err(span_verify_error(content)); 26 | } 27 | Ok(()) 28 | }; 29 | let fill = match rc(content, move |c| CobLoadable::try_parse(fill, c))? { 30 | (Some(loadable), next_fill, remaining) => { 31 | (check_newline)()?; 32 | // NOTE: macro params are not allowed in commands but we don't check here to avoid the perf cost 33 | // of traversing the structure. Allow errors to be detected downstream (e.g. when deserializing). 34 | // TODO: re-evaluate if this is useful; the perf cost of traversing everything again is 35 | // non-negligible 36 | return Ok((Some(Self(loadable)), next_fill, remaining)); 37 | } 38 | (None, fill, _) => fill, 39 | }; 40 | 41 | Ok((None, fill, content)) 42 | } 43 | } 44 | 45 | //------------------------------------------------------------------------------------------------------------------- 46 | 47 | #[derive(Debug, Clone, PartialEq)] 48 | pub struct CobCommands 49 | { 50 | pub start_fill: CobFill, 51 | pub entries: Vec, 52 | } 53 | 54 | impl CobCommands 55 | { 56 | pub fn write_to(&self, first_section: bool, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 57 | { 58 | let space = if first_section { "" } else { "\n\n" }; 59 | self.start_fill.write_to_or_else(writer, space)?; 60 | writer.write_bytes("#commands".as_bytes())?; 61 | for entry in self.entries.iter() { 62 | entry.write_to(writer)?; 63 | } 64 | Ok(()) 65 | } 66 | 67 | pub fn try_parse(start_fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 68 | { 69 | let Ok((remaining, _)) = tag::<_, _, ()>("#commands").parse(content) else { 70 | return Ok((None, start_fill, content)); 71 | }; 72 | 73 | if start_fill.len() != 0 && !start_fill.ends_with_newline() { 74 | tracing::warn!("failed parsing commands section at {} that doesn't start on newline", 75 | get_location(content).as_str()); 76 | return Err(span_verify_error(content)); 77 | } 78 | 79 | let (mut item_fill, mut remaining) = CobFill::parse(remaining); 80 | let mut entries = vec![]; 81 | 82 | let end_fill = loop { 83 | match rc(remaining, move |rm| CobCommandEntry::try_parse(item_fill, rm))? { 84 | (Some(entry), next_fill, after_entry) => { 85 | entries.push(entry); 86 | item_fill = next_fill; 87 | remaining = after_entry; 88 | } 89 | (None, end_fill, after_end) => { 90 | remaining = after_end; 91 | break end_fill; 92 | } 93 | } 94 | }; 95 | 96 | let command = Self { start_fill, entries }; 97 | Ok((Some(command), end_fill, remaining)) 98 | } 99 | } 100 | 101 | impl Default for CobCommands 102 | { 103 | fn default() -> Self 104 | { 105 | Self { start_fill: CobFill::default(), entries: Vec::default() } 106 | } 107 | } 108 | 109 | //------------------------------------------------------------------------------------------------------------------- 110 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/cob/cob_defs.rs: -------------------------------------------------------------------------------- 1 | use nom::bytes::complete::tag; 2 | use nom::Parser; 3 | 4 | use crate::prelude::*; 5 | 6 | //------------------------------------------------------------------------------------------------------------------- 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub enum CobDefEntry 10 | { 11 | Constant(CobConstantDef), 12 | SceneMacro(CobSceneMacroDef), 13 | } 14 | 15 | impl CobDefEntry 16 | { 17 | pub fn write_to(&self, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 18 | { 19 | match self { 20 | Self::Constant(entry) => { 21 | entry.write_to(writer)?; 22 | } 23 | Self::SceneMacro(entry) => { 24 | entry.write_to(writer)?; 25 | } 26 | } 27 | Ok(()) 28 | } 29 | 30 | pub fn try_parse(fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 31 | { 32 | let starts_newline = fill.ends_with_newline(); 33 | let check_newline = || -> Result<(), SpanError> { 34 | if !starts_newline { 35 | tracing::warn!("def entry doesn't start on a new line at {}", get_location(content).as_str()); 36 | return Err(span_verify_error(content)); 37 | } 38 | Ok(()) 39 | }; 40 | let fill = match rc(content, move |c| CobConstantDef::try_parse(fill, c))? { 41 | (Some(def), next_fill, remaining) => { 42 | (check_newline)()?; 43 | return Ok((Some(Self::Constant(def)), next_fill, remaining)); 44 | } 45 | (None, fill, _) => fill, 46 | }; 47 | let fill = match rc(content, move |c| CobSceneMacroDef::try_parse(fill, c))? { 48 | (Some(def), next_fill, remaining) => { 49 | (check_newline)()?; 50 | return Ok((Some(Self::SceneMacro(def)), next_fill, remaining)); 51 | } 52 | (None, fill, _) => fill, 53 | }; 54 | 55 | Ok((None, fill, content)) 56 | } 57 | } 58 | 59 | //------------------------------------------------------------------------------------------------------------------- 60 | 61 | /// Includes constants and macros. A constant is equivalent to a macro with no parameters. 62 | #[derive(Default, Debug, Clone, PartialEq)] 63 | pub struct CobDefs 64 | { 65 | pub start_fill: CobFill, 66 | pub entries: Vec, 67 | } 68 | 69 | impl CobDefs 70 | { 71 | pub fn write_to(&self, first_section: bool, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 72 | { 73 | let space = if first_section { "" } else { "\n\n" }; 74 | self.start_fill.write_to_or_else(writer, space)?; 75 | writer.write_bytes("#defs".as_bytes())?; 76 | for entry in self.entries.iter() { 77 | entry.write_to(writer)?; 78 | } 79 | Ok(()) 80 | } 81 | 82 | pub fn try_parse(start_fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 83 | { 84 | let Ok((remaining, _)) = tag::<_, _, ()>("#defs").parse(content) else { 85 | return Ok((None, start_fill, content)); 86 | }; 87 | 88 | if start_fill.len() != 0 && !start_fill.ends_with_newline() { 89 | tracing::warn!("failed parsing defs section at {} that doesn't start on newline", 90 | get_location(content).as_str()); 91 | return Err(span_verify_error(content)); 92 | } 93 | 94 | let (mut item_fill, mut remaining) = CobFill::parse(remaining); 95 | let mut entries = vec![]; 96 | 97 | let end_fill = loop { 98 | match rc(remaining, move |rm| CobDefEntry::try_parse(item_fill, rm))? { 99 | (Some(entry), next_fill, after_entry) => { 100 | entries.push(entry); 101 | item_fill = next_fill; 102 | remaining = after_entry; 103 | } 104 | (None, end_fill, after_end) => { 105 | remaining = after_end; 106 | break end_fill; 107 | } 108 | } 109 | }; 110 | 111 | let defs = CobDefs { start_fill, entries }; 112 | Ok((Some(defs), end_fill, remaining)) 113 | } 114 | } 115 | 116 | //------------------------------------------------------------------------------------------------------------------- 117 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/cob/cob_scenes.rs: -------------------------------------------------------------------------------- 1 | use nom::bytes::complete::tag; 2 | use nom::Parser; 3 | 4 | use crate::prelude::*; 5 | 6 | //------------------------------------------------------------------------------------------------------------------- 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct CobScenes 10 | { 11 | pub start_fill: CobFill, 12 | pub scenes: Vec, 13 | } 14 | 15 | impl CobScenes 16 | { 17 | pub fn write_to(&self, first_section: bool, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 18 | { 19 | let space = if first_section { "" } else { "\n\n" }; 20 | self.start_fill.write_to_or_else(writer, space)?; 21 | writer.write_bytes("#scenes".as_bytes())?; 22 | for layer in self.scenes.iter() { 23 | layer.write_to(writer)?; 24 | } 25 | Ok(()) 26 | } 27 | 28 | pub fn try_parse(start_fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 29 | { 30 | let Ok((remaining, _)) = tag::<_, _, ()>("#scenes").parse(content) else { 31 | return Ok((None, start_fill, content)); 32 | }; 33 | 34 | if start_fill.len() != 0 && !start_fill.ends_with_newline() { 35 | tracing::warn!("failed parsing scenes section at {} that doesn't start on newline", 36 | get_location(remaining).as_str()); 37 | return Err(span_verify_error(remaining)); 38 | } 39 | 40 | let (mut item_fill, mut remaining) = CobFill::parse(remaining); 41 | let mut scenes = vec![]; 42 | 43 | let end_fill = loop { 44 | let item_depth = item_fill.ends_newline_then_num_spaces(); 45 | match rc(remaining, move |rm| CobSceneLayer::try_parse(item_fill, rm))? { 46 | (Some(entry), next_fill, after_entry) => { 47 | if item_depth != Some(0) { 48 | tracing::warn!("failed parsing scene at {}; scene is assessed to be on base layer \ 49 | but doesn't start with a newline", get_location(remaining).as_str()); 50 | return Err(span_verify_error(remaining)); 51 | } 52 | scenes.push(entry); 53 | item_fill = next_fill; 54 | remaining = after_entry; 55 | } 56 | (None, end_fill, after_end) => { 57 | remaining = after_end; 58 | break end_fill; 59 | } 60 | } 61 | }; 62 | 63 | let scenes = CobScenes { start_fill, scenes }; 64 | Ok((Some(scenes), end_fill, remaining)) 65 | } 66 | } 67 | 68 | // Parsing: layers cannot contain scene macro params, and layer entries cannot contain macro params. 69 | // - TODO: evaluate if this is useful, the perf cost to validate is non-negligible if done by re-traversing the 70 | // data 71 | 72 | //------------------------------------------------------------------------------------------------------------------- 73 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/cob/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob; 2 | mod cob_commands; 3 | mod cob_defs; 4 | mod cob_import; 5 | mod cob_manifest; 6 | mod cob_scenes; 7 | mod references; 8 | 9 | pub use cob::*; 10 | pub use cob_commands::*; 11 | pub use cob_defs::*; 12 | pub use cob_import::*; 13 | pub use cob_manifest::*; 14 | pub use cob_scenes::*; 15 | pub use references::*; 16 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/cob_file.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::sync::Arc; 3 | 4 | use nom::bytes::complete::{tag, take_until}; 5 | use nom::sequence::delimited; 6 | use nom::Parser; 7 | 8 | use crate::prelude::*; 9 | 10 | //------------------------------------------------------------------------------------------------------------------- 11 | 12 | /// Represents the path to a cobweb asset file in the `asset` directory. 13 | /// 14 | /// Cobweb asset files use the `.cob` extension. If your original path includes an asset source, then 15 | /// the asset source must be included in the name (e.g. `embedded://scene.cob` -> `scene.cob`). 16 | /// 17 | /// Example: `ui/home.cob` for a `home` cobweb asset in `assets/ui`. 18 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 19 | pub struct CobFile(Arc); 20 | 21 | impl CobFile 22 | { 23 | /// Tries to create a new COB file reference. 24 | /// 25 | /// Fails if the file doesn't end with `.cob` or `.cobweb`. 26 | pub fn try_new(file: impl AsRef) -> Option 27 | { 28 | let file = file.as_ref(); 29 | if !file.ends_with(".cob") && !file.ends_with(".cobweb") { 30 | return None; 31 | } 32 | if file.find("\\").is_some() { 33 | let file = file.replace("\\", "/"); 34 | Some(Self(Arc::from(file.as_str()))) 35 | } else { 36 | Some(Self(Arc::from(file))) 37 | } 38 | } 39 | 40 | pub fn write_to(&self, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 41 | { 42 | writer.write_bytes("\"".as_bytes())?; 43 | writer.write_bytes(self.0.as_bytes())?; 44 | writer.write_bytes("\"".as_bytes())?; 45 | Ok(()) 46 | } 47 | 48 | pub fn parse(content: Span) -> Result<(Self, Span), SpanError> 49 | { 50 | let (remaining, path) = delimited(tag("\""), take_until("\""), tag("\"")).parse(content)?; 51 | 52 | // Validate 53 | if !path.ends_with(".cob") && !path.ends_with(".cobweb") { 54 | tracing::warn!("failed parsing COB file path at {}; file does not end with '.cob' or '.cobweb' extension", 55 | get_location(content).as_str()); 56 | return Err(span_verify_error(content)); 57 | } 58 | 59 | Ok((Self(Arc::from(*path.fragment())), remaining)) 60 | } 61 | 62 | pub fn as_str(&self) -> &str 63 | { 64 | &self.0 65 | } 66 | 67 | pub fn get(&self) -> &Arc 68 | { 69 | &self.0 70 | } 71 | } 72 | 73 | impl Default for CobFile 74 | { 75 | fn default() -> Self 76 | { 77 | Self(Arc::from("")) 78 | } 79 | } 80 | 81 | impl Borrow for CobFile 82 | { 83 | fn borrow(&self) -> &str 84 | { 85 | self.as_str() 86 | } 87 | } 88 | 89 | //------------------------------------------------------------------------------------------------------------------- 90 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/de/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "builtin")] 2 | mod cob_builtin; 3 | mod cob_enum_variant; 4 | mod cob_loadable; 5 | mod cob_number; 6 | mod cob_value; 7 | mod containers; 8 | 9 | #[cfg(feature = "builtin")] 10 | pub(self) use cob_builtin::*; 11 | pub(self) use cob_enum_variant::*; 12 | pub(self) use containers::*; 13 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/defs/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob_constant; 2 | mod cob_scene_macro; 3 | mod cob_value_group; 4 | 5 | pub use cob_constant::*; 6 | pub use cob_scene_macro::*; 7 | pub use cob_value_group::*; 8 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob_file; 2 | mod cob_fill; 3 | mod cob_generics; 4 | mod cob_loadable; 5 | mod cob_scene_layer; 6 | mod de; 7 | #[cfg(feature = "full")] 8 | mod defs; 9 | mod ser; 10 | mod value; 11 | 12 | pub use cob_file::*; 13 | pub use cob_fill::*; 14 | pub use cob_generics::*; 15 | pub use cob_loadable::*; 16 | pub use cob_scene_layer::*; 17 | #[cfg(feature = "full")] 18 | pub use defs::*; 19 | pub use ser::*; 20 | pub use value::*; 21 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/ser/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob_loadable; 2 | mod cob_value; 3 | 4 | pub use cob_loadable::CobLoadableSerializer; 5 | pub use cob_value::CobValueSerializer; 6 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/value/cob_bool.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | //------------------------------------------------------------------------------------------------------------------- 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct CobBool 7 | { 8 | pub fill: CobFill, 9 | pub value: bool, 10 | } 11 | 12 | impl CobBool 13 | { 14 | pub fn write_to(&self, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 15 | { 16 | self.write_to_with_space(writer, "") 17 | } 18 | 19 | pub fn write_to_with_space(&self, writer: &mut impl RawSerializer, space: &str) -> Result<(), std::io::Error> 20 | { 21 | self.fill.write_to_or_else(writer, space)?; 22 | let string = match self.value { 23 | true => "true", 24 | false => "false", 25 | }; 26 | writer.write_bytes(string.as_bytes())?; 27 | Ok(()) 28 | } 29 | 30 | pub fn try_parse(fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 31 | { 32 | // NOTE: recursion not tested here (not vulnerable) 33 | 34 | let Ok((remaining, maybe_bool)) = snake_identifier(content) else { return Ok((None, fill, content)) }; 35 | let value = match *maybe_bool.fragment() { 36 | "true" => true, 37 | "false" => false, 38 | _ => return Ok((None, fill, content)), 39 | }; 40 | let (next_fill, remaining) = CobFill::parse(remaining); 41 | Ok((Some(Self { fill, value }), next_fill, remaining)) 42 | } 43 | 44 | pub fn recover_fill(&mut self, other: &Self) 45 | { 46 | self.fill.recover(&other.fill); 47 | } 48 | } 49 | 50 | impl From for CobBool 51 | { 52 | fn from(value: bool) -> Self 53 | { 54 | Self { fill: CobFill::default(), value } 55 | } 56 | } 57 | 58 | /* 59 | Parsing: 60 | - parse as string 61 | 62 | fn parse() 63 | { 64 | value(false, false_parser) 65 | value(true, true_parser) 66 | } 67 | */ 68 | 69 | //------------------------------------------------------------------------------------------------------------------- 70 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/value/cob_none.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | //------------------------------------------------------------------------------------------------------------------- 4 | 5 | #[derive(Default, Debug, Clone, PartialEq)] 6 | pub struct CobNone 7 | { 8 | pub fill: CobFill, 9 | } 10 | 11 | impl CobNone 12 | { 13 | pub fn write_to(&self, writer: &mut impl RawSerializer) -> Result<(), std::io::Error> 14 | { 15 | self.write_to_with_space(writer, "") 16 | } 17 | 18 | pub fn write_to_with_space(&self, writer: &mut impl RawSerializer, space: &str) -> Result<(), std::io::Error> 19 | { 20 | self.fill.write_to_or_else(writer, space)?; 21 | writer.write_bytes("none".as_bytes())?; 22 | Ok(()) 23 | } 24 | 25 | pub fn try_parse(fill: CobFill, content: Span) -> Result<(Option, CobFill, Span), SpanError> 26 | { 27 | // NOTE: recursion not tested here (not vulnerable) 28 | 29 | let Ok((remaining, maybe_none)) = snake_identifier(content) else { return Ok((None, fill, content)) }; 30 | if *maybe_none.fragment() != "none" { 31 | return Ok((None, fill, content)); 32 | }; 33 | let (next_fill, remaining) = CobFill::parse(remaining); 34 | Ok((Some(Self { fill }), next_fill, remaining)) 35 | } 36 | 37 | pub fn recover_fill(&mut self, other: &Self) 38 | { 39 | self.fill.recover(&other.fill); 40 | } 41 | } 42 | 43 | /* 44 | Parsing: 45 | - parse as string 46 | */ 47 | 48 | //------------------------------------------------------------------------------------------------------------------- 49 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/data/value/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob_array; 2 | mod cob_bool; 3 | #[cfg(feature = "builtin")] 4 | mod cob_builtin; 5 | mod cob_enum; 6 | mod cob_map; 7 | mod cob_none; 8 | mod cob_number; 9 | mod cob_string; 10 | mod cob_tuple; 11 | mod cob_value; 12 | 13 | pub use cob_array::*; 14 | pub use cob_bool::*; 15 | #[cfg(feature = "builtin")] 16 | pub use cob_builtin::*; 17 | pub use cob_enum::*; 18 | pub use cob_map::*; 19 | pub use cob_none::*; 20 | pub use cob_number::*; 21 | pub use cob_string::*; 22 | pub use cob_tuple::*; 23 | pub use cob_value::*; 24 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | #![allow(rustdoc::redundant_explicit_links)] 3 | #![doc = include_str!("../README.md")] 4 | #[allow(unused_imports)] 5 | use crate as cobweb_asset_format; 6 | 7 | #[cfg(feature = "full")] 8 | mod cob; 9 | mod data; 10 | mod parsing; 11 | mod raw_serializer; 12 | #[cfg(feature = "full")] 13 | mod resolver; 14 | 15 | pub mod prelude 16 | { 17 | #[cfg(feature = "full")] 18 | pub use crate::cob::*; 19 | pub use crate::data::*; 20 | pub use crate::parsing::*; 21 | pub use crate::raw_serializer::*; 22 | #[cfg(feature = "full")] 23 | pub use crate::resolver::*; 24 | } 25 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/parsing/identifiers.rs: -------------------------------------------------------------------------------- 1 | use nom::branch::alt; 2 | use nom::character::complete::{alpha1, alphanumeric0, char, digit1}; 3 | use nom::combinator::recognize; 4 | use nom::error::ErrorKind; 5 | use nom::multi::many0_count; 6 | use nom::sequence::tuple; 7 | use nom::{IResult, InputTakeAtPosition, Parser}; 8 | 9 | use super::Span; 10 | 11 | //------------------------------------------------------------------------------------------------------------------- 12 | 13 | /// Recognizes lowercase letters. 14 | fn lowercase_alpha1(input: Span) -> IResult 15 | { 16 | input.split_at_position1_complete(|item| !item.is_ascii_lowercase(), ErrorKind::Alpha) 17 | } 18 | 19 | //------------------------------------------------------------------------------------------------------------------- 20 | 21 | /// Recognizes uppercase letters. 22 | fn uppercase_alpha1(input: Span) -> IResult 23 | { 24 | input.split_at_position1_complete(|item| !item.is_ascii_uppercase(), ErrorKind::Alpha) 25 | } 26 | 27 | //------------------------------------------------------------------------------------------------------------------- 28 | 29 | /// Parses a snake-case identifier from the input. 30 | /// 31 | /// The identifier must contain only lower-case letters, numbers, and underscores, and must start with a letter. 32 | pub(crate) fn snake_identifier(input: Span) -> IResult 33 | { 34 | recognize(tuple(( 35 | lowercase_alpha1, 36 | many0_count(alt((lowercase_alpha1, digit1, recognize(char('_'))))), 37 | ))) 38 | .parse(input) 39 | } 40 | 41 | //------------------------------------------------------------------------------------------------------------------- 42 | 43 | // Not currently used anywhere. 44 | // /// Parses a snake-case identifier from the input. 45 | // /// 46 | // /// The identifier must contain only lower-case letters, numbers, and underscores, and must start with a letter 47 | // or /// number. 48 | // pub(crate) fn numerical_snake_identifier(input: Span) -> IResult 49 | // { 50 | // recognize(tuple(( 51 | // alt((lowercase_alpha1, digit1)), 52 | // many0_count(alt((lowercase_alpha1, digit1, recognize(char('_'))))), 53 | // ))) 54 | // .parse(input) 55 | // } 56 | 57 | //------------------------------------------------------------------------------------------------------------------- 58 | 59 | /// Parses a camel-case identifier from the input. 60 | /// 61 | /// The identifier must contain only upper-case and lower-case letters and numbers, and must start with an 62 | /// upper-case letter. 63 | pub(crate) fn camel_identifier(input: Span) -> IResult 64 | { 65 | recognize(tuple((uppercase_alpha1, alphanumeric0))).parse(input) 66 | } 67 | 68 | //------------------------------------------------------------------------------------------------------------------- 69 | 70 | /// Parses an identifier from the input. 71 | /// 72 | /// The identifier must contain only upper and lower-case letters, numbers, and underscores. It must start with 73 | /// a letter or number. 74 | pub(crate) fn anything_identifier(input: Span) -> IResult 75 | { 76 | recognize(tuple(( 77 | alt((alpha1, digit1)), 78 | many0_count(alt((alpha1, digit1, recognize(char('_'))))), 79 | ))) 80 | .parse(input) 81 | } 82 | 83 | //------------------------------------------------------------------------------------------------------------------- 84 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/parsing/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod identifiers; 3 | mod recursion; 4 | mod span; 5 | 6 | pub use error::*; 7 | pub(crate) use identifiers::*; 8 | pub use recursion::*; 9 | pub use span::*; 10 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/parsing/recursion.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use crate::prelude::*; 4 | 5 | //------------------------------------------------------------------------------------------------------------------- 6 | 7 | thread_local! { 8 | static RECURSION_LIMIT: Cell = Cell::new(250); 9 | static RECURSION_COUNT: Cell = Cell::new(0); 10 | } 11 | 12 | //------------------------------------------------------------------------------------------------------------------- 13 | 14 | fn get_local_recursion_limit() -> u32 15 | { 16 | RECURSION_LIMIT.get() 17 | } 18 | 19 | //------------------------------------------------------------------------------------------------------------------- 20 | 21 | pub fn get_local_recursion_count() -> u32 22 | { 23 | RECURSION_COUNT.get() 24 | } 25 | 26 | //------------------------------------------------------------------------------------------------------------------- 27 | 28 | fn _set_local_recursion_limit(limit: u32) 29 | { 30 | RECURSION_LIMIT.set(limit); 31 | } 32 | 33 | //------------------------------------------------------------------------------------------------------------------- 34 | 35 | fn set_local_recursion_count(count: u32) 36 | { 37 | RECURSION_COUNT.set(count); 38 | } 39 | 40 | //------------------------------------------------------------------------------------------------------------------- 41 | 42 | /// Returns false if the count becomes >= the limit. 43 | fn try_increment_recursion_count() -> bool 44 | { 45 | let limit = get_local_recursion_limit(); 46 | let count = get_local_recursion_count(); 47 | 48 | if count + 1 >= limit { 49 | return false; 50 | } 51 | 52 | set_local_recursion_count(count + 1); 53 | 54 | true 55 | } 56 | 57 | //------------------------------------------------------------------------------------------------------------------- 58 | 59 | fn decrement_recursion_count() 60 | { 61 | let count = get_local_recursion_count(); 62 | set_local_recursion_count(count.saturating_sub(1)); 63 | } 64 | 65 | //------------------------------------------------------------------------------------------------------------------- 66 | 67 | /// Recursion-count a parser callback. 68 | /// 69 | /// TODO: Calling this increases the stack by 2 layers. One for `rc()`, and another for the callback. 70 | pub fn rc<'a, T>( 71 | content: Span<'a>, 72 | callback: impl FnOnce(Span<'a>) -> Result>, 73 | ) -> Result> 74 | { 75 | if !try_increment_recursion_count() { 76 | tracing::warn!("aborting COB parse at {:?}; exceeded recursion limit of {}", 77 | get_location(content).as_str(), get_local_recursion_limit()); 78 | return Err(span_verify_failure(content)); 79 | } 80 | let res = (callback)(content); 81 | decrement_recursion_count(); 82 | res 83 | } 84 | 85 | //------------------------------------------------------------------------------------------------------------------- 86 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/parsing/span.rs: -------------------------------------------------------------------------------- 1 | use nom::error::ErrorKind; 2 | use nom_locate::LocatedSpan; 3 | 4 | //------------------------------------------------------------------------------------------------------------------- 5 | 6 | /// Metadata passed along with [`Span`] for error messages. 7 | #[derive(Debug, Copy, Clone)] 8 | pub struct CobLocationMetadata<'a> 9 | { 10 | /// The name of the COB file being parsed. 11 | pub file: &'a str, 12 | } 13 | 14 | //------------------------------------------------------------------------------------------------------------------- 15 | 16 | /// Type alias for [`LocatedSpan`]. Used in [`Cob`](crate::prelude::Cob) parsing for identifying the location of 17 | /// errors. 18 | pub type Span<'a> = LocatedSpan<&'a str, CobLocationMetadata<'a>>; 19 | 20 | /// Type alias for span errors. 21 | pub type SpanError<'a> = nom::Err>>; 22 | 23 | //------------------------------------------------------------------------------------------------------------------- 24 | 25 | /// Converts a [`Span`] to a formatted location. 26 | pub fn get_location(span: Span) -> String 27 | { 28 | format!("file: {}, line: {}, column: {}", span.extra.file, span.location_line(), span.get_utf8_column()) 29 | } 30 | 31 | //------------------------------------------------------------------------------------------------------------------- 32 | 33 | /// Makes a [`SpanError`] for a specific error code while parsing. 34 | pub fn span_error(content: Span, code: ErrorKind) -> SpanError 35 | { 36 | nom::Err::Error(nom::error::Error { input: content, code }) 37 | } 38 | 39 | //------------------------------------------------------------------------------------------------------------------- 40 | 41 | /// Makes a [`SpanError`] for a verification error while parsing. 42 | pub fn span_verify_error(content: Span) -> SpanError 43 | { 44 | span_error(content, ErrorKind::Verify) 45 | } 46 | 47 | //------------------------------------------------------------------------------------------------------------------- 48 | 49 | /// Makes an unrecoverable [`SpanError`]. 50 | pub fn span_failure(content: Span, code: ErrorKind) -> SpanError 51 | { 52 | nom::Err::Failure(nom::error::Error { input: content, code }) 53 | } 54 | 55 | //------------------------------------------------------------------------------------------------------------------- 56 | 57 | /// Makes a [`SpanError`] for a verification failure while parsing (not recoverable). 58 | pub fn span_verify_failure(content: Span) -> SpanError 59 | { 60 | span_failure(content, ErrorKind::Verify) 61 | } 62 | 63 | //------------------------------------------------------------------------------------------------------------------- 64 | 65 | /// Extracts the span that a [`SpanError`] references. 66 | pub fn unwrap_error_content(error: SpanError) -> Span 67 | { 68 | let nom::Err::Error(nom::error::Error { input, .. }) = error else { 69 | panic!("failed unwrapping span error content from {error:?}"); 70 | }; 71 | input 72 | } 73 | 74 | //------------------------------------------------------------------------------------------------------------------- 75 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/resolver/cob_resolver.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | //------------------------------------------------------------------------------------------------------------------- 4 | 5 | /// Collection of resolvers for `CobLoadables`. 6 | #[derive(Default, Debug)] 7 | pub struct CobLoadableResolver 8 | { 9 | pub constants: ConstantsResolver, 10 | } 11 | 12 | impl CobLoadableResolver 13 | { 14 | pub fn start_new_file(&mut self) 15 | { 16 | self.constants.start_new_file(); 17 | } 18 | 19 | pub fn end_new_file(&mut self) 20 | { 21 | self.constants.end_new_file(); 22 | } 23 | 24 | pub fn append(&mut self, alias: &str, to_append: &Self) 25 | { 26 | self.constants.append(alias, &to_append.constants); 27 | } 28 | } 29 | 30 | //------------------------------------------------------------------------------------------------------------------- 31 | 32 | /// Collection of resolvers for `CobSceneLayers`. 33 | #[derive(Default, Debug)] 34 | pub struct CobSceneResolver 35 | { 36 | pub scene_macros: SceneMacrosResolver, 37 | } 38 | 39 | impl CobSceneResolver 40 | { 41 | pub fn start_new_file(&mut self) 42 | { 43 | self.scene_macros.start_new_file(); 44 | } 45 | 46 | pub fn end_new_file(&mut self) 47 | { 48 | self.scene_macros.end_new_file(); 49 | } 50 | 51 | pub fn append(&mut self, alias: &str, to_append: &Self) 52 | { 53 | self.scene_macros.append(alias, &to_append.scene_macros); 54 | } 55 | } 56 | 57 | //------------------------------------------------------------------------------------------------------------------- 58 | 59 | /// Collection of resolvers for `Cob` structures. 60 | #[derive(Default, Debug)] 61 | pub struct CobResolver 62 | { 63 | pub loadables: CobLoadableResolver, 64 | pub scenes: CobSceneResolver, 65 | } 66 | 67 | impl CobResolver 68 | { 69 | pub fn start_new_file(&mut self) 70 | { 71 | self.loadables.start_new_file(); 72 | self.scenes.start_new_file(); 73 | } 74 | 75 | pub fn end_new_file(&mut self) 76 | { 77 | self.loadables.end_new_file(); 78 | self.scenes.end_new_file(); 79 | } 80 | 81 | pub fn append(&mut self, alias: &str, to_append: &Self) 82 | { 83 | self.loadables.append(alias, &to_append.loadables); 84 | self.scenes.append(alias, &to_append.scenes); 85 | } 86 | } 87 | 88 | //------------------------------------------------------------------------------------------------------------------- 89 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/resolver/constants_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::sync::Arc; 4 | 5 | use smallvec::SmallVec; 6 | use smol_str::SmolStr; 7 | 8 | use super::{path_to_string, DEFS_SEPARATOR}; 9 | use crate::prelude::{CobConstantValue, CobFile}; 10 | 11 | //------------------------------------------------------------------------------------------------------------------- 12 | 13 | // [ identifier : constant value ] 14 | type ConstantsMap = HashMap; 15 | 16 | //------------------------------------------------------------------------------------------------------------------- 17 | 18 | /// Records a stack of constant maps. 19 | /// 20 | /// Used to efficiently merge constants when importing them into new files. 21 | #[derive(Default, Debug)] 22 | pub struct ConstantsResolver 23 | { 24 | stack: SmallVec<[(SmolStr, Arc); 5]>, 25 | new_file: ConstantsMap, 26 | } 27 | 28 | impl ConstantsResolver 29 | { 30 | pub(crate) fn start_new_file(&mut self) 31 | { 32 | self.new_file = HashMap::default(); 33 | } 34 | 35 | pub(crate) fn end_new_file(&mut self) 36 | { 37 | let map = std::mem::take(&mut self.new_file); 38 | self.stack.push((SmolStr::default(), Arc::new(map))); 39 | } 40 | 41 | /// Adds an entry to the new file being collected. 42 | pub fn insert(&mut self, file: &CobFile, name: SmolStr, value: CobConstantValue) 43 | { 44 | match self.new_file.entry(name) { 45 | Entry::Vacant(vacant) => { 46 | vacant.insert(value); 47 | } 48 | Entry::Occupied(mut occupied) => { 49 | tracing::warn!("overwriting constant definition ${} in {:?}", occupied.key().as_str(), file); 50 | occupied.insert(value); 51 | } 52 | } 53 | } 54 | 55 | /// Searches backward through the stack until a match is found. 56 | pub fn get(&self, path: impl AsRef) -> Option<&CobConstantValue> 57 | { 58 | let path = path.as_ref(); 59 | self.new_file.get(path).or_else(|| { 60 | self.stack.iter().rev().find_map(|(prefix, m)| { 61 | let stripped = path.strip_prefix(prefix.as_str())?; 62 | let cleaned = stripped.strip_prefix(DEFS_SEPARATOR).unwrap_or(stripped); 63 | m.get(cleaned) 64 | }) 65 | }) 66 | } 67 | 68 | pub fn append(&mut self, alias: &str, to_append: &Self) 69 | { 70 | // Remove duplicate maps in self. 71 | for (to_append_prefix, to_append) in to_append.stack.iter() { 72 | let new_to_append_prefix = path_to_string(DEFS_SEPARATOR, &[alias, &*to_append_prefix]); 73 | let Some(existing) = self.stack.iter().position(|(prefix, m)| { 74 | *prefix == new_to_append_prefix && Arc::as_ptr(m) == Arc::as_ptr(to_append) 75 | }) else { 76 | continue; 77 | }; 78 | self.stack.remove(existing); 79 | } 80 | 81 | // Append. 82 | self.stack.reserve(to_append.stack.len()); 83 | self.stack 84 | .extend(to_append.stack.iter().map(|(old_prefix, map)| { 85 | let new_prefix = path_to_string(DEFS_SEPARATOR, &[alias, &*old_prefix]); 86 | (new_prefix, map.clone()) 87 | })); 88 | } 89 | } 90 | 91 | //------------------------------------------------------------------------------------------------------------------- 92 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/resolver/mod.rs: -------------------------------------------------------------------------------- 1 | mod cob_resolver; 2 | mod constants_resolver; 3 | mod scene_macros_resolver; 4 | mod utils; 5 | 6 | pub use cob_resolver::*; 7 | pub use constants_resolver::*; 8 | pub use scene_macros_resolver::*; 9 | pub(self) use utils::*; 10 | -------------------------------------------------------------------------------- /crates/cobweb_asset_format/src/resolver/utils.rs: -------------------------------------------------------------------------------- 1 | use smol_str::SmolStr; 2 | 3 | //------------------------------------------------------------------------------------------------------------------- 4 | 5 | pub(super) const DEFS_SEPARATOR: &str = "::"; 6 | 7 | //------------------------------------------------------------------------------------------------------------------- 8 | 9 | pub(super) fn path_to_string>(separator: &str, path: &[T]) -> SmolStr 10 | { 11 | // skip empties and concatenate: a::b::c 12 | let mut count = 0; 13 | SmolStr::from_iter( 14 | path.iter() 15 | .filter(|p| !p.as_ref().is_empty()) 16 | .flat_map(|p| { 17 | count += 1; 18 | match count { 19 | 1 => ["", p.as_ref()], 20 | _ => [separator, p.as_ref()], 21 | } 22 | }), 23 | ) 24 | } 25 | 26 | //------------------------------------------------------------------------------------------------------------------- 27 | -------------------------------------------------------------------------------- /crates/sickle_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cob_sickle_macros" 3 | version = "0.7.0" 4 | edition = "2021" 5 | description = "Macros for sickle_ui" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/UkoeHB/bevy_cobweb_ui" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | syn = { version = "2.0", features = ["extra-traits"] } 14 | quote = "1.0" 15 | proc-macro2 = "1.0" 16 | -------------------------------------------------------------------------------- /crates/sickle_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod style_commands; 2 | 3 | use proc_macro::TokenStream; 4 | use syn::DeriveInput; 5 | 6 | #[proc_macro_derive( 7 | StyleCommands, 8 | attributes( 9 | static_style_only, 10 | skip_enity_command, 11 | skip_ui_style_ext, 12 | skip_lockable_enum, 13 | animatable, 14 | target_enum, 15 | target_tupl, 16 | target_component, 17 | target_component_attr, 18 | ) 19 | )] 20 | pub fn style_commands_macro_derive(input: TokenStream) -> TokenStream 21 | { 22 | let ast: DeriveInput = syn::parse(input.clone()).unwrap(); 23 | style_commands::derive_style_commands_macro(&ast) 24 | } 25 | -------------------------------------------------------------------------------- /crates/sickle_math/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cob_sickle_math" 3 | version = "0.7.0" 4 | edition = "2021" 5 | description = "Math dependencies for sickle_ui" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/UkoeHB/bevy_cobweb_ui" 8 | 9 | [dependencies] 10 | bevy.workspace = true 11 | serde.workspace = true 12 | -------------------------------------------------------------------------------- /crates/sickle_math/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ease; 2 | mod lerp; 3 | 4 | pub use ease::*; 5 | pub use lerp::*; 6 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cob_sickle_ui_scaffold" 3 | version = "0.7.0" 4 | edition = "2021" 5 | description = "Scaffolding framework for sickle_ui" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/UkoeHB/bevy_cobweb_ui" 8 | 9 | [features] 10 | dev = ["bevy/dynamic_linking"] 11 | disable-ui-context-placeholder-warn = [] 12 | 13 | [dependencies] 14 | cob_sickle_math = { path = "../sickle_math", version = "0.7.0" } 15 | cob_sickle_macros = { path = "../sickle_macros", version = "0.7.0" } 16 | 17 | bevy.workspace = true 18 | dyn-clone.workspace = true 19 | serde.workspace = true 20 | smallvec.workspace = true 21 | smol_str.workspace = true 22 | tracing.workspace = true 23 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/attributes/mod.rs: -------------------------------------------------------------------------------- 1 | mod custom_attrs; 2 | mod dynamic_style; 3 | mod dynamic_style_attribute; 4 | mod pseudo_state; 5 | mod style_animation; 6 | mod traits; 7 | mod ui_context; 8 | 9 | pub use custom_attrs::*; 10 | pub use dynamic_style::*; 11 | pub use dynamic_style_attribute::*; 12 | pub use pseudo_state::*; 13 | pub use style_animation::*; 14 | pub use traits::*; 15 | pub use ui_context::*; 16 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/attributes/traits.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::fmt::Debug; 3 | 4 | use bevy::prelude::*; 5 | use dyn_clone::DynClone; 6 | 7 | use crate::*; 8 | 9 | //------------------------------------------------------------------------------------------------------------------- 10 | 11 | /// Helper trait for storing custom static attributes. 12 | pub trait StaticAttributeObject: Any + DynClone + Debug + Send + Sync + 'static 13 | { 14 | /// Convert self to an `Any` reference. 15 | fn as_any(&self) -> &dyn Any; 16 | /// Convert self to a mutable `Any` reference. 17 | fn as_any_mut(&mut self) -> &mut dyn Any; 18 | 19 | /// Applies the attribute to the target entity. 20 | fn apply(&self, entity: Entity, world: &mut World); 21 | } 22 | 23 | //------------------------------------------------------------------------------------------------------------------- 24 | 25 | /// Helper trait for storing custom responsive attributes. 26 | pub trait ResponsiveAttributeObject: Any + DynClone + Debug + Send + Sync + 'static 27 | { 28 | /// Convert self to an `Any` reference. 29 | fn as_any(&self) -> &dyn Any; 30 | /// Convert self to a mutable `Any` reference. 31 | fn as_any_mut(&mut self) -> &mut dyn Any; 32 | 33 | /// Applies the attribute to the target entity. 34 | fn apply(&self, entity: Entity, world: &mut World, state: FluxInteraction); 35 | } 36 | 37 | //------------------------------------------------------------------------------------------------------------------- 38 | 39 | /// Helper trait for storing custom animated attributes. 40 | pub trait AnimatedAttributeObject: Any + DynClone + Debug + Send + Sync + 'static 41 | { 42 | /// Convert self to an `Any` reference. 43 | fn as_any(&self) -> &dyn Any; 44 | /// Convert self to a mutable `Any` reference. 45 | fn as_any_mut(&mut self) -> &mut dyn Any; 46 | 47 | /// Initializes self when begining to enter the idle state. 48 | fn initialize_enter(&mut self, entity: Entity, world: &World); 49 | 50 | /// Applies the attribute to the target entity. 51 | fn apply(&self, entity: Entity, world: &mut World, state: AnimationState); 52 | } 53 | 54 | //------------------------------------------------------------------------------------------------------------------- 55 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/attributes/ui_context.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | pub trait UiContext 4 | { 5 | fn get(&self, _target: &str) -> Result 6 | { 7 | Err(format!( 8 | "{} has no UI contexts", 9 | std::any::type_name::() 10 | )) 11 | } 12 | 13 | /// These are the contexts cleared by the parent theme when no DynamicStyle 14 | /// is placed to them. 15 | /// 16 | /// By default this is the full list of contexts. 17 | /// 18 | /// Warning: If a context is a sub-widget with its own theme, it should not 19 | /// be included in the cleared contexts, nor should it be used for placement 20 | /// from the main entity. The behavior is undefined. 21 | fn cleared_contexts(&self) -> impl Iterator + '_ 22 | { 23 | self.contexts() 24 | } 25 | 26 | fn contexts(&self) -> impl Iterator + '_ 27 | { 28 | [].into_iter() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/builder_ext/container.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::*; 4 | 5 | pub trait UiContainerExt 6 | { 7 | fn container( 8 | &mut self, 9 | bundle: impl Bundle, 10 | spawn_children: impl FnOnce(&mut UiBuilder), 11 | ) -> UiBuilder; 12 | } 13 | 14 | impl UiContainerExt for UiBuilder<'_, UiRoot> 15 | { 16 | fn container( 17 | &mut self, 18 | bundle: impl Bundle, 19 | spawn_children: impl FnOnce(&mut UiBuilder), 20 | ) -> UiBuilder 21 | { 22 | let mut new_builder = self.spawn(bundle); 23 | spawn_children(&mut new_builder); 24 | 25 | new_builder 26 | } 27 | } 28 | 29 | impl UiContainerExt for UiBuilder<'_, Entity> 30 | { 31 | fn container( 32 | &mut self, 33 | bundle: impl Bundle, 34 | spawn_children: impl FnOnce(&mut UiBuilder), 35 | ) -> UiBuilder 36 | { 37 | let mut new_builder = self.spawn(bundle); 38 | spawn_children(&mut new_builder); 39 | 40 | new_builder 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/builder_ext/mod.rs: -------------------------------------------------------------------------------- 1 | mod container; 2 | 3 | pub use container::*; 4 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod attributes; 2 | mod builder_ext; 3 | mod flux_interaction; 4 | mod ui_builder; 5 | mod ui_commands; 6 | mod ui_style; 7 | mod ui_utils; 8 | 9 | pub use attributes::*; 10 | pub use builder_ext::*; 11 | pub use flux_interaction::*; 12 | pub use ui_builder::*; 13 | pub use ui_commands::*; 14 | pub use ui_style::*; 15 | pub use ui_utils::*; 16 | -------------------------------------------------------------------------------- /crates/sickle_ui_scaffold/src/ui_style/mod.rs: -------------------------------------------------------------------------------- 1 | mod attribute; 2 | mod builder; 3 | mod generated; 4 | mod manual; 5 | mod style; 6 | 7 | pub use attribute::*; 8 | pub use builder::*; 9 | pub use generated::*; 10 | pub use manual::*; 11 | pub use style::*; 12 | -------------------------------------------------------------------------------- /examples/calculator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "calculator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["dev"] 8 | dev = ["bevy_cobweb_ui/dev"] 9 | 10 | [dependencies] 11 | bevy.workspace = true 12 | bevy_cobweb.workspace = true 13 | bevy_cobweb_ui = { path = "../../", default-features = false, features = ["colors", "firasans_default"] } 14 | calc = { version = "0.4" } 15 | itertools = { version = "0.13" } 16 | rust_decimal = { version = "1.35" } 17 | tracing = { version = "0.1" } 18 | -------------------------------------------------------------------------------- /examples/calculator/assets/main.cob: -------------------------------------------------------------------------------- 1 | #import 2 | builtin.colors.tailwind as tw 3 | 4 | #defs 5 | $NORMAL_BUTTON = $tw::SLATE_500 6 | $HOVERED_BUTTON = $tw::SLATE_400 7 | $PRESSED_BUTTON = $tw::GREEN_400 8 | $BORDER_BUTTON = $tw::SLATE_400 9 | $BORDER_DISPLAY = $tw::SKY_950 10 | // Defaults to 'button' styling. 11 | +calculator_item = \ 12 | GridNode{ justify_main:Center justify_self_cross:Stretch } 13 | GridColumn{ span:1 } 14 | Splat(1px) 15 | Splat(20px) 16 | Splat(5px) 17 | BrRadius(5px) 18 | BorderColor($BORDER_BUTTON) 19 | Responsive{ idle:$NORMAL_BUTTON hover:$HOVERED_BUTTON press:$PRESSED_BUTTON } 20 | 21 | "text" 22 | FlexNode 23 | TextLine{ text:"" size:30 } 24 | \ 25 | 26 | #scenes 27 | "scene" 28 | GridNode{ grid_template_columns: [(Count(4) auto)] } 29 | Splat(auto) 30 | 31 | "button" 32 | +calculator_item{} 33 | 34 | "display" 35 | +calculator_item{ 36 | GridColumn{ span:3 } 37 | BrRadius(0px) 38 | BorderColor($BORDER_DISPLAY) 39 | -Responsive 40 | BackgroundColor($NORMAL_BUTTON) 41 | } 42 | -------------------------------------------------------------------------------- /examples/checkbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "checkbox" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["dev"] 8 | dev = ["bevy_cobweb_ui/dev"] 9 | 10 | [dependencies] 11 | bevy.workspace = true 12 | bevy_cobweb.workspace = true 13 | bevy_cobweb_ui = { path = "../../", default-features = false, features = ["widgets", "firasans_default"] } 14 | tracing = { version = "0.1" } 15 | -------------------------------------------------------------------------------- /examples/checkbox/assets/main.cob: -------------------------------------------------------------------------------- 1 | #defs 2 | $toggle_animation = {duration:0.2 ease:InOutSine} 3 | 4 | #scenes 5 | "scene" 6 | FlexNode{width:100vw height:100vh flex_direction:Column justify_main:SpaceEvenly justify_cross:Center} 7 | 8 | "basic" 9 | FlexNode{width: 200px flex_direction:Row justify_main:FlexStart justify_cross:Center} 10 | 11 | "checkbox" 12 | Checkbox // <-- Sets up a checkbox 13 | ControlRoot 14 | FlexNode{width:20px height:20px justify_main:Center justify_cross:Center} 15 | Splat(2px) 16 | BackgroundColor(#777777) 17 | BorderColor(#333333) 18 | 19 | "marker" 20 | ControlMember 21 | AbsoluteNode{top:auto left:auto} 22 | TextLine{text:"x" size:15} 23 | Multi>[{value:Hide} {state:[Checked] value:Show}] 24 | 25 | "text" 26 | FlexNode{margin:{left:10px}} 27 | TextLine 28 | 29 | "toggle" 30 | FlexNode{width: 200px flex_direction:Row justify_main:FlexStart justify_cross:Center} 31 | 32 | "checkbox" 33 | Checkbox // <-- Sets up a checkbox 34 | ControlRoot 35 | FlexNode{width:70px height:35px justify_main:Center justify_cross:Center} 36 | BrRadius(35px) 37 | Splat(2px) 38 | Multi>[ 39 | {idle:#777777 enter_idle_with:$toggle_animation delete_on_entered:true} 40 | {state:[Checked] idle:#3333FF enter_idle_with:$toggle_animation delete_on_entered:true} 41 | ] 42 | BorderColor(#333333) 43 | 44 | // Channel for the marker to move in. 45 | "channel" 46 | FlexNode{width:35px flex_direction:Row justify_main:Center justify_cross:Center} 47 | 48 | // Anchor for the marker. The anchor slides in the channel. 49 | "anchor" 50 | ControlMember 51 | AbsoluteNode{top:auto justify_main:Center justify_cross:Center} 52 | Multi>[ 53 | {idle:0% enter_idle_with:$toggle_animation delete_on_entered:true} 54 | {state:[Checked] idle:100% enter_idle_with:$toggle_animation delete_on_entered:true} 55 | ] 56 | 57 | // Marker centered on top of the anchor. 58 | "marker" 59 | AbsoluteNode{top:auto left:auto width:25px height:25px} 60 | BrRadius(25px) 61 | BackgroundColor(#33FF33) 62 | -------------------------------------------------------------------------------- /examples/checkbox/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Example demonstrating the checkbox widget. 2 | 3 | use bevy::prelude::*; 4 | use bevy::window::WindowTheme; 5 | use bevy_cobweb::prelude::*; 6 | use bevy_cobweb_ui::prelude::*; 7 | 8 | //------------------------------------------------------------------------------------------------------------------- 9 | 10 | fn build_ui(mut c: Commands, mut s: SceneBuilder) 11 | { 12 | let scene = ("main.cob", "scene"); 13 | c.ui_root().spawn_scene(scene, &mut s, |h| { 14 | h.edit("basic", |h| { 15 | let text_id = h.get_entity("text").unwrap(); 16 | 17 | h.edit("checkbox", |h| { 18 | let id = h.id(); 19 | // Use update_on so it also initializes the text. 20 | h.update_on(entity_event::(id), move |_: TargetId, mut e: TextEditor| { 21 | write_text!(e, text_id, "Unchecked"); 22 | }) 23 | .on_check(move |mut e: TextEditor| { 24 | write_text!(e, text_id, "Checked"); 25 | }); 26 | }); 27 | }); 28 | }); 29 | } 30 | 31 | //------------------------------------------------------------------------------------------------------------------- 32 | 33 | fn setup(mut c: Commands) 34 | { 35 | c.spawn(Camera2d); 36 | } 37 | 38 | //------------------------------------------------------------------------------------------------------------------- 39 | 40 | fn main() 41 | { 42 | App::new() 43 | .add_plugins(bevy::DefaultPlugins.set(WindowPlugin { 44 | primary_window: Some(Window { window_theme: Some(WindowTheme::Dark), ..default() }), 45 | ..default() 46 | })) 47 | .add_plugins(CobwebUiPlugin) 48 | .load("main.cob") 49 | .add_systems(PreStartup, setup) 50 | .add_systems(OnEnter(LoadState::Done), build_ui) 51 | .run(); 52 | } 53 | 54 | //------------------------------------------------------------------------------------------------------------------- 55 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["dev"] 8 | dev = ["bevy_cobweb_ui/dev"] 9 | 10 | [dependencies] 11 | bevy.workspace = true 12 | bevy_cobweb.workspace = true 13 | bevy_cobweb_ui = { path = "../../", default-features = false, features = ["firasans_default"] } 14 | tracing = { version = "0.1" } 15 | -------------------------------------------------------------------------------- /examples/counter/assets/main.cob: -------------------------------------------------------------------------------- 1 | #scenes 2 | // Custom node structure for the example app. 3 | "root" 4 | // Root node covers the window. 5 | FlexNode{ width:100vw height:100vh justify_main:SpaceEvenly justify_cross:Center } 6 | 7 | // Sets up a button with centered content, that animates its background in response to hovers/presses. 8 | "button" 9 | ControlRoot 10 | FlexNode{ justify_main:Center justify_cross:Center } 11 | Animated{ idle:#007000 hover:#006200 press:#005500 } 12 | 13 | // Sets up the button's text as a single line of text with margin to control the edges of the button. 14 | "text" 15 | ControlMember 16 | FlexNode{ margin:{top:10px bottom:10px left:18px right:18px} } 17 | TextLine{ size:50 } 18 | Animated{ idle:#05080F hover:#5080F0 press:#4070E0 } 19 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates building a simple counter as a small reactive scene. 2 | 3 | use bevy::prelude::*; 4 | use bevy::window::WindowTheme; 5 | use bevy_cobweb::prelude::*; 6 | use bevy_cobweb_ui::prelude::*; 7 | 8 | //------------------------------------------------------------------------------------------------------------------- 9 | 10 | #[derive(ReactComponent, Deref, Default, Debug, Clone)] 11 | struct Counter(usize); 12 | 13 | impl Counter 14 | { 15 | fn increment(&mut self) 16 | { 17 | self.0 += 1; 18 | } 19 | } 20 | 21 | //------------------------------------------------------------------------------------------------------------------- 22 | 23 | fn build_ui(mut c: Commands, mut s: SceneBuilder) 24 | { 25 | let scene = ("main.cob", "root"); 26 | c.ui_root().spawn_scene(scene, &mut s, |h| { 27 | h.edit("button", |h| { 28 | let button_id = h.id(); 29 | h.insert_reactive(Counter(0)).on_pressed( 30 | move |mut c: Commands, mut counters: ReactiveMut| { 31 | counters 32 | .get_mut(&mut c, button_id) 33 | .map(Counter::increment)?; 34 | OK 35 | }, 36 | ); 37 | 38 | h.get("text").update_on( 39 | entity_mutation::(button_id), 40 | move |id: TargetId, mut e: TextEditor, counters: Reactive| { 41 | let counter = counters.get(button_id)?; 42 | write_text!(e, id, "Counter: {}", **counter); 43 | OK 44 | }, 45 | ); 46 | }); 47 | }); 48 | } 49 | 50 | //------------------------------------------------------------------------------------------------------------------- 51 | 52 | fn setup(mut commands: Commands) 53 | { 54 | commands.spawn(Camera2d); 55 | } 56 | 57 | //------------------------------------------------------------------------------------------------------------------- 58 | 59 | fn main() 60 | { 61 | App::new() 62 | .add_plugins(bevy::DefaultPlugins.set(WindowPlugin { 63 | primary_window: Some(Window { window_theme: Some(WindowTheme::Dark), ..default() }), 64 | ..default() 65 | })) 66 | .add_plugins(CobwebUiPlugin) 67 | .load("main.cob") 68 | .add_systems(PreStartup, setup) 69 | .add_systems(OnEnter(LoadState::Done), build_ui) 70 | .run(); 71 | } 72 | 73 | //------------------------------------------------------------------------------------------------------------------- 74 | -------------------------------------------------------------------------------- /examples/cursors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cursors" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["dev"] 8 | dev = ["bevy_cobweb_ui/dev"] 9 | 10 | [dependencies] 11 | bevy.workspace = true 12 | bevy_cobweb.workspace = true 13 | bevy_cobweb_ui = { path = "../../", default-features = false } 14 | tracing = { version = "0.1" } 15 | 16 | serde = { version = "1.0" } 17 | -------------------------------------------------------------------------------- /examples/cursors/assets/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UkoeHB/bevy_cobweb_ui/f48fae2cf155286a66de409ddb3ce619aacd49ee/examples/cursors/assets/cursor.png -------------------------------------------------------------------------------- /examples/cursors/assets/main.cob: -------------------------------------------------------------------------------- 1 | #commands 2 | PrimaryCursor(Custom{image:"cursor.png" hotspot:(9, 9)}) 3 | 4 | #scenes 5 | "scene" 6 | FlexNode{width:100vw height:100vh justify_main:Center justify_cross:Center} 7 | 8 | "box" 9 | FlexNode{width:200px height:200px justify_main:Center justify_cross:Center} 10 | BackgroundColor(#00BB00) 11 | ResponsiveCursor{ hover: System(Move) } 12 | 13 | "inner" 14 | FlexNode{width:100px height:100px} 15 | BackgroundColor(#0000FF) 16 | ResponsiveCursor{ hover: System(Grab), press: System(Grabbing) } 17 | FocusPolicy::Block // This prevents interactions from reaching the lower node and causing cursor race conditions. 18 | -------------------------------------------------------------------------------- /examples/cursors/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates setting custom cursors that respond to interactions on UI elements. 2 | 3 | use bevy::prelude::*; 4 | use bevy::window::WindowTheme; 5 | use bevy_cobweb_ui::prelude::*; 6 | 7 | //------------------------------------------------------------------------------------------------------------------- 8 | 9 | fn build_ui(mut c: Commands, mut s: SceneBuilder) 10 | { 11 | c.ui_root() 12 | .spawn_scene_simple(("main.cob", "scene"), &mut s); 13 | } 14 | 15 | //------------------------------------------------------------------------------------------------------------------- 16 | 17 | fn main() 18 | { 19 | App::new() 20 | .add_plugins(bevy::DefaultPlugins.set(WindowPlugin { 21 | primary_window: Some(Window { window_theme: Some(WindowTheme::Dark), ..default() }), 22 | ..default() 23 | })) 24 | .add_plugins(CobwebUiPlugin) 25 | .load("main.cob") 26 | .add_systems(PreStartup, |mut c: Commands| { 27 | c.spawn(Camera2d); 28 | }) 29 | .add_systems(OnEnter(LoadState::Done), build_ui) 30 | .run(); 31 | } 32 | 33 | //------------------------------------------------------------------------------------------------------------------- 34 | -------------------------------------------------------------------------------- /examples/editor_demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor_demo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | default = ["dev", "editor"] 8 | dev = ["bevy_cobweb_ui/dev"] 9 | editor = ["bevy_cobweb_ui/editor"] 10 | 11 | [dependencies] 12 | bevy.workspace = true 13 | bevy_cobweb.workspace = true 14 | bevy_cobweb_ui = { path = "../../", default-features = false, features = ["colors", "firasans_default"] } 15 | rand = { version = "0.8" } 16 | rand_chacha = { version = "0.3" } 17 | tracing = { version = "0.1" } 18 | 19 | serde = { version = "1.0" } 20 | -------------------------------------------------------------------------------- /examples/editor_demo/assets/editor_ext.cob: -------------------------------------------------------------------------------- 1 | #import 2 | builtin.colors.tailwind as tw 3 | 4 | #scenes 5 | "orbiter_widget" 6 | FlexNode{flex_direction:Column justify_main:FlexStart justify_cross:FlexStart} 7 | FocusPolicy::Block 8 | 9 | "field_widget" 10 | FlexNode{ 11 | margin:{bottom:4px} 12 | flex_direction:Row justify_main:FlexStart justify_cross:Center 13 | } 14 | 15 | "name" 16 | FlexNode{margin:{right:5px}} 17 | TextLine{size:14} 18 | 19 | "lower_bound" 20 | FlexNode{margin:{right:6px}} 21 | TextLine{size:14} 22 | 23 | "value" 24 | FlexNode{width:65px flex_direction:Row justify_main:Center justify_cross:Center} 25 | BrRadius(4px) 26 | Splat(1px) 27 | BorderColor(#99FFFFFF) 28 | Responsive{idle:#00000000 hover:#66888888} 29 | ResponsiveCursor{hover:System(ColResize)} 30 | 31 | "text" 32 | FlexNode{margin:{top:5px bottom:5px}} 33 | TextLine{size:14} 34 | 35 | "upper_bound" 36 | FlexNode{margin:{left:5px}} 37 | TextLine{size:14} 38 | -------------------------------------------------------------------------------- /examples/editor_demo/assets/main.cob: -------------------------------------------------------------------------------- 1 | #manifest 2 | "editor_ext.cob" as editor_ext 3 | 4 | #scenes 5 | "orbit" 6 | Orbiter{radius:9.609539 velocity:6.5658884} 7 | -------------------------------------------------------------------------------- /examples/editor_demo/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "editor")] 2 | pub mod editor_ext; 3 | pub mod orbiter; 4 | pub mod rng; 5 | 6 | use std::f32::consts::TAU; 7 | 8 | use bevy::prelude::*; 9 | use bevy_cobweb_ui::prelude::*; 10 | use rand::Rng; 11 | 12 | //------------------------------------------------------------------------------------------------------------------- 13 | 14 | const SCREEN_HALF_WIDTH: f32 = 400.0; 15 | const SCREEN_HALF_HEIGHT: f32 = 300.0; 16 | 17 | //------------------------------------------------------------------------------------------------------------------- 18 | 19 | fn spawn_scene_simples( 20 | mut c: Commands, 21 | mut s: SceneBuilder, 22 | mut rng: ResMut, 23 | mut meshes: ResMut>, 24 | mut materials: ResMut>, 25 | ) 26 | { 27 | c.spawn(Camera2d); 28 | 29 | let rng = rng.rng(); 30 | let shape = meshes.add(Circle::new(50.0)); 31 | let color = materials.add(Color::from(bevy::color::palettes::tailwind::ORANGE_600)); 32 | 33 | for _ in 0..20 { 34 | c.spawn_scene(("main.cob", "orbit"), &mut s, |h| { 35 | // Random starting location and angle. 36 | let start_x = rng.gen_range(-SCREEN_HALF_WIDTH..=SCREEN_HALF_WIDTH); 37 | let start_y = rng.gen_range(-SCREEN_HALF_HEIGHT..=SCREEN_HALF_HEIGHT); 38 | let start_radial = rng.gen_range((0.)..TAU); 39 | 40 | h.insert(( 41 | Mesh2d(shape.clone()), 42 | MeshMaterial2d(color.clone()), 43 | orbiter::Orbit::new(Vec2::new(start_x, start_y), start_radial), 44 | )); 45 | }); 46 | } 47 | } 48 | 49 | //------------------------------------------------------------------------------------------------------------------- 50 | 51 | fn main() 52 | { 53 | let mut app = App::new(); 54 | 55 | app.add_plugins(bevy::DefaultPlugins.set(WindowPlugin { 56 | primary_window: Some(Window { 57 | window_theme: Some(bevy::window::WindowTheme::Dark), 58 | ..default() 59 | }), 60 | ..default() 61 | })) 62 | .add_plugins(CobwebUiPlugin) 63 | .add_plugins(orbiter::DemoOrbiterPlugin) 64 | .insert_resource(rng::DemoRng::new(0)) 65 | .load("main.cob") 66 | .add_systems(OnEnter(LoadState::Done), spawn_scene_simples); 67 | 68 | #[cfg(feature = "editor")] 69 | app.add_plugins(editor_ext::DemoEditorExtPlugin); 70 | 71 | app.run(); 72 | } 73 | 74 | //------------------------------------------------------------------------------------------------------------------- 75 | -------------------------------------------------------------------------------- /examples/editor_demo/src/orbiter.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | use std::time::Duration; 3 | 4 | use bevy::prelude::*; 5 | use bevy_cobweb_ui::prelude::*; 6 | use bevy_cobweb_ui::sickle::ApplyFluxChanges; 7 | 8 | //------------------------------------------------------------------------------------------------------------------- 9 | 10 | const MAX_RADIUS: f32 = 200.0; 11 | const MIN_RADIUS: f32 = 0.0; 12 | const MAX_VELOCITY: f32 = 25.0; 13 | const MIN_VELOCITY: f32 = -MAX_VELOCITY; 14 | 15 | //------------------------------------------------------------------------------------------------------------------- 16 | 17 | fn update_oribiters(time: Res