├── richtext ├── assets ├── Cargo.toml └── src │ ├── modifiers │ ├── rich_impl.rs │ └── cresus_impl.rs │ └── modifiers.rs ├── widges ├── src │ ├── widge │ │ ├── labelled.rs │ │ ├── visual.rs │ │ ├── list.rs │ │ ├── event_button.rs │ │ ├── checkbox.rs │ │ ├── composed.rs │ │ └── action_button.rs │ ├── activation.rs │ ├── lib.rs │ ├── from_world_entity.rs │ ├── read_world_value.rs │ ├── widge.rs │ └── prefab.rs ├── Cargo.toml └── README.md ├── fab ├── src │ ├── resolve │ │ ├── make_item.rs │ │ ├── impl_fmt.rs │ │ ├── make │ │ │ ├── mask_range.rs │ │ │ └── is_static.rs │ │ └── minimal.rs │ ├── lib.rs │ ├── dummy_modify.rs │ ├── binding │ │ └── entry.rs │ └── modify.rs ├── README.md ├── Cargo.toml └── examples │ └── impl_modify.rs ├── .gitignore ├── assets ├── lock.png ├── rarrow.png ├── green_circle.png └── fonts │ ├── FiraSans-Bold.ttf │ ├── FiraMono-Medium.ttf │ └── FiraMono-LICENSE ├── clippy.toml ├── rustfmt.toml ├── .cargo └── config.toml ├── design_doc ├── styling_widges.md ├── state_of_the_art.md ├── fab │ ├── binding_source_perf.md │ ├── higher_level_templates.md │ ├── binding_2_modify_dependency.md │ ├── track_nested_field.md │ └── track_a_field.md ├── fine-grained-component-access.md ├── activation │ ├── scoped_systems.md │ └── old_school_tree.md ├── layout │ ├── when_is_PosRect_updated.md │ ├── render_layer_handling.md │ └── better_layout_containers.md ├── hierarchy-scoped-queries.md ├── richtext │ ├── hlist_interpreting.md │ ├── nested_dependency_implementation.md │ ├── post_process_content.md │ ├── better_section_impl.md │ ├── change_detection.md │ ├── cresus_linebreak.md │ ├── system-as-modify.md │ ├── dynamic_format.md │ ├── cresustext.md │ └── informal_grammar.md ├── birds_eye_view.md └── datazoo │ └── bit_assocarray.md ├── Cargo.toml ├── Makefile ├── bevy_fab ├── src │ ├── track.rs │ ├── fmt_system.rs │ ├── local.rs │ ├── trait_extensions.rs │ ├── lib.rs │ ├── track │ │ └── write.rs │ └── make.rs └── Cargo.toml ├── reflect_query ├── src │ ├── lib.rs │ ├── custom_ref.rs │ └── predefined.rs └── Cargo.toml ├── bevy_layout_offset ├── Cargo.toml ├── src │ └── lib.rs └── README.md ├── fab_derive ├── Cargo.toml ├── src │ ├── extensions.rs │ └── lib.rs └── README.md ├── datazoo ├── Cargo.toml ├── README.md └── src │ ├── enum_multimap.rs │ ├── index_multimap.rs │ ├── lib.rs │ ├── enum_bitmatrix.rs │ ├── index_map.rs │ ├── jagged_vec.rs │ └── bitmatrix.rs ├── layout ├── Cargo.toml ├── src │ ├── render.rs │ └── error.rs └── README.md ├── fab_parse ├── Cargo.toml └── src │ ├── hook.rs │ ├── error.rs │ └── rt_fmt.rs └── licenses └── LICENSE-MIT /richtext/assets: -------------------------------------------------------------------------------- 1 | ../assets/ -------------------------------------------------------------------------------- /widges/src/widge/labelled.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fab/src/resolve/make_item.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /widges/src/widge/visual.rs: -------------------------------------------------------------------------------- 1 | //! A static image. 2 | -------------------------------------------------------------------------------- /widges/src/activation.rs: -------------------------------------------------------------------------------- 1 | /*! Scope-limited systems */ 2 | -------------------------------------------------------------------------------- /fab/README.md: -------------------------------------------------------------------------------- 1 | A Reactive programming framework with no state management. 2 | -------------------------------------------------------------------------------- /assets/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicopap/cuicui/HEAD/assets/lock.png -------------------------------------------------------------------------------- /assets/rarrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicopap/cuicui/HEAD/assets/rarrow.png -------------------------------------------------------------------------------- /assets/green_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicopap/cuicui/HEAD/assets/green_circle.png -------------------------------------------------------------------------------- /assets/fonts/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicopap/cuicui/HEAD/assets/fonts/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicopap/cuicui/HEAD/assets/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # Bevy systems can have a lot of arguments 2 | too-many-arguments-threshold = 16 3 | type-complexity-threshold = 600 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | single_line_if_else_max_width = 66 3 | struct_lit_width = 50 4 | use_field_init_shorthand = true 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | linker="clang" 3 | rustflags = [ 4 | "-Clink-arg=-fuse-ld=/usr/bin/mold", 5 | "-Clink-arg=-Wl,--as-needed", 6 | "-Clink-arg=-Wl,--compress-debug-sections=zlib-gabi", #zlib-gabi 7 | ] -------------------------------------------------------------------------------- /design_doc/styling_widges.md: -------------------------------------------------------------------------------- 1 | # Styling widges 2 | 3 | Widges should really just define behaviors, somehow they however need to 4 | communicate their state to external systems so that they 5 | 6 | --> hmm, maybe letting users read `ReadWorldValue` is fine? 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bevy_fab", 4 | "bevy_layout_offset", 5 | "datazoo", 6 | "fab", 7 | "fab_derive", 8 | "layout", 9 | "reflect_query", 10 | "richtext", 11 | "widges", 12 | ] 13 | 14 | resolver = "2" 15 | -------------------------------------------------------------------------------- /widges/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod activation; 2 | pub mod from_world_entity; 3 | pub mod prefab; 4 | pub mod read_world_value; 5 | pub mod widge; 6 | 7 | pub use from_world_entity::ExtractPrefab; 8 | pub use prefab::Prefab; 9 | pub use read_world_value::WorldValue; 10 | pub use widge::Widge; 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: 2 | cargo clippy --examples --workspace --all-targets --all-features 3 | run: 4 | # cargo run --package cuicui_richtext --example richtext 5 | cargo run --package cuicui_richtext --example breakout 6 | # cargo test --workspace --features winnow/debug 7 | # cargo test --workspace 8 | -------------------------------------------------------------------------------- /bevy_fab/src/track.rs: -------------------------------------------------------------------------------- 1 | //! Tracker structs to easily insert into ECS components you want to read 2 | //! into modifiers. 3 | 4 | mod read; 5 | mod write; 6 | 7 | pub use read::{GetError, ParseError, Read}; 8 | pub(crate) use write::UserFmts; 9 | pub use write::{Error as WriteError, UserFmt, Write}; 10 | -------------------------------------------------------------------------------- /fab/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod binding; 2 | mod dummy_modify; 3 | pub mod modify; 4 | pub mod resolve; 5 | 6 | pub use fab_derive::impl_modify; 7 | pub use modify::Modify; 8 | 9 | #[doc(hidden)] 10 | pub mod __private { 11 | pub use crate::dummy_modify::DummyModify; 12 | pub use anyhow; 13 | pub use enumset; 14 | } 15 | -------------------------------------------------------------------------------- /reflect_query/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(clippy::nursery, clippy::pedantic, missing_docs)] 3 | #![allow(clippy::use_self)] 4 | 5 | mod custom_ref; 6 | pub mod predefined; 7 | pub mod queries; 8 | mod queryable; 9 | 10 | pub use custom_ref::Ref; 11 | pub use queryable::{ReflectQueryable, ReflectQueryableFns}; 12 | -------------------------------------------------------------------------------- /design_doc/state_of_the_art.md: -------------------------------------------------------------------------------- 1 | https://raphlinus.github.io/ui/druid/2019/11/22/reactive-ui.html 2 | 3 | https://web.archive.org/web/20160310063221/http://repository.cmu.edu/cgi/viewcontent.cgi?article=1761&context=isr 4 | 5 | https://tonsky.me/blog/humble-state/ 6 | 7 | https://blog.metaobject.com/2014/03/the-siren-call-of-kvo-and-cocoa-bindings.html 8 | 9 | -------------------------------------------------------------------------------- /widges/src/from_world_entity.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{SystemParam, SystemParamItem}, 3 | prelude::Entity, 4 | }; 5 | 6 | /// A thing assoicated with an entity that can be read from the world. 7 | pub trait ExtractPrefab: Sized { 8 | type ExtractParam<'w, 's>: SystemParam; 9 | fn extract( 10 | entity: Entity, 11 | params: &SystemParamItem>, 12 | ) -> Option; 13 | } 14 | -------------------------------------------------------------------------------- /widges/src/read_world_value.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{SystemParam, SystemParamItem}, 3 | prelude::Entity, 4 | }; 5 | 6 | /// Something spawned in the world which value can be read. 7 | pub trait WorldValue { 8 | type Value; 9 | type ReadParam<'w, 's>: SystemParam; 10 | fn read( 11 | entity: Entity, 12 | params: &SystemParamItem>, 13 | ) -> Option; 14 | } 15 | -------------------------------------------------------------------------------- /design_doc/fab/binding_source_perf.md: -------------------------------------------------------------------------------- 1 | # Binding source implementation 2 | 3 | Problem: Entirely reyling on `ReflectComponent` for `Component`-based binding 4 | sources is extremely inefficient. 5 | 6 | Basically no way to get from a `ReflectComponent` to something in the world. 7 | 8 | Solution: Define your own `TypeData` that stores a function taking a world 9 | and returning list of entities with given componet. 10 | 11 | Added bonus is that it's possible to add it to pre-existing components. -------------------------------------------------------------------------------- /bevy_layout_offset/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_bevy_layout_offset" 3 | authors = ["Nicola Papale"] 4 | description = "A small bevy plugin to manipulate UI element transform" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "ui"] 8 | categories = ["game-development"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | bevy = { version = "0.10", default-features = false, features = ["bevy_ui"] } 15 | -------------------------------------------------------------------------------- /fab_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_fab_derive" 3 | authors = ["Nicola Papale"] 4 | description = "Proc macros for the cuicui_fab crate" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy"] 8 | categories = ["game-development", "gui", "value-formatting"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "2.0", features = ["full", "extra-traits"] } 18 | proc-macro2 = "1.0" 19 | quote = "1.0" 20 | heck = "0.4" 21 | -------------------------------------------------------------------------------- /datazoo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_datazoo" 3 | authors = ["Nicola Papale"] 4 | description = "Data structures used in cuicui" 5 | license = "MIT OR Apache-2.0 OR Zlib" 6 | readme = "README.md" 7 | keywords = ["multimap", "bitmap", "jagged array", "enummultimap"] 8 | categories = ["data-structures"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | enumset = { version = "1.1", features = ["std"] } 15 | sorted-iter = "0.1.11" 16 | thiserror = "1" 17 | 18 | [dev-dependencies] 19 | pretty_assertions = "1.3" 20 | -------------------------------------------------------------------------------- /layout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_layout" 3 | authors = ["Nicola Papale"] 4 | description = "A layout algorithm made to be understood by humans" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "layout", "ui"] 8 | categories = ["game-development", "gui"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | anyhow = "1" 15 | thiserror = "1" 16 | bevy_mod_sysfail = "2" 17 | bevy = { version = "0.10", default-features = false } 18 | 19 | [dev-dependencies] 20 | bevy-inspector-egui = "0.18.3" 21 | bevy = { version = "0.10", features = ["png", "x11", "bevy_asset", "bevy_render" ] } 22 | -------------------------------------------------------------------------------- /design_doc/fine-grained-component-access.md: -------------------------------------------------------------------------------- 1 | # Fine-grained component access 2 | 3 | I've had this crazy idea just earlier. **fine-grained component access** 4 | 5 | So suppose we have a generalized tool to go from a `T: Reflect + Component` to 6 | `R` a field of `T` (some call it lens). 7 | 8 | For example, we could have a `WorldQuery` that takes `T` and a path, 9 | say `Path![T, .field.foo]`, and can only a access the field in question. 10 | 11 | This adds some interesting properties, we could imagine integrating 12 | this with the scheduler, it would enable parallelizing systems that access 13 | mutably the `translation` and `rotation` fields of `Transform`. 14 | We could also imagine an extension to change detection 15 | that manages path projections. -------------------------------------------------------------------------------- /widges/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_widges" 3 | authors = ["Nicola Papale"] 4 | description = "Collection of widgets (widges) for bevy" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "ui"] 8 | categories = ["game-development", "gui"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | bevy = { version = "0.10", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_text", "bevy_ui"] } 15 | bevy-ui-navigation = { version = "0.24.0", default-features = false } 16 | bevy_mod_sysfail = "2" 17 | 18 | [dev-dependencies] 19 | bevy-inspector-egui = "0.18.3" 20 | bevy = { version = "0.10", features = ["png", "x11", "bevy_asset", "bevy_render" ] } 21 | -------------------------------------------------------------------------------- /reflect_query/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_reflect_query" 3 | authors = ["Nicola Papale"] 4 | description = "A bevy reflection addon to query world data from Reflect Components" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "reflection"] 8 | categories = ["game-development"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.1" 11 | edition = "2021" 12 | 13 | [features] 14 | default = [] 15 | register_core_pipeline = ["bevy/bevy_core_pipeline"] 16 | register_pbr = ["bevy/bevy_pbr"] 17 | register_sprite = ["bevy/bevy_sprite"] 18 | register_render = ["bevy/bevy_render"] 19 | register_ui = ["bevy/bevy_ui"] 20 | register_text = ["bevy/bevy_text"] 21 | 22 | [dependencies] 23 | bevy = { version = "0.10", default-features = false } 24 | -------------------------------------------------------------------------------- /bevy_layout_offset/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | #[derive(Component, Debug, Default, Clone, Copy, PartialEq)] 4 | pub struct UiOffset(pub Transform); 5 | 6 | fn offset(mut query: Query<(&mut Transform, &UiOffset)>) { 7 | query.for_each_mut(|(mut transform, offset)| { 8 | *transform = transform.mul_transform(offset.0); 9 | }) 10 | } 11 | 12 | pub struct OffsetPlugin; 13 | impl Plugin for OffsetPlugin { 14 | fn build(&self, app: &mut App) { 15 | use bevy::transform::TransformSystem; 16 | use bevy::ui::UiSystem; 17 | 18 | app.add_system( 19 | offset 20 | .after(UiSystem::Flex) 21 | .before(TransformSystem::TransformPropagate) 22 | .in_base_set(CoreSet::PostUpdate), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fab_parse/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_fab_parse" 3 | authors = ["Nicola Papale"] 4 | description = "Parse a cuicui_fab resolver specification" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["markup", "template"] 8 | categories = ["game-development", "value-formatting", "template-engine"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [features] 14 | default = [] 15 | no_tracked_debug = [] 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | bevy_math = "0.10" 20 | enumset = { version = "1.1", features = ["std"] } 21 | log = "0.4" 22 | thiserror = "1" 23 | winnow = "0.4.4" 24 | 25 | fab = { path = "../fab", package = "cuicui_fab" } 26 | datazoo = { path = "../datazoo", package = "cuicui_datazoo" } 27 | 28 | [dev-dependencies] 29 | pretty_assertions = "1.3" 30 | -------------------------------------------------------------------------------- /widges/src/widge.rs: -------------------------------------------------------------------------------- 1 | // pub mod action_button; 2 | // pub mod checkbox; 3 | pub mod composed; 4 | pub mod event_button; 5 | pub mod labelled; 6 | pub mod list; 7 | pub mod visual; 8 | 9 | use bevy::{ 10 | ecs::system::{EntityCommands, SystemParam, SystemParamItem}, 11 | prelude::{DespawnRecursiveExt, Entity, In}, 12 | }; 13 | 14 | /// A value that has a `cuicui` representation. 15 | /// 16 | /// It supports spawning a `Prefab` 17 | pub trait Widge { 18 | fn spawn(&self, commands: EntityCommands); 19 | fn despawn(&self, commands: EntityCommands) { 20 | commands.despawn_recursive() 21 | } 22 | 23 | type ReadSystemParam<'w, 's>: SystemParam; 24 | fn read_from_ecs( 25 | entity: In, 26 | params: &SystemParamItem>, 27 | ) -> Option 28 | where 29 | Self: Sized; 30 | } 31 | -------------------------------------------------------------------------------- /bevy_layout_offset/README.md: -------------------------------------------------------------------------------- 1 | # Bevy UI layout offset 2 | 3 | A small bevy plugin to manipulate UI element transform. 4 | 5 | Full code: 6 | 7 | ```rust 8 | use bevy::prelude::*; 9 | 10 | #[derive(Component, Debug, Default, Clone, Copy, PartialEq)] 11 | pub struct UiOffset(pub Transform); 12 | 13 | fn offset(mut query: Query<(&mut Transform, &UiOffset)>) { 14 | query.for_each_mut(|(mut transform, offset)| { 15 | *transform = transform.mul_transform(offset.0); 16 | }) 17 | } 18 | 19 | pub struct OffsetPlugin; 20 | impl Plugin for OffsetPlugin { 21 | fn build(&self, app: &mut App) { 22 | use bevy::transform::TransformSystem; 23 | use bevy::ui::UiSystem; 24 | 25 | app.add_system( 26 | offset 27 | .after(UiSystem::Flex) 28 | .before(TransformSystem::TransformPropagate) 29 | .in_base_set(CoreSet::PostUpdate), 30 | ); 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /design_doc/activation/scoped_systems.md: -------------------------------------------------------------------------------- 1 | # Scoped systems 2 | 3 | Idea: Widges based on `Prefab` is meaningless. `Component`s are what matters, 4 | with regard to layout, styling, visuals, etc. 5 | 6 | What we are missing in the ECS is a way to limit system to a subset of the world. 7 | A vocabulary to express specific interactions between entities. 8 | 9 | ## Implementation 10 | 11 | - `Activated` component 12 | - A registry that associate `Entity` -> `System` 13 | - Something similar to QT signals: a way to express interaction sources, 14 | capture and "slots" for updating data. 15 | 16 | System that walks downard `Entity` hierarchy with a `Activated(true)` component. 17 | Some entities have an associated `ActiveSystem`, it is "triggered" when the 18 | `Activated(true)` for given entity. 19 | 20 | An `ActiveSystem` has `In<>` parameters, the walking system, when encoutering 21 | an `ActiveSystem`, walks down the hierarchy to find entities with corresponding 22 | `Out<>`, collect them and trigger the system. 23 | -------------------------------------------------------------------------------- /fab/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_fab" 3 | authors = ["Nicola Papale"] 4 | description = "A Bevy ECS reactive programming framework" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy"] 8 | categories = ["game-development", "gui", "value-formatting"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [features] 14 | default = [] 15 | no_tracked_debug = [] 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | enumset = { version = "1.1", features = ["std"] } 20 | log = "0.4" 21 | nonmax = "0.5.3" 22 | smallvec = { version = "1.10", features = ["union", "const_generics", "const_new"] } 23 | string-interner = { version = "0.14", default-features = false, features = ["std", "inline-more", "backends"] } 24 | thiserror = "1" 25 | 26 | datazoo = { path = "../datazoo", package = "cuicui_datazoo" } 27 | fab_derive = { path = "../fab_derive", package = "cuicui_fab_derive" } 28 | 29 | [dev-dependencies] 30 | pretty_assertions = "1.3" 31 | -------------------------------------------------------------------------------- /bevy_fab/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_bevy_fab" 3 | authors = ["Nicola Papale"] 4 | description = "Integrate cuicui_fab and cuicui_fab_parse with bevy" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "markup", "text", "template", "styling"] 8 | categories = ["game-development", "value-formatting", "template-engine"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [features] 14 | default = [] 15 | no_tracked_debug = [] 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | bevy = { version = "0.10", default-features = false } 20 | # bevycheck = "0.5.2" 21 | enumset = { version = "1.1", features = ["std"] } 22 | log = "0.4" 23 | thiserror = "1" 24 | 25 | fab_parse = { path = "../fab_parse", package = "cuicui_fab_parse" } 26 | fab = { path = "../fab", package = "cuicui_fab" } 27 | reflect_query = { path = "../reflect_query", package = "cuicui_reflect_query" } 28 | 29 | [dev-dependencies] 30 | bevy = { version = "0.10", features = ["png", "x11", "bevy_asset", "bevy_render" ] } 31 | bevy-inspector-egui = "0.18.3" 32 | pretty_assertions = "1.3" 33 | -------------------------------------------------------------------------------- /licenses/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | bevy cuicui suite (c) Nicola Papale 2023 2 | MIT License 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /design_doc/layout/when_is_PosRect_updated.md: -------------------------------------------------------------------------------- 1 | ## Optimizing layouting algorithm 2 | 3 | Walking through the whole layout tree each frame is not ideal. 4 | Likely easy optimization because very streightforward. 5 | 6 | What are the input of a given entity's PosRect? 7 | 8 | PosRect is: 9 | 10 | - Size 11 | - Container 12 | - cross always depends on max size of children 13 | - axis for Compact depends on total size of children 14 | - axis for Stretch depends on parent axis (equals) 15 | - Spacer 16 | - axis depends on size of parent 17 | - Also depends on field parent_ratio value 18 | - Static 19 | - only depends on self 20 | - Pos 21 | - is set by parent always 22 | 23 | For containers (Stretch): 24 | If self not changed, and no children changed, and parent axis not changed, 25 | then no need to update. 26 | 27 | For containers (Compact): 28 | Independent from parent so: 29 | If self not changed, and no children changed, 30 | then no need to update. 31 | 32 | For spacers: 33 | if parent axis not changed, and self not changed, 34 | then no need to update. 35 | 36 | For static: 37 | if self not changed, then no need to update. 38 | 39 | -------------------------------------------------------------------------------- /fab_derive/src/extensions.rs: -------------------------------------------------------------------------------- 1 | /// Interpret something as an identifier 2 | pub trait GetIdentExt { 3 | fn get_ident(&self) -> Option<&syn::Ident>; 4 | } 5 | impl GetIdentExt for syn::Type { 6 | fn get_ident(&self) -> Option<&syn::Ident> { 7 | use syn::{Type::Path, TypePath}; 8 | let Path(TypePath { qself: None, path }) = self else { return None; }; 9 | path.get_ident() 10 | } 11 | } 12 | impl GetIdentExt for syn::FnArg { 13 | fn get_ident(&self) -> Option<&syn::Ident> { 14 | use syn::{FnArg::Typed, Pat::Ident, PatIdent, PatType}; 15 | let Typed(PatType { pat, .. }) = self else { return None; }; 16 | let Ident(PatIdent { ident, .. }) = &**pat else { return None; }; 17 | Some(ident) 18 | } 19 | } 20 | 21 | /// Convert a collection of errors into a syn error 22 | pub trait IntoSynErrorsExt { 23 | fn into_syn_errors(self) -> Option; 24 | } 25 | impl> IntoSynErrorsExt for T { 26 | fn into_syn_errors(self) -> Option { 27 | self.into_iter().reduce(|mut acc, err| { 28 | acc.combine(err); 29 | acc 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fab_parse/src/hook.rs: -------------------------------------------------------------------------------- 1 | use fab::binding; 2 | 3 | use crate::{tree, RuntimeFormat}; 4 | 5 | #[derive(PartialEq, Debug, Clone, Copy)] 6 | pub enum Format { 7 | UserDefined(binding::Id), 8 | Fmt(RuntimeFormat), 9 | } 10 | impl Format { 11 | fn from_tree(bindings: &mut binding::World, tree: tree::Format) -> Self { 12 | match tree { 13 | tree::Format::UserDefined(name) => Format::UserDefined(bindings.get_or_add(name)), 14 | tree::Format::Fmt(fmt) => Format::Fmt(fmt), 15 | } 16 | } 17 | } 18 | 19 | #[derive(PartialEq, Debug, Clone, Copy)] 20 | pub struct Hook<'a> { 21 | // TODO: use binding::id also for Source 22 | pub source: tree::Source<'a>, 23 | pub format: Option, 24 | } 25 | impl<'a> Hook<'a> { 26 | pub(crate) fn from_tree( 27 | bindings: &mut binding::World, 28 | binding: tree::Binding<'a>, 29 | ) -> Option { 30 | if let tree::Path::Tracked(source) = binding.path { 31 | let format = binding.format.map(|f| Format::from_tree(bindings, f)); 32 | Some(Hook { source, format }) 33 | } else { 34 | None 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /design_doc/fab/higher_level_templates.md: -------------------------------------------------------------------------------- 1 | # Templates for entities 2 | 3 | Slowly going from exclusively working with Text to working with anything. 4 | But a few missing keys to get the transition complete. 5 | 6 | - How to represent non-text in format string? (heterogenous) 7 | - How to combine together several kinds of prefabs? 8 | - How to avoid repeating yourself? 9 | 10 | ## How to combine together several kinds of prefabs? 11 | 12 | Currently, format string creates a **flat list** of sections. 13 | Also, it only supports list with a single kind of items. 14 | 15 | A prefab system would require nested layout (think menu -> menu button -> label) 16 | Also needs heterogenous list (menu can have button, slider etc.) 17 | 18 | Idea: Projections 19 | 20 | ### Projections 21 | 22 | Projections are lists inside of lists. I've no idea how they could work. 23 | 24 | Supposedly, they inherit a subset of fields of their parent, and operate on those. 25 | 26 | ### Struct as prefab 27 | 28 | We actually don't need heterogenous list, 29 | we could do with a "heterogenous" thing be really just a single element. 30 | 31 | ## How would it look like? 32 | 33 | → Let's leave this for later. 34 | 35 | Seems like the best way of approaching it is to contribute to bevy_proto. -------------------------------------------------------------------------------- /datazoo/README.md: -------------------------------------------------------------------------------- 1 | # The Cuicui Data Zoo 2 | 3 | A collection of data structures used in `cuicui_richtext`. 4 | Mostly used for dependency resolution and specialized graph traversal tasks. 5 | 6 | Note that this library doesn't work on 16 bits plateforms. 7 | If you need support, consider opening an issue. 8 | 9 | You probably need to add [`enumset`] to your dependencies to use this crate. 10 | Due to a rust proc macro limitation, it's impossible to derive `EnumSetType` 11 | without directly depending on `enumset`. 12 | 13 | ## Limitations 14 | 15 | - Data structures are **untested with sizes `> u32::MAX`** 16 | - Effort is made to panic in those situations though, but you never know 17 | - Generally assumes `size_of(usize) >= size_of(u32)`, effort is made to use 18 | `u32::try_from(usize).unwrap()` though! 19 | - No `#[no_std]` but I don't see why this couldn't be added as a feature 20 | 21 | ## Data structures 22 | 23 | This is a collection of [multimaps], [jagged arrays], [bit sets], 24 | and combination thereof. 25 | 26 | See `docrs` documentation for details. 27 | 28 | [`enumset`]: https://lib.rs/crates/enumset 29 | [multimaps]: https://en.wikipedia.org/wiki/Multimap 30 | [jagged arrays]: https://en.wikipedia.org/wiki/Jagged_array 31 | [bit sets]: https://en.wikipedia.org/wiki/Bit_array -------------------------------------------------------------------------------- /fab/src/resolve/impl_fmt.rs: -------------------------------------------------------------------------------- 1 | //! Custom `Debug` impl for `resolver` structs, so that debug print output 2 | //! is more dense and easier to parse as a human. 3 | use std::fmt; 4 | 5 | use super::{MakeModify, Modifier, Modify, ModifyIndex, ModifyKind}; 6 | 7 | impl fmt::Debug for MakeModify { 8 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 9 | f.debug_tuple("Mk") 10 | .field(&self.kind) 11 | .field(&self.range) 12 | .finish() 13 | } 14 | } 15 | impl fmt::Debug for ModifyKind { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | match self { 18 | ModifyKind::Bound { binding, .. } => f.debug_tuple("Bound").field(binding).finish(), 19 | ModifyKind::Modify(modify) => f.debug_tuple("Set").field(&modify).finish(), 20 | } 21 | } 22 | } 23 | impl fmt::Debug for ModifyIndex { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "", self.0) 26 | } 27 | } 28 | 29 | impl fmt::Debug for Modifier { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | f.debug_tuple("Mod") 32 | .field(&self.modify) 33 | .field(&self.range) 34 | .finish() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /fab_parse/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use winnow::error::{ContextError, ErrorKind, ParseError}; 4 | 5 | #[derive(Debug)] 6 | pub struct Parse(Vec<(I, InternalElem)>); 7 | 8 | impl fmt::Display for Parse { 9 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 10 | write!( 11 | f, 12 | "I'd really like to make this error message more user-friendly.\n\ 13 | But I need to first prioritize other features of cuicui_richtext.\n\ 14 | Parse error:\n" 15 | )?; 16 | for (input, error) in &self.0 { 17 | match error { 18 | InternalElem::Context(s) => writeln!(f, "in section '{s}', at: {input}")?, 19 | } 20 | } 21 | Ok(()) 22 | } 23 | } 24 | 25 | #[derive(Debug)] 26 | pub(super) enum InternalElem { 27 | Context(&'static str), 28 | } 29 | impl ParseError for Parse { 30 | fn from_error_kind(_: I, _: ErrorKind) -> Self { 31 | Parse(Vec::new()) 32 | } 33 | fn append(self, _: I, _: ErrorKind) -> Self { 34 | self 35 | } 36 | } 37 | impl ContextError for Parse { 38 | fn add_context(mut self, input: I, ctx: &'static str) -> Self { 39 | self.0.push((input, InternalElem::Context(ctx))); 40 | self 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /widges/src/widge/list.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{EntityCommands, SystemParamItem}, 3 | prelude::{BuildChildren, Children, Component, Entity, In, Query, With}, 4 | }; 5 | 6 | use crate::Widge; 7 | 8 | #[derive(Component)] 9 | pub struct ListItem; 10 | 11 | pub struct List { 12 | items: Vec, 13 | } 14 | impl Widge for List { 15 | fn spawn(&self, mut commands: EntityCommands) { 16 | commands.with_children(|commands| { 17 | for elem in &self.items { 18 | let commands = commands.spawn(ListItem); 19 | elem.spawn(commands); 20 | } 21 | }); 22 | } 23 | 24 | type ReadSystemParam<'w, 's> = ( 25 | Query<'w, 's, Entity, With>, 26 | Query<'w, 's, &'static Children>, 27 | T::ReadSystemParam<'w, 's>, 28 | ); 29 | fn read_from_ecs( 30 | entity: bevy::prelude::In, 31 | (items, children, param): &SystemParamItem>, 32 | ) -> Option 33 | where 34 | Self: Sized, 35 | { 36 | let items = items 37 | .iter_many(children.get(entity.0).ok()?) 38 | .map(|item| T::read_from_ecs(In(item), param)) 39 | .collect::>()?; 40 | Some(List { items }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /design_doc/hierarchy-scoped-queries.md: -------------------------------------------------------------------------------- 1 | # Hierarchy-scoped queries 2 | 3 | So here is another crazy idea. Relations could make it trivial to implement. 4 | 5 | This is something I thought of during bevy jam 2, when I was making the `Prefab` 6 | system for the game editor. 7 | 8 | This would also allow me to replace `impl_modify` macro by the world query 9 | systems. 10 | 11 | I'd like a way to run queries on only on a subset of all entities. Typically, 12 | I'd want to run a query iterating over components on entities children 13 | (recursively) of given entity. 14 | 15 | It's actually possible to implement already, even as a 3rd party crate, 16 | by manually implementing `SystemParam` on — say ­— `ScopedQuery`. 17 | The `ScopedQuery`'s `iter` and `get` methods would require a `Entity` parameter. 18 | A naive implementation would internally use `iter_many{_mut}` internally. 19 | 20 | A more interesting version of this — not sure if possible today, in bevy or 21 | as 3rd party crate — could detect multiple `ScopedQuery` that run on disjoint 22 | subsets and run systems with `ScopedQuery` in parallel, 23 | even if they mutate the same components. 24 | 25 | This advanced version would need to rely on a component that exists 26 | in the ECS rather than an `Entity` provided as argument within the system. 27 | 28 | It's a bit like running a query on the `World` within a `Scene`, 29 | but after spawning them. -------------------------------------------------------------------------------- /bevy_fab/src/fmt_system.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{ReadOnlySystemParam, SystemState}, 3 | prelude::{FromWorld, World}, 4 | reflect::Reflect, 5 | }; 6 | use fab::binding::Entry; 7 | 8 | pub trait IntoFmtSystem> { 9 | fn into_fmt_system(self, world: &mut World) -> TRGT; 10 | } 11 | pub trait FmtSystem: Send + Sync + 'static { 12 | fn run(&mut self, value: &dyn Reflect, entry: Entry, world: &World); 13 | } 14 | pub struct ArbitraryFmt { 15 | function: F, 16 | state: S, 17 | } 18 | impl FmtSystem for ArbitraryFmt> 19 | where 20 | F0: ReadOnlySystemParam, 21 | F: FnMut(&dyn Reflect, Entry, F0::Item<'_, '_>) + Send + Sync + 'static, 22 | { 23 | fn run(&mut self, value: &dyn Reflect, entry: Entry, world: &World) { 24 | let Self { function, state, .. } = self; 25 | (function)(value, entry, state.get(world)); 26 | } 27 | } 28 | impl IntoFmtSystem>> for F 29 | where 30 | F0: ReadOnlySystemParam, 31 | F: FnMut(&dyn Reflect, Entry, F0::Item<'_, '_>) + Send + Sync + 'static, 32 | for<'a> &'a mut F: FnMut(&dyn Reflect, Entry, F0), 33 | { 34 | fn into_fmt_system(self, world: &mut World) -> ArbitraryFmt> { 35 | let state = SystemState::from_world(world); 36 | ArbitraryFmt { function: self, state } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /design_doc/richtext/hlist_interpreting.md: -------------------------------------------------------------------------------- 1 | # Of the use of Hlists in `RichText` 2 | 3 | A hlist aka "heterogenous list" is a fancy tuple of multiple different types 4 | that all implement the same trait. 5 | 6 | Instead of creating a `HashMap<&'static str, (TypeId, MakeModifyBox)>`, we could 7 | pass as type parameter a hlist. 8 | 9 | Now, in `interpret`, we could iterate over the `hlist` and use the appropriate 10 | `Modify::parse` function. 11 | 12 | Furthermore, instead of storing a `GoldMap` per section, we 13 | could store a `Box`. This avoids pointer chasing, since instead 14 | of storing a pointer to our `Modify`, we store the `Modify` itself as a field 15 | of an anonymous `struct` (the hlist). 16 | 17 | That seems insane and highly unlikely! Why? Well first off we need the 18 | _exhaustive_ list of types when calling `interpret` on the _list of sections_. 19 | 20 | ```rust 21 | fn interpret(sections: Vec) -> RichText { 22 | //... 23 | } 24 | ``` 25 | 26 | Then, for each individual section, you need to produce a distinct list of types 27 | based on the list itself (😰). Our one advantage is that the produced list of 28 | type is erased. But it should really be distinct, as each section will have 29 | a different number of types. 30 | 31 | The list of types to return depends on a runtime value, so I'm actually not quite 32 | sure it's possible. 33 | 34 | Where `ModifyList` is a hlist of `Modify`, particularity is that the -------------------------------------------------------------------------------- /design_doc/richtext/nested_dependency_implementation.md: -------------------------------------------------------------------------------- 1 | # Nested Dependency implementation 2 | 3 | So how to implement this? 4 | 5 | ## Classical graph 6 | 7 | too much pointer chasing most likely 8 | 9 | ## ECS 10 | 11 | I've no idea, but it sounds possible. 12 | 13 | ## Bitsets array 14 | 15 | Assuming all `Modify` stored depend on root (or depend on M that depends on root). 16 | 17 | - _R_ is the # of `DependsOn` except `DependsOn::Binding`, more on it later. 18 | - _N_ is the # of `Modify` 19 | 20 | We have a bit matrix of _R_ × _N_ bits. 21 | Bit at row _r_, column _n_ is activated when `Modify` at index 22 | Bit is activated when `Modify` at index _n_ DependsOn _r_. 23 | 24 | - `Box<[ModifyBox]>` 25 | - `BTreeMap` 26 | 27 | 28 | ## Other 29 | 30 | - `DependsOn` can work as an index 31 | - Store a `dependencies: [[ModifyBox]; DependsOn#length]` 32 | - Store a `high_order_dependencies` a struct that contains 3 fields: 33 | - `[u32]` mapping of `ModifyBox` index to sparse relation array. 34 | - Matrix `Bitset` where column is what changed and row which modify depends 35 | on this change 36 | - `[u32]` associate row index to `ModifyBox` to update 37 | 38 | iterate through `for modify in &dependencies[depends_on]` 39 | 40 | ## Other performance improvements 41 | 42 | - `Dynamic` isn't a `Modify` anymore. 43 | - `Content` row of dependencies can be removed, 44 | as it is always applied to a single section, never depends on anything. 45 | -------------------------------------------------------------------------------- /bevy_fab/src/local.rs: -------------------------------------------------------------------------------- 1 | //! Local entity-scopped data relevant to [`Modify`]s located in the bevy ECS. 2 | use bevy::ecs::prelude::Component; 3 | 4 | use fab::{binding, modify::Changing, resolve::Resolver, Modify}; 5 | 6 | use crate::WorldBindings; 7 | 8 | #[derive(Component)] 9 | pub struct LocalBindings { 10 | resolver: M::Resolver, 11 | pub root_data: Changing, 12 | pub bindings: binding::Local, 13 | } 14 | impl LocalBindings { 15 | /// Update `to_update` with updated values from `world` and `self`-local bindings. 16 | /// 17 | /// Only the relevant sections of `to_update` are updated. The change trackers 18 | /// are then reset. 19 | pub fn update( 20 | &mut self, 21 | items: &mut M::Items<'_, '_, '_>, 22 | world: &WorldBindings, 23 | ctx: &M::Context<'_>, 24 | ) { 25 | let Self { root_data, bindings, resolver } = self; 26 | 27 | // TODO(clean): this code should be in cuicui_fab 28 | let view = world.bindings.view_with_local(bindings).unwrap(); 29 | resolver.update(items, root_data, view, ctx); 30 | root_data.reset_updated(); 31 | bindings.reset_changes(); 32 | } 33 | pub(crate) fn new(resolver: M::Resolver, root_data: M::MakeItem) -> Self { 34 | LocalBindings { 35 | resolver, 36 | root_data: Changing::new(root_data), 37 | bindings: Default::default(), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /design_doc/richtext/post_process_content.md: -------------------------------------------------------------------------------- 1 | # Splitting Content 2 | 3 | ## Problem 4 | 5 | Splitting by word/char makes it impossible to control a-posteriory the text 6 | vale of split stuff. 7 | 8 | It seems impossible to reconciliate. 9 | 10 | Splitting requires creating sections based on `Content`. 11 | The `Resolver` depends on the precise section location, 12 | and generally is immutable, I can't remove or add modifiers once the `Resolver` 13 | is generated. 14 | 15 | Changing the `Content` of split section would require adding/removing sections. 16 | Which is incompatible with the description I described last paragraph. 17 | 18 | ## Workaround 19 | 20 | It's possible to set the content dynamically _before_ generating the `RichText`. 21 | For example, using the `format!` macro. 22 | However, this is highly annoying, given the `RichText` format syntax. 23 | Need to duplicate all `{}`. 24 | 25 | ```rust 26 | format!("{{M4 | {{ M2 | {{M1, M3 | Hello}}{{M2, R |{{M1 | This has several}}{{M5 | {user_display} words here }}}}}}{{M6 | also more}}}}") 27 | ``` 28 | 29 | The one user-defined value is `{user_display}`, it's difficult to spot, 30 | and required changing most of the string. 31 | 32 | Maybe it could be possible to define a macro with a different format syntax. 33 | We replace `{user_display}` by `$user_display`, no need to escape `{}` anymore. 34 | 35 | ```rust 36 | richtext_format!("{M4 | { M2 | {M1, M3 | Hello}{M2, R |{M1 | This has several}{M5 | $user_display words here }}}{M6 | also more}} 37 | ") 38 | ``` 39 | -------------------------------------------------------------------------------- /widges/README.md: -------------------------------------------------------------------------------- 1 | ## Widges 2 | 3 | Widgets are called `widges` in cuicui because I am t3h PeNgU1N oF d00m and bcuz 4 | its SOOOO random!!! 5 | 6 | cuicui contains N times more widgets than `bevy_ui` (and as mentioned earlier 7 | they are also called `widges`, and _de facto_ a lot cooler) 8 | 9 | - [ ] `Toggle` 10 | - [ ] `Button` 11 | - [ ] `Counter` 12 | - [ ] `Menu` 13 | - [ ] `Checkbox` 14 | - [ ] `ProgressBar` 15 | - [ ] `Slider` 16 | - [ ] `Cancel` 17 | - [ ] `List` 18 | 19 | cuicui integrates [`bevy-ui-navigaiton`] and a system similar to `bevy_ui_dsl`. 20 | 21 | ## TODO 22 | 23 | - [ ] Widges 24 | - [ ] ~~Prefab system~~ --> Redesign documented in design_doc/widges.md 25 | - [X] basic composable trait that allows spawning widgets 26 | - [X] composable trait to query widget value from world 27 | - [ ] Widge system 28 | - [ ] A set of simple but effective widges 29 | - [ ] `Toggle` 30 | - [ ] `Button` 31 | - [ ] `Counter` 32 | - [ ] `Menu` 33 | - [ ] `Checkbox` 34 | - [ ] `ProgressBar` 35 | - [ ] `Slider` 36 | - [ ] `Cancel` 37 | - [X] `List` 38 | - [ ] "Structural" widges based on bevy's `Reflect` trait (see `ReflectRef`) 39 | - [ ] `struct` 40 | - [ ] `enum` 41 | - [ ] `List` 42 | - [ ] `Map` 43 | - [ ] Gallery example. 44 | - [ ] System to select widges based on external definition 45 | - [ ] System to manipulate style-based components based on external definition 46 | - [ ] Windowmaker app to create re-usable widget trees. 47 | 48 | -------------------------------------------------------------------------------- /richtext/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cuicui_richtext" 3 | authors = ["Nicola Papale"] 4 | description = "A bevy plugin to manage text without boilerplate" 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["bevy", "markup", "text", "template", "styling"] 8 | categories = ["game-development", "gui", "value-formatting", "template-engine"] 9 | repository = "https://github.com/nicopap/cuicui" 10 | version = "0.1.0" 11 | edition = "2021" 12 | 13 | [features] 14 | default = ["cresustext"] 15 | richtext = [] 16 | cresustext = ["bevy_layout_offset"] 17 | 18 | [dependencies] 19 | anyhow = "1" 20 | bevy = { version = "0.10", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_text", "bevy_ui"] } 21 | enumset = { version = "1.1", features = ["std"] } 22 | thiserror = "1" 23 | 24 | bevy_layout_offset = { path = "../bevy_layout_offset", package = "cuicui_bevy_layout_offset", optional = true } 25 | fab = { path = "../fab", package = "cuicui_fab" } 26 | fab_parse = { path = "../fab_parse", package = "cuicui_fab_parse" } 27 | bevy_fab = { path = "../bevy_fab", package = "cuicui_bevy_fab" } 28 | reflect_query = { path = "../reflect_query", package = "cuicui_reflect_query", features = [ 29 | "register_core_pipeline", 30 | "register_pbr", 31 | "register_sprite", 32 | "register_render", 33 | "register_ui", 34 | "register_text", 35 | ] } 36 | 37 | [dev-dependencies] 38 | bevy = { version = "0.10", features = ["png", "x11", "bevy_asset", "bevy_render" ] } 39 | bevy-inspector-egui = "0.18.3" 40 | pretty_assertions = "1.3" 41 | -------------------------------------------------------------------------------- /design_doc/layout/render_layer_handling.md: -------------------------------------------------------------------------------- 1 | # Handling of RenderLayers 2 | 3 | The UI is constructed around RenderLayers using the 2d Rendering pipeline, 4 | the user specifies the RenderLayer to use, the library will comply 5 | 6 | ## Propagate ourselves 7 | 8 | RenderLayers only work for individual entities, it doesn't propagate down. 9 | This might surprise end users who just casually add a UI tree without 10 | much more consideration than it being a UI tree. 11 | 12 | Should we propagate it ourselves? 13 | 14 | We could force this by pushing usage of `RootBundle` rather than `Root`, 15 | this way, the user is made aware of it. 16 | 17 | But is this enough? Surely not, I thinik it will be necessary to propagate 18 | the RenderLayers. 19 | 20 | This is more difficult than at first sight, because we want to limit the 21 | propagation to children (recursively) of `Root`. When an entity is added, 22 | how do we know it is a child of root? Should we iterate over the whole 23 | tree every time an entity is added, just because it could be a recursive 24 | child of `Root`? This seems very costly! 25 | 26 | Could we do it like the bevy `Transform` propagation and ignore all entities 27 | that do not have a relevant componet? Like force adding a component? 28 | => But then it defeats the purpose of doing it in the first place 29 | 30 | ## Decision 31 | 32 | I think we should do nothing. 33 | 34 | The reason being we define ourself most of the widgets, in which we can 35 | sneak in our RenderLayers component, and if someone wants to create their own 36 | widget, they'll have to learn how to do it, which is actually not trivial. 37 | 38 | We can expose them to the RenderLayers quirk this way. 39 | -------------------------------------------------------------------------------- /design_doc/birds_eye_view.md: -------------------------------------------------------------------------------- 1 | # Global Architecture 2 | 3 | bevy mod cuicui is a UI framework. A UI framework is surprisingly difficult 4 | thing to make (ok, most people are aware of this, I was just assuming they were 5 | wrong). 6 | 7 | A UI framework is actually many non-trivial systems deeply interwined. Which I 8 | assume is why they are all confusing, and why it's difficult to make one. 9 | 10 | Bevy mod cuicui has the following systems: 11 | 12 | - A bevy-specific `Prefab` system that enables loading, reloading, querying and 13 | despawning parametrizeable quantums of ECS hierarchy of entity with specific 14 | components. 15 | - A `Widge` (widgets) system built on prefab 16 | - A state management system that translates a complex tree of widges into (and 17 | from) a more simple `struct` (or `enum`) 18 | - A rich text component with a styling system reflecting the larger styling 19 | and presentation APIs. 20 | - A presentation system that allows choosing what part of the state is 21 | represented with which widge 22 | - A second, more primitive presentation system, styling, that controls individual 23 | fields of components of individual parts of the widges, that should integrate 24 | with widges so that it's possible to express styling fluently[0]. 25 | - Of course, those two systems should be bidirectional, specifiable 26 | through an external asset and hot-reloadbale. This isn't about *why*, it's 27 | about *why not*! 28 | - A layouting system 29 | - An activation tree manager. 30 | 31 | 32 | - [0] Example: A checkbox + label is an entity that represents the check, the 33 | box, the label. Each with many parameters. Not even taking animation in 34 | consideration. -------------------------------------------------------------------------------- /fab/src/dummy_modify.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use enumset::{EnumSet, EnumSetType}; 4 | 5 | use crate::{ 6 | binding::View, 7 | modify::{Changing, Indexed, Modify}, 8 | resolve::{MakeModify, Resolver}, 9 | }; 10 | 11 | /// A `Modify` That does literally nothing. 12 | /// 13 | /// This can be useful for tests or example code. 14 | #[derive(Debug, Clone)] 15 | pub struct DummyModify; 16 | 17 | #[derive(EnumSetType, Debug)] 18 | pub enum NoFields {} 19 | 20 | impl Indexed for () { 21 | fn get_mut(&mut self, _: usize) -> Option<&mut ()> { 22 | None 23 | } 24 | } 25 | impl Resolver for () { 26 | fn new(mods: Vec>, f: F, _: &()) -> ((), Vec<()>) { 27 | let Some(section_count) = mods.iter().map(|m| m.range.end).max() else { 28 | return ((), Vec::new()) 29 | }; 30 | let dummies = iter::repeat_with(f).take(section_count as usize).collect(); 31 | ((), dummies) 32 | } 33 | fn update<'a>( 34 | &'a self, 35 | _: &mut (), 36 | _: &'a Changing, 37 | _: View<'a, DummyModify>, 38 | _: &(), 39 | ) { 40 | } 41 | } 42 | impl Modify for DummyModify { 43 | type Item<'a> = &'a mut (); 44 | type MakeItem = (); 45 | type Items<'a, 'b, 'c> = (); 46 | type Field = NoFields; 47 | type Context<'a> = (); 48 | type Resolver = (); 49 | 50 | fn apply(&self, (): &(), (): &mut ()) -> anyhow::Result<()> { 51 | Ok(()) 52 | } 53 | fn depends(&self) -> EnumSet { 54 | EnumSet::EMPTY 55 | } 56 | fn changes(&self) -> EnumSet { 57 | EnumSet::EMPTY 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /design_doc/layout/better_layout_containers.md: -------------------------------------------------------------------------------- 1 | # Layout containers 2 | 3 | Due to explicit limitations, we need better containers. 4 | Specifically, containers with set sizes, and maybe sizes expressed 5 | as percentage of parent container. 6 | 7 | Why? 8 | 9 | 1. Explicit limitations (ambiguous situations such as two horizontal stretch 10 | within a horizontal stretch) should be detected and result in warning. 11 | 2. layout update could have much more fine-grained work skipping heuristic 12 | 3. it is easier and more expected to specify the container with its constraints 13 | 14 | We could simply define `Spacer` and `Static` as potential containers themselves, 15 | but this adds additional constraints: for example, we now need to check they 16 | have only a single child, propagate the constraints downwards coherently etc. 17 | 18 | Assume `Static` and `Spacer` are leaf nodes only. They are for terminal 19 | nodes in the UI such as text or image widgets. 20 | 21 | Remember dependencies from `when_is_PosRect_updated.md`: 22 | 23 | - Size 24 | - Container 25 | - cross always depends on max size of children 26 | - axis for Compact depends on total size of children 27 | - axis for Stretch depends on parent axis (equals) 28 | - Spacer 29 | - axis depends on size of parent 30 | - Also depends on field parent_ratio value 31 | - Static 32 | - only depends on self 33 | - Pos 34 | - is set by parent always 35 | 36 | When do I know the parent sizes? 37 | 38 | - Has a set size 39 | - The axis in parent Stretch container direction 40 | => Is it true? => Only if you know parent's parent container axis size 41 | => how to fix? => Propagate uncertainty downard 42 | 43 | When do I **not** know the parent sizes? 44 | 45 | - The cross when child of Stretch Container 46 | - Both axis when child of Compact Container -------------------------------------------------------------------------------- /fab/src/resolve/make/mask_range.rs: -------------------------------------------------------------------------------- 1 | use std::{mem::size_of, ops::Range}; 2 | 3 | use datazoo::{jagged_bitset, Bitset, JaggedBitset}; 4 | use enumset::EnumSet; 5 | 6 | use crate::{modify::Modify, resolve::MakeModify}; 7 | 8 | type Mask = Bitset>; 9 | 10 | // M0 masks M1 when: 11 | // M0 child of M1 12 | // and ∃ c ∈ M1.C, 13 | // c ∈ M0.C 14 | // and c ∉ M0.D 15 | fn mask_range(parent: &MakeModify, child: &MakeModify) -> Range { 16 | if parent.changes() & child.changes() & !child.depends() != EnumSet::EMPTY { 17 | let offset = parent.range.start; 18 | let r = child.range.clone(); 19 | r.start - offset..r.end - offset 20 | } else { 21 | 0..0 22 | } 23 | } 24 | fn mask(modifiers: &[MakeModify], i: usize, m: &MakeModify) -> Mask { 25 | let capacity = m.range.len() / size_of::(); 26 | let mut mask = Bitset(Vec::with_capacity(capacity)); 27 | 28 | let children = modifiers.iter().skip(i + 1).take_while(|c| m.parent_of(*c)); 29 | mask.extend(children.flat_map(|c| mask_range(m, c))); 30 | mask 31 | } 32 | 33 | #[derive(Debug)] 34 | pub(super) struct MaskRange { 35 | masks: Box<[Mask]>, 36 | builder: jagged_bitset::Builder, 37 | } 38 | impl MaskRange { 39 | pub(super) fn new(modifiers: &[MakeModify]) -> Self { 40 | let masks = modifiers.iter().enumerate(); 41 | let masks = masks.map(|(i, m)| mask(modifiers, i, m)).collect(); 42 | Self { 43 | masks, 44 | builder: jagged_bitset::Builder::with_capacity(modifiers.len()), 45 | } 46 | } 47 | pub(super) fn add_index(&mut self, i: usize) { 48 | self.builder.add_row(&self.masks[i]) 49 | } 50 | 51 | pub(super) fn build(self) -> JaggedBitset { 52 | self.builder.build() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /widges/src/widge/event_button.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bevy::{ 4 | ecs::{ 5 | event::Event, 6 | system::{EntityCommands, SystemParamItem}, 7 | }, 8 | prelude::*, 9 | }; 10 | use bevy_ui_navigation::prelude::*; 11 | 12 | use crate::Widge; 13 | 14 | pub trait PodEvent: Into + From + Copy {} 15 | 16 | #[derive(Component)] 17 | pub struct Button { 18 | event: u32, 19 | } 20 | impl Button { 21 | pub fn new(event: T) -> Self { 22 | Button { event: event.into() } 23 | } 24 | } 25 | 26 | pub struct ButtonWidge { 27 | event: T, 28 | } 29 | impl Widge for ButtonWidge { 30 | fn spawn(&self, mut commands: EntityCommands) { 31 | commands.insert(Button::new(self.event)); 32 | } 33 | 34 | type ReadSystemParam<'w, 's> = Query<'w, 's, &'static Button>; 35 | fn read_from_ecs( 36 | entity: In, 37 | params: &SystemParamItem>, 38 | ) -> Option 39 | where 40 | Self: Sized, 41 | { 42 | Some(ButtonWidge { event: params.get(entity.0).ok()?.event.into() }) 43 | } 44 | } 45 | 46 | fn activate_button( 47 | actions: Query<&Button>, 48 | mut nav_events: EventReader, 49 | mut button_events: EventWriter, 50 | ) { 51 | for action in nav_events.nav_iter().activated_in_query(&actions) { 52 | button_events.send(action.event.into()); 53 | } 54 | } 55 | pub struct Plug(PhantomData); 56 | impl Plug { 57 | pub const fn new() -> Self { 58 | Self(PhantomData) 59 | } 60 | } 61 | impl Plugin for Plug { 62 | fn build(&self, app: &mut App) { 63 | app.add_event::().add_system(activate_button::); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /widges/src/widge/checkbox.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{EntityCommands, SystemParamItem}, 3 | prelude::*, 4 | }; 5 | use bevy_ui_navigation::prelude::*; 6 | 7 | use crate::{widge, ExtractPrefab, Prefab, Widge, WorldValue}; 8 | 9 | #[derive(Debug)] 10 | pub struct Check { 11 | pub checked: bool, 12 | pub frame: widge::Image, 13 | pub fill: widge::Image, 14 | } 15 | #[derive(Component, Clone, Copy)] 16 | struct CheckCompo { 17 | checked: bool, 18 | } 19 | impl Prefab for Check { 20 | type Param = (); 21 | fn spawn(&self, mut commands: EntityCommands, _: &mut SystemParamItem) { 22 | commands 23 | .insert(CheckCompo { checked: self.checked }) 24 | .with_children(|cmds| {}); 25 | } 26 | } 27 | impl ExtractPrefab for Check { 28 | type ExtractParam<'w, 's> = Query<'w, 's, &'static CheckCompo>; 29 | fn extract( 30 | entity: Entity, 31 | params: &SystemParamItem>, 32 | ) -> Option { 33 | params.get(entity).ok().copied() 34 | } 35 | } 36 | impl WorldValue for Check { 37 | type Value = bool; 38 | type ReadParam<'w, 's> = Query<'w, 's, &'static CheckCompo>; 39 | fn read( 40 | entity: Entity, 41 | params: &SystemParamItem>, 42 | ) -> Option { 43 | params.get(entity).ok().map(|c| c.checked) 44 | } 45 | } 46 | impl Widge for Check {} 47 | 48 | fn activate(mut checkboxes: Query<&mut CheckCompo>, mut nav_events: EventReader) { 49 | nav_events 50 | .nav_iter() 51 | .activated_in_query_foreach_mut(&mut checkboxes, |mut check| { 52 | check.checked = !check.checked; 53 | }); 54 | } 55 | pub struct Plug; 56 | impl Plugin for Plug { 57 | fn build(&self, app: &mut App) { 58 | app.add_system(activate); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /design_doc/richtext/better_section_impl.md: -------------------------------------------------------------------------------- 1 | `clone_dyn` in `Modify` is used at the end of `richtext::parser::helpers::elements_and_content` 2 | to propagate modifiers to nested text segments. Can't use `Modify: Clone` 3 | since we need to work on trait objects and clone is not object-safe. 4 | The alternative of using bevy's reflect is painful, since this would require 5 | `ReflectFromReflect` and access to the type registry where the modifiers would 6 | be pre-registered 7 | 8 | Not require `clone_dyn` on Modify. Would require `RichText` to be 9 | a specialized datastructure. 10 | 11 | - A `Vec>` where `usize` tells for how 12 | many additional positions in the `Vec` the modifier is relevant. 13 | It would require accumulating ModifierBoxes when traversing sections. 14 | It would be made extra tricky if we allow modifier shadowing (you need to 15 | keep track of previous value of modifiers) But currently we don't allow that 16 | so the list of modifiers is flat. 17 | - `Vec>>` where `enum OrRef { Actual(T), AtIndex(usize) }` 18 | would replace the clone with adding a `OrRef::AtIndex(index_or_ref_actual)` to 19 | subsequent indices where ModifierBox should be cloned. 20 | When traversing, we could just sections[at_index][type_id] to access the actual 21 | value. 22 | - `Vec>` where `enum OrPtr { Box(ModifierBox), Ptr(*const dyn Modify) }` 23 | replace the clone with a ptr copy, can implement `AsRef` for `OrPtr`, traversing 24 | is equivalent to the naive `clone_dyn`-based implementation. 25 | The only worry here is to make sure the `Ptr` variants are dropped at the same time 26 | as the Box, (at least I think?) 27 | 28 | ## `Modify` 29 | 30 | Do I need to store the `dyn Modify` that are not `modifiers::Dynamic`? Non-dynamic 31 | modifiers only need to be aplied once right? 32 | 33 | Counter example: `RelSize`, if we update the parent size, we should also re-run it. 34 | -------------------------------------------------------------------------------- /layout/src/render.rs: -------------------------------------------------------------------------------- 1 | //! Smooth out integration of cuicui layout with bevy, simply using 2 | //! render targets and a camera component marker. 3 | 4 | use bevy::{ 5 | prelude::*, 6 | render::view::{Layer, RenderLayers}, 7 | }; 8 | use bevy_mod_sysfail::quick_sysfail; 9 | 10 | use super::Root; 11 | 12 | #[derive(Component, Clone, Copy, Debug, Default)] 13 | #[cfg_attr(feature = "reflect", derive(Reflect, FromReflect))] 14 | pub struct UiCamera; 15 | 16 | #[derive(Bundle)] 17 | pub struct RootBundle { 18 | pub node: Root, 19 | pub layer: RenderLayers, 20 | } 21 | 22 | #[derive(Bundle)] 23 | pub struct UiCameraBundle { 24 | #[bundle] 25 | pub camera: Camera2dBundle, 26 | pub layer: RenderLayers, 27 | pub ui_camera: UiCamera, 28 | } 29 | impl UiCameraBundle { 30 | pub fn for_layer(order: isize, layer: Layer) -> Self { 31 | UiCameraBundle { 32 | camera: Camera2dBundle { 33 | projection: OrthographicProjection { 34 | far: 1000.0, 35 | viewport_origin: Vec2::new(0.0, 0.0), 36 | ..default() 37 | }, 38 | camera: Camera { order, ..default() }, 39 | ..default() 40 | }, 41 | layer: RenderLayers::none().with(layer), 42 | ui_camera: UiCamera, 43 | } 44 | } 45 | } 46 | 47 | #[quick_sysfail] 48 | pub fn update_ui_camera_root( 49 | ui_cameras: Query<(&Camera, &RenderLayers), (With, Changed)>, 50 | mut roots: Query<(&mut Root, &RenderLayers)>, 51 | ) { 52 | for (cam, layers) in &ui_cameras { 53 | let size = cam.logical_viewport_size()?; 54 | let is_layer = |(r, l)| (l == layers).then_some(r); 55 | for mut root in roots.iter_mut().filter_map(is_layer) { 56 | root.bounds.width = size.x; 57 | root.bounds.height = size.y; 58 | } 59 | } 60 | } 61 | // TODO: 62 | // Compute the Static (and likely offset) of sprites and meshes (including 63 | // rotation) 64 | -------------------------------------------------------------------------------- /design_doc/datazoo/bit_assocarray.md: -------------------------------------------------------------------------------- 1 | We have a `binding::Id`, a small integer. And we have collections of: 2 | 3 | `binding::Id` -> `Option` 4 | 5 | How to store `T`s with optimal memory usage, yet keeping lookups O(1) as an 6 | index dereference? 7 | 8 | ## Sparse array 9 | 10 | We could have a `[Option; max(binding::Id)]`. 11 | 12 | But supposing `size_of::() > 2000`, that we have a single `T` and its binding 13 | value is something like `100`, this is a lot of memory that exists to do about 14 | exactly nothing! 15 | 16 | ## Sorted array 17 | 18 | We currently use two different storing means for this: 19 | 20 | - `BTreeMap`: Works, is `O(log(n))`, and it's a fine thing given 21 | small size collection 22 | - `SortedByKeyVec`: This is still `O(log(n))`, better cache 23 | locality, not very good with insertions, but we generally don't care about 24 | insertions. 25 | 26 | ## Bit matrix map 27 | 28 | Consider `BitMultimap`, it stores a `[K]`, a `[V]` and a `BitMatrix`. 29 | 30 | We could drop the `[K]` for `binding::Id` (we could even drop the `[V]` in the 31 | case where we use it for `ModifyIndex`!) Directly using the `Id` value for 32 | indexing. I mean, this is insane and reasonable at the same time. 33 | 34 | But this is a _multimap_, it is made for situations where a single `K` can have 35 | multiple `V`s. 36 | 37 | Our situation describes a 1-to-0 relation. Not a n-to-m. 38 | 39 | ## Compact Index matrix 40 | 41 | The idea of the `BitMatrix` would allocate `max(K)` rows of `max(V)` bits. In the case 42 | of the 1-to-0 relation, either the row has a single bit enabled, or it has none. 43 | 44 | Can this be done with less zeros? 45 | 46 | Yes. Consider this: instead of representing a row with a bitset, we represent 47 | it with a single integer. 48 | 49 | We could use a `usize` as integer, but that would take 64 bits, only for a 50 | target space of `max(V)`. If we know the upper bound of our target space (we do) 51 | then, we can reduce the size of the integer (in bits) so that it can just 52 | represent the largest index in the target space, and not more! -------------------------------------------------------------------------------- /design_doc/fab/binding_2_modify_dependency.md: -------------------------------------------------------------------------------- 1 | # Binding to Modify dependency 2 | 3 | We want to unify the binding list and the modify list in `Resolver`. 4 | This would allow resolving binding deps exactly like a modify dep. 5 | 6 | ## Problems 7 | 8 | - When declared, a `Binding` doesn't have a serialized value. 9 | This means, I can't use it to build the dependency graph. 10 | - However, I do have the `Modify` type as the name, 11 | it should technically be possible to get it. 12 | But requires fiddling with the `impl_modify` macro most likely. 13 | - This fiddling would involve creating associated const on the `Modifier` 14 | with the name of the modify function + `_{changes,depends}` 15 | 16 | ## Alternatives 17 | 18 | There are two types of bindings. 19 | 20 | 1. binding without parent or child dependencies 21 | 2. binding with either parent of child dependency 22 | 23 | Current design only allows (1), preventing cool stuff like moving rainbows. 24 | 25 | Proposed design allows both, but would make (1) much less performant 26 | (eg: need to clone the `Modify` inline the `Resolver`) 27 | 28 | ```rust 29 | struct Resolver { 30 | modifiers: Box<[Modifier

]>, 31 | // ... 32 | } 33 | enum Modifier { 34 | Binding { 35 | inner: Option, 36 | range: Range, 37 | }, 38 | Modifier { 39 | inner: P::Modify, 40 | range: Range, 41 | }, 42 | } 43 | ``` 44 | 45 | Ok, this seems redundant, what about just modifying the existing `Modifier`? 46 | 47 | ```rust 48 | struct Modifier { 49 | inner: Option, 50 | range: Range, 51 | } 52 | ``` 53 | 54 | Bindings without deps do not need to be inserted, `inner` stays `None`. 55 | Bindings with deps can be inserted. 56 | No need to store in `Resolver` whether a binding has dependencies or not, 57 | I can call `.depends` on the `Modify` stored in the `binding::View`. 58 | 59 | This doesn't trim the `modifiers: Box<[_]>` as much as if we removed binding modifiers. 60 | `modifiers` might be sparse/slightly sparse. 61 | But it doesn't matter, because we never iterate `modifiers`, and nothing depends 62 | directly on its size. -------------------------------------------------------------------------------- /fab_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | // Architecture: 4 | // 5 | // `block` (`Block`) parses the whole item block, inlcuding the `impl Modify for Bar`. 6 | // It also generates most of the code. 7 | // 8 | // `modify_fn` (`ModifyFn`) is a single function item in the `impl_modify` block. 9 | // It processes the single declared function and its `#[modify(…)]` attributes 10 | // and provides methods to create the generated code based on those. 11 | // 12 | // `modifiers` (`Modifiers`) is a list of `ModifyFn` modifiers. Modifiers are 13 | // declaration of what fields the function reads and writes, and which parameter 14 | // of the modify function those fields correspond to. 15 | // It provides methods to generate the fields and modify enums 16 | // and argument lists for the modify functions. 17 | // 18 | // `modifiers::path` (`Path`) is a modifier path into the item. Provides method to create 19 | // the fields enum. 20 | // 21 | // `modifiers::deps` A `Modifiers` is not enough to decide what dependencies 22 | // a modify function touches. Consider `modify1` and `modify2`, two modify functions. 23 | // `modify1` depends on `item.field` and `modify2` changes `item.field.inner`. 24 | // `modify1` is affected by changes made by `modify2`, yet, `Modifiers` doesn't know that. 25 | // `deps` provides way to split to the most precise dependency a set of `Modifiers`. 26 | 27 | mod block; 28 | mod extensions; 29 | mod modifiers; 30 | mod modify_fn; 31 | 32 | use block::Config; 33 | use proc_macro::TokenStream as TokenStream1; 34 | use syn::{parse_macro_input, ItemImpl}; 35 | 36 | use crate::block::Block; 37 | 38 | #[doc = include_str!("../README.md")] 39 | #[proc_macro_attribute] 40 | pub fn impl_modify(attrs: TokenStream1, input: TokenStream1) -> TokenStream1 { 41 | let mut config = Config::default(); 42 | 43 | if !attrs.is_empty() { 44 | let config_parser = syn::meta::parser(|meta| config.parse(meta)); 45 | parse_macro_input!(attrs with config_parser); 46 | } 47 | let input = parse_macro_input!(input as ItemImpl); 48 | match Block::parse(config, input) { 49 | Err(errors) => errors.into_compile_error().into(), 50 | Ok(block) => block.generate_impl().into(), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /widges/src/prefab.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | ecs::system::{Command, EntityCommands, SystemParam, SystemParamItem, SystemState}, 3 | prelude::{Commands, DespawnRecursiveExt, Entity, World}, 4 | }; 5 | 6 | /// A thing that describes an entity tree that can be spawned in the world. 7 | pub trait Prefab { 8 | type Param: SystemParam; 9 | fn spawn(&self, commands: EntityCommands, param: &mut SystemParamItem); 10 | fn despawn(&self, commands: EntityCommands) { 11 | commands.despawn_recursive() 12 | } 13 | } 14 | 15 | /// A [`Command`] for spawning [`Prefab`]s. 16 | struct InsertPrefab { 17 | prefab: T, 18 | entity: Entity, 19 | } 20 | 21 | impl Command for InsertPrefab { 22 | fn write(self, world: &mut World) { 23 | let mut state: SystemState<(Commands, T::Param)> = SystemState::new(world); 24 | let (mut commands, mut param) = state.get_mut(world); 25 | let e_commands = commands.entity(self.entity); 26 | self.prefab.spawn(e_commands, &mut param); 27 | drop((commands, param)); 28 | state.apply(world); 29 | } 30 | } 31 | 32 | //===== EXTENSIONS TO COMMANDS / ENTITY_COMMANDS ======= 33 | pub trait InsertPrefabCommand { 34 | fn insert_prefab(&mut self, prefab: T) -> &mut Self; 35 | } 36 | 37 | impl<'w, 's, 'a> InsertPrefabCommand for EntityCommands<'w, 's, 'a> { 38 | fn insert_prefab(&mut self, prefab: T) -> &mut Self { 39 | let entity = self.id(); 40 | self.commands().add(InsertPrefab { prefab, entity }); 41 | self 42 | } 43 | } 44 | 45 | pub trait SpawnPrefabCommand<'w, 's> { 46 | fn spawn_prefab<'a, T: Prefab + Send + Sync + 'static>( 47 | &'a mut self, 48 | prefab: T, 49 | ) -> EntityCommands<'w, 's, 'a>; 50 | } 51 | impl<'w, 's> SpawnPrefabCommand<'w, 's> for Commands<'w, 's> { 52 | fn spawn_prefab<'a, T: Prefab + Send + Sync + 'static>( 53 | &'a mut self, 54 | prefab: T, 55 | ) -> EntityCommands<'w, 's, 'a> { 56 | let entity = self.spawn_empty().id(); 57 | self.add(InsertPrefab { prefab, entity }); 58 | return self.entity(entity); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /fab/src/binding/entry.rs: -------------------------------------------------------------------------------- 1 | use datazoo::sorted; 2 | 3 | use super::Id; 4 | 5 | pub enum Entry<'a, V> { 6 | Occupied(&'a mut V), 7 | Vacant(Id, &'a mut sorted::ByKeyVec), 8 | } 9 | impl<'a, V> Entry<'a, V> { 10 | pub(super) fn new(parent: &'a mut sorted::ByKeyVec, key: Id) -> Self { 11 | if parent.contains_key(&key) { 12 | let (change, value) = parent.get_mut(&key).unwrap(); 13 | *change = true; 14 | Entry::Occupied(value) 15 | } else { 16 | Entry::Vacant(key, parent) 17 | } 18 | } 19 | /// Update the `Entry` if it's already occupied, does nothing if it is vacant. 20 | pub fn modify(mut self, f: impl FnOnce(&mut V)) -> Self { 21 | if let Entry::Occupied(entry) = &mut self { 22 | f(*entry); 23 | } 24 | self 25 | } 26 | /// If this `Entry` is vacant, set it to its default value, returns entry. 27 | pub fn or_default(self) -> &'a mut V 28 | where 29 | V: Default, 30 | { 31 | self.or_insert_with(V::default) 32 | } 33 | /// If this `Entry` is vacant, set it to `default`, returns entry. 34 | pub fn or_insert(self, default: V) -> &'a mut V { 35 | self.or_insert_with(|| default) 36 | } 37 | /// If this `Entry` is vacant, set it to `default`, returns entry. 38 | /// 39 | /// `default` is only evaluated if the entry is vacant. 40 | pub fn or_insert_with(self, default: impl FnOnce() -> V) -> &'a mut V { 41 | match self { 42 | Entry::Occupied(value) => value, 43 | Entry::Vacant(key, parent) => { 44 | parent.insert(key, (true, default())); 45 | &mut parent.get_mut(&key).unwrap().1 46 | } 47 | } 48 | } 49 | /// Set this entry to `new_value`, even if already occupied. 50 | pub fn insert(self, new_value: V) -> &'a mut V { 51 | match self { 52 | Entry::Occupied(value) => { 53 | *value = new_value; 54 | value 55 | } 56 | Entry::Vacant(key, parent) => { 57 | parent.insert(key, (true, new_value)); 58 | &mut parent.get_mut(&key).unwrap().1 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /fab_parse/src/rt_fmt.rs: -------------------------------------------------------------------------------- 1 | //! Get a string from reflection. 2 | use std::{any::Any, fmt}; 3 | 4 | use bevy_math::*; 5 | 6 | // TODO(feat): make this public, allow trait reflection 7 | struct Formattable(fn(&RuntimeFormat, &dyn Any, &mut fmt::Formatter) -> Option); 8 | 9 | /// A runtime formatters for rust primitives. 10 | #[derive(PartialEq, Debug, Clone, Copy)] 11 | pub struct RuntimeFormat { 12 | pub width: u16, 13 | pub prec: u16, 14 | pub sign: bool, 15 | pub debug: bool, 16 | } 17 | pub struct DisplayFormatAny<'a> { 18 | any: &'a dyn Any, 19 | format: &'a RuntimeFormat, 20 | } 21 | impl fmt::Display for DisplayFormatAny<'_> { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | self.format.format(self.any, f) 24 | } 25 | } 26 | impl RuntimeFormat { 27 | fn format_display(&self, v: &T, f: &mut fmt::Formatter) -> fmt::Result 28 | where 29 | T: fmt::Display + fmt::Debug, 30 | { 31 | macro_rules! write_runtime { 32 | ($fmt:literal) => { 33 | write!(f, $fmt, v, w = self.width as usize, p = self.prec as usize) 34 | }; 35 | } 36 | match (self.sign, self.debug) { 37 | (true, false) => write_runtime!("{:+0w$.p$}"), 38 | (false, false) => write_runtime!("{:0w$.p$}"), 39 | (true, true) => write_runtime!("{:+0w$.p$?}"), 40 | (false, true) => write_runtime!("{:0w$.p$?}"), 41 | } 42 | } 43 | pub fn display<'a>(&'a self, any: &'a dyn Any) -> DisplayFormatAny<'a> { 44 | DisplayFormatAny { any, format: self } 45 | } 46 | pub fn format(&self, input: &dyn Any, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | fn format_any() -> Formattable { 48 | Formattable(|format, input, f| { 49 | input 50 | .downcast_ref::() 51 | .map(|v| format.format_display(v, f)) 52 | }) 53 | } 54 | macro_rules! all_formats { 55 | ($( $to_format:ty ),* $(,)?) => { 56 | [ $( format_any::<$to_format>() ),* ] 57 | }; 58 | } 59 | let formattables = all_formats![ 60 | bool, f32, f64, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, String, 61 | IVec2, IVec3, IVec4, UVec2, UVec3, UVec4, Vec2, Vec3, Vec4, 62 | ]; 63 | for Formattable(format) in formattables.into_iter() { 64 | if format(self, input, f).is_some() { 65 | return Ok(()); 66 | } 67 | } 68 | Err(fmt::Error) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /design_doc/fab/track_nested_field.md: -------------------------------------------------------------------------------- 1 | # `impl_modify` atomization of nested fields 2 | 3 | **Problem:** we can declare a modify function as reading `.foo`, another as 4 | reading `.foo.bar`, another `.foo.baz` etc. 5 | 6 | This is a problem for dependency resolution, since we lose the relationship 7 | between `.foo` and its subfields. We need to "specialize" `.foo` into its 8 | atomic accessors. 9 | 10 | Note that we can technically have a struct in the shape: 11 | 12 | ``` 13 | Bungle { 14 | zooba: Zooba 15 | gee: Gee { 16 | wooz: Wooz 17 | zaboo: Zaboo { 18 | boolo: Boolo 19 | dodo: Dodo 20 | } 21 | wabwab: WabWab 22 | gomez: Gomez { 23 | zim: Zim 24 | zoom: Zoom 25 | } 26 | } 27 | greebo: Greebo 28 | } 29 | ``` 30 | 31 | Accepting the following paths: 32 | 33 | ``` 34 | bungle.zooba 35 | bungle.gee 36 | bungle.gee.wooz 37 | bungle.gee.zaboo 38 | bungle.gee.zaboo.boolo 39 | bungle.gee.zaboo.dodo 40 | bungle.gee.wabwab 41 | bungle.gee.gomez 42 | bungle.gee.gomez.zim 43 | bungle.gee.gomez.zoom 44 | bungle.greebo 45 | ``` 46 | 47 | However, if our `Modify` only accesses `bungle.zooba` and `bungle.gee`, we 48 | absolutely can ignore the rich complexity of the data structure, we only care 49 | about what we access. 50 | 51 | The problem occurs when we access both a nested field and the field 52 | containing that field, such as `bungle.gee` and `bungle.gee.zaboo`. 53 | 54 | When we run a modify function that changes `bungle.gee`, we need to modify all 55 | functions that depends on `bungle.gee.zaboo`. 56 | 57 | ## Algorithm 58 | 59 | So at one point in our macro, we have the list _A_ of all modify functions, 60 | which themselves have a list of fields they access "accessors". 61 | 62 | In this list we need to find the accessors that are non-atomic 63 | 64 | An accessor _a0_ is atom of _a1_ when: _a1_ prefix _a0_ and _a1_ ≠ _a0_ 65 | 66 | An accessor _a1_ is non-atomic when: 67 | ∃ _a0_ ∈ _A_, _a0_ atom of _a1_ 68 | 69 | In which case, we need to collect all the atoms of _ai_ ∈ _A_, and replace 70 | _a1_ by its atoms. 71 | 72 | Naive implementation: 73 | 74 | * let _M_ be the set of accessors for a modify function 75 | * let _A*_ the set of all **atomic** accessors used by all modify functions 76 | * ∀ _accessor_ in _M_: 77 | * if _A*_ doesn't contain _accessor_: 78 | * remove it from _M_ 79 | * then, ∀ _maybe_suffix_ in _A*_: 80 | * if _maybe_suffix_ atom of _accessor_: 81 | * insert _maybe_suffix_ in _M_ 82 | 83 | ## Integration 84 | 85 | We can't just add the atomized paths to the modifier list, as this list is read 86 | to generate the argument list, not just the dependencies and chages. 87 | -------------------------------------------------------------------------------- /layout/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use bevy::prelude::{Entity, Name, Query}; 4 | use bevy_mod_sysfail::FailureMode; 5 | use thiserror::Error; 6 | 7 | #[derive(Clone, Debug, Hash, Eq, PartialEq)] 8 | pub(super) enum Handle { 9 | Unnamed(Entity), 10 | Named(Name), 11 | } 12 | impl Handle { 13 | pub(super) fn of(entity: Entity, names: &Query<&Name>) -> Self { 14 | match names.get(entity) { 15 | Ok(name) => Handle::Named(name.clone()), 16 | Err(_) => Handle::Unnamed(entity), 17 | } 18 | } 19 | } 20 | impl fmt::Display for Handle { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | Handle::Unnamed(entity) => write!(f, "<{entity:?}>"), 24 | Handle::Named(name) => write!(f, "{name}"), 25 | } 26 | } 27 | } 28 | #[derive(Clone, Debug, PartialEq, Error)] 29 | pub(super) enum Why { 30 | #[error( 31 | "{this} needs to know its {axis}, \ 32 | but {parent}, an ancestor of {this}, doesn't have a defined {axis}. \ 33 | Try specifying the {axis} of any container between {parent} and {this} \ 34 | (included)" 35 | )] 36 | ParentIsStretch { 37 | this: Handle, 38 | parent: Handle, 39 | axis: &'static str, 40 | // TODO: include a "because Stretch/Ratio" explanation 41 | }, 42 | #[error( 43 | "Yo container {this} of size {bounds} contains more stuff than it possibly can! \ 44 | It has {node_children_count} items of total {dir_name} {child_size}. \ 45 | You gotta either make it larger or reduce the size of things within it." 46 | )] 47 | ContainerOverflow { 48 | this: Handle, 49 | bounds: super::Bounds, 50 | node_children_count: u32, 51 | dir_name: &'static str, 52 | child_size: f32, 53 | }, 54 | } 55 | impl FailureMode for Why { 56 | fn log_level(&self) -> bevy_mod_sysfail::LogLevel { 57 | bevy_mod_sysfail::LogLevel::Error 58 | } 59 | 60 | type ID = Handle; 61 | 62 | fn identify(&self) -> Self::ID { 63 | match self { 64 | Why::ParentIsStretch { this, .. } | Why::ContainerOverflow { this, .. } => this.clone(), 65 | } 66 | } 67 | fn display(&self) -> Option { 68 | Some(self.to_string()) 69 | } 70 | } 71 | pub(super) fn parent_is_stretch( 72 | axis: &'static str, 73 | this: Entity, 74 | parent: Entity, 75 | query: &Query<&Name>, 76 | ) -> Why { 77 | Why::ParentIsStretch { 78 | this: Handle::of(this, query), 79 | parent: Handle::of(parent, query), 80 | axis, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /layout/README.md: -------------------------------------------------------------------------------- 1 | # Cuicui Layout 2 | 3 | cuicui defines its own layout algorithm. 4 | 5 | ### Why not Flexbox 6 | 7 | You are writing text to get 2d visual results on screen. 8 | The translation from text to screen should be trivial, easy to do in your head. 9 | Otherwise you need visual feedback to get what you want. 10 | Bevy, even with hot reloading or [`bevy-inspector-egui`] 11 | will always have extremely slow visual feedback. 12 | 13 | Flexbox has too many parameters and depends on implicit properties of UI elements, 14 | it is not possible to emulate it in your head. 15 | 16 | cuicui's layout in contrast to Flexbox is easy to fit in your head. 17 | In fact, I will forecefully push cuicui's layout algorithm in your head 18 | in two short bullet points. 19 | 20 | - A node can be a `Node::Container` and distribute its children 21 | along a `Direction` either by evenly spacing them (`Stretched`) 22 | or putting them directly one after another (`Compact`). 23 | - A `Container`'s size can be expressed as a static value or a fraction 24 | of the size of what contains it. 25 | 26 | That's it. There are some edge cases, but cuicui will ~~yell at you~~ 27 | tell you nicely when you hit them and tell you how to handle them properly. 28 | 29 | ### Why cuicui layout 30 | 31 | On top of the very friendly layout algorithm, 32 | cuicui runs on `bevy_ecs` and therefore can ~~abuse~~ use it as a backing storage. 33 | 34 | Layouts are generally backed by a tree, 35 | [`taffy`]'s implementation of Flexbox internally uses a [`slotmap`]. 36 | cuicui uses the ECS, which is basically a faster slotmap. 37 | 38 | Also, cuicui's layouting system relinquishes control to give more power to users. 39 | Meaning that you can tell cuicui to not manage UI entities `Transform` 40 | and instead chose yourself to build the UI based on what info cuicui gives you. 41 | 42 | ### Limitations 43 | 44 | cuicui layout returns postion as offset from parent, which may not be useful 45 | if you do not use bevy's transform hierarchy. This also locks you into using 46 | bevy hierarchy for your Ui. 47 | 48 | ## TODO 49 | 50 | - [X] Basic algorithm 51 | - [X] Typed constructor 52 | - [X] In depth documentation explaining the algorithm 53 | - [X] Meaningfull error messages when algorithm hits circular constraints 54 | - [ ] Ergonomic macro to define a UI tree 55 | - [ ] `ChildDefined(how_much_larger_than_child)` 56 | - [ ] API cleanup 57 | - [ ] Define a parametrable plugin to add smoothly the layout systems to app 58 | - [ ] Integrate Change detection 59 | - [ ] Accumulate errors instead of early exit. 60 | - [ ] Root expressed as percent of UiCamera 61 | - [ ] Write a tool to make and export layouts. 62 | - [ ] Separate the algo into its own crate independent of bevy 63 | -------------------------------------------------------------------------------- /design_doc/richtext/change_detection.md: -------------------------------------------------------------------------------- 1 | # Change detection 2 | 3 | We want finer-grained change detection, otherwise this is going to be too slow 4 | and cause a lot of spurious uniform updates. 5 | 6 | ## What changes? 7 | 8 | The thing that changes is the `Text` (or rather its `TextSection`s) component 9 | neighbor of `RichText`. 10 | 11 | ## `Modify`-level change detection 12 | 13 | Different kind of `Modify` depends on different inputs: 14 | 15 | - `Color` and `Content` depends on **nothing**, ie: it's monotonic, calling 16 | `apply` more than once on the same `TextSection` is useless for those. 17 | Unless the things those modifiers touch were modified. 18 | - `Font` depends on `Context.fonts`, but this _should_ never change between calls. 19 | - `RelSize` depends on `Context.parent_style.font_size` 20 | - `Dynamic` depends on `Context.{bindings,world_bindings,type_bindings}` and 21 | all the dependency of the `Modify` the binding resolves to. 22 | 23 | ## `Tracker`-level change detection 24 | 25 | "Depends on `Context.bindings` is way too large. 1 `Dynamic` depends on 1 binding. 26 | 27 | ## `RichText`-level change detection 28 | 29 | `RichText` is immutable. But It is that which knows `Modify`es. Hence, this is 30 | who we ask to know what to update when something changes. 31 | 32 | ### `update`-level detection 33 | 34 | See [./nested_modify.md] 35 | 36 | ## Interning 37 | 38 | `RichText` now contains `dependencies`. List of `DependsOn` to `Vec` where 39 | `index` are the indices in `modifiers` of modifiers that depend on `DependsOn`. 40 | 41 | `DependsOn` represents either a binding or any field of the `modify::Context` 42 | struct. 43 | 44 | We should use a string interner or `slotmap` + `HashMap` 45 | to convert the string into a a compact `K` inline value. This way, at runtime, we 46 | do no comparison, and, ideally, we index by the `K`, to avoid hashing and 47 | dereferrencing for comparision, etc. 48 | 49 | This is incompatible in several ways with our current implementation: 50 | 51 | - "type-bound" namespace is distinct from "name-bound" namespace 52 | - There is a distinct "local" and "world" namespace 53 | - Keep track of type of the binding. 54 | 55 | ### local vs world namespaces 56 | 57 | Most notably, when building the RichText, we do not have access to the world 58 | bindings. How are we to reconcile world and local `K`s? 59 | 60 | Idea: `WorldBindings` should be special, and be aware of all local namespaces. 61 | The backing storage could be `Vec<((Entity, BindingId), ModifyBox)>`. 62 | 63 | Problem: however, we typically want the same name used in two different RichText 64 | to be bound to the same value. 65 | 66 | We could force to use a global interner when parsing the format string. 67 | 68 | -------------------------------------------------------------------------------- /richtext/src/modifiers/rich_impl.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use bevy::prelude::*; 4 | use fab::{impl_modify, modify::Indexed, Modify}; 5 | 6 | use super::{GetFont, ModifyBox}; 7 | 8 | impl Indexed for Text { 9 | fn get_mut(&mut self, index: usize) -> Option<&mut TextSection> { 10 | self.sections.as_mut_slice().get_mut(index) 11 | } 12 | } 13 | 14 | /// Operations on bevy [`TextSection`]s. 15 | /// 16 | /// You can create your own operations. At the cost of storing them as a [`ModifyBox`] 17 | /// and having to be careful about what you update. You create such a `Modifier` 18 | /// using [`Modifier::Dynamic`]. 19 | #[impl_modify(cuicui_fab_path = fab, no_derive(Debug))] 20 | #[derive(PartialEq)] 21 | impl Modify for Modifier { 22 | type Context<'a> = GetFont<'a>; 23 | type Item<'a> = &'a mut TextSection; 24 | type MakeItem = TextSection; 25 | type Items<'a, 'b, 'c> = Text; 26 | 27 | /// Set the font to provided `path`. 28 | #[modify(context(get_font), write(.style.font))] 29 | pub fn font(path: &Cow<'static, str>, get_font: &GetFont) -> Handle { 30 | trace!("Apply =font=: {path:?}"); 31 | get_font.get(path).unwrap_or_default() 32 | } 33 | /// Increase the font size relative to the current section. 34 | #[modify(read_write(.style.font_size))] 35 | pub fn rel_size(relative_size: f32, font_size: &mut f32) { 36 | trace!("Apply :rel_size: {relative_size:?}"); 37 | *font_size *= relative_size; 38 | } 39 | /// Set font size to `size`. 40 | #[modify(write(.style.font_size))] 41 | pub fn font_size(size: f32) -> f32 { 42 | size 43 | } 44 | /// Set the color of the [`TextSection`] to `statik`. 45 | #[modify(write(.style.color))] 46 | pub fn color(statik: Color) -> Color { 47 | trace!("Apply ~COLOR~: {statik:?}"); 48 | statik 49 | } 50 | /// Offset the color's Hue by `offset`. 51 | #[modify(read_write(.style.color))] 52 | pub fn hue_offset(offset: f32, color: &mut Color) { 53 | trace!("Apply ~HueOffset~: {offset:?}"); 54 | let mut hsl = color.as_hsla_f32(); 55 | hsl[0] = (hsl[0] + offset) % 360.0; 56 | *color = Color::hsla(hsl[0], hsl[1], hsl[2], hsl[3]); 57 | } 58 | /// Set the text content of the [`TextSection`] to `statik`. 59 | #[modify(write_mut(.value))] 60 | pub fn content(statik: &Cow<'static, str>, value: &mut String) { 61 | trace!("Apply $CONTENT$: {statik:?}"); 62 | value.clear(); 63 | value.push_str(statik); 64 | } 65 | /// Use an arbitrary [`ModifyBox`] to modify this section. 66 | #[modify(dynamic_read_write(depends, changes, item), context(ctx))] 67 | pub fn dynamic(boxed: &ModifyBox, ctx: &GetFont, item: &mut TextSection) { 68 | boxed.apply(ctx, item); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /widges/src/widge/composed.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | use bevy::{ 4 | ecs::{ 5 | query::{ROQueryItem, WorldQuery}, 6 | system::{EntityCommands, SystemParamItem}, 7 | }, 8 | prelude::*, 9 | utils::HashMap, 10 | }; 11 | 12 | use crate::Widge; 13 | 14 | #[derive(Default)] 15 | pub struct Style(HashMap>); 16 | impl Style { 17 | pub fn with(&mut self, value: T) { 18 | self.0.insert(TypeId::of::(), Box::new(value)); 19 | } 20 | } 21 | 22 | pub struct Compose { 23 | pub widge: W, 24 | pub bundle: T, 25 | } 26 | impl Compose { 27 | pub fn with(self, addon: U) -> Compose { 28 | Compose { widge: self.widge, bundle: (addon, self.bundle) } 29 | } 30 | pub fn with_style(widge: W, map: &Style) -> Self { 31 | Compose { widge, bundle: T::from_map(&map.0) } 32 | } 33 | } 34 | pub trait WidgeComposeExt: Sized + Widge { 35 | fn compose(self, addon: U) -> Compose; 36 | } 37 | impl WidgeComposeExt for T { 38 | fn compose(self, addon: U) -> Compose { 39 | Compose { widge: self, bundle: (addon, ()) } 40 | } 41 | } 42 | pub trait FromEcs: 'static { 43 | type QueryP: WorldQuery; 44 | fn from_query_item(item: ROQueryItem<'_, Self::QueryP>) -> Self; 45 | fn from_map(map: &HashMap>) -> Self; 46 | } 47 | impl FromEcs for () { 48 | type QueryP = (); 49 | fn from_query_item(_: ROQueryItem<'_, Self::QueryP>) -> Self {} 50 | fn from_map(_: &HashMap>) -> Self {} 51 | } 52 | impl FromEcs for (T, U) { 53 | type QueryP = (&'static T, U::QueryP); 54 | 55 | fn from_query_item(item: ROQueryItem<'_, Self::QueryP>) -> Self { 56 | (item.0.clone(), U::from_query_item(item.1)) 57 | } 58 | 59 | fn from_map(map: &HashMap>) -> Self { 60 | ( 61 | map.get(&TypeId::of::()) 62 | .map_or_else(T::default, |t| t.downcast_ref::().unwrap().clone()), 63 | U::from_map(map), 64 | ) 65 | } 66 | } 67 | 68 | impl Widge for Compose { 69 | fn spawn(&self, mut commands: EntityCommands) { 70 | commands.insert(self.bundle.clone()); 71 | self.widge.spawn(commands); 72 | } 73 | 74 | type ReadSystemParam<'w, 's> = (W::ReadSystemParam<'w, 's>, Query<'w, 's, T::QueryP>); 75 | 76 | fn read_from_ecs( 77 | entity: In, 78 | (widge, bundle): &SystemParamItem>, 79 | ) -> Option 80 | where 81 | Self: Sized, 82 | { 83 | Some(Compose { 84 | widge: W::read_from_ecs(In(entity.0), widge)?, 85 | bundle: T::from_query_item(bundle.get(entity.0).ok()?), 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /design_doc/activation/old_school_tree.md: -------------------------------------------------------------------------------- 1 | # Activation Tree 2 | 3 | Goal of `label` widge: extend the click area of another widge (such as 4 | `checkbox` or `toggle`) to encompas not only the child widge, but the whole 5 | child widge + `label` text. 6 | 7 | How to do it? Consider this: We use `bevy-ui-navigation`, it relies exclusively 8 | on `Focusable`, and how does `Focusable` knows its being selected? Through the 9 | `NavRequest` events! That we will have total control over. 10 | 11 | So we need some event "rail switch" system on top of the likely-mod_picking 12 | based selection system. 13 | 14 | ## Delegation 15 | 16 | But it does mean we need to be aware of what `Entity` we need to activate with 17 | which screen area. (since the navigation system is `Entity` dependent) 18 | 19 | ### Is this a property of `Widge`? 20 | 21 | A `Widge` may encompass several `Widge`, and they may also have no activations. 22 | 23 | Seems this isn't a widge property, so how to express it? 24 | 25 | ### Activation tree 26 | 27 | A classic solution to this is to give each widge the responsability to handle 28 | clicks. So whenever a `Widge` is embedded into another one, they lose all 29 | rights to handle click events, and must now be entirely dependent on their 30 | parent to charitably transimit them clicks. 31 | 32 | This seems fine at first glance but there is an issue: what about dragging, 33 | hovering, basically things that happens once per frame? 34 | 35 | This would involve descending the activation tree the whole way every frame. 36 | Maybe that's alright? It's like a scene graph, it should enable logarithmic 37 | traversal unlike the current solution which involves iterating over all 38 | entities that have a selectable area. 39 | 40 | Regarding dragging: we also want to disable all other forms of picking when 41 | dragging. It feels an activation tree is the wrong abstraction, if you need to 42 | add to it many knobs in order to make it useable. 43 | 44 | **But responsablity is burden**. ie: a footgun. I build a container widge and 45 | now "oh crap! I need to do all those things!" (or forget, and things may or 46 | may not work, and I would be at a loss at why things are not working). 47 | Supposedly the computer is smarter than us at kind of things. 48 | 49 | => Good default beahvior? => How would that look like? 50 | 51 | #### Bottom-up activation tree? 52 | 53 | Those are issues if parents get all the responsability. What about holocracy? 54 | This allows not carring about activation when you are not working on activation. 55 | 56 | => For this to work, you need to make it explicit that you need to inform 57 | others that you rely on activations, so that widges that manipulate inner 58 | widge's activation zone always work. 59 | 60 | ### How does this look like as an API? 61 | 62 | Ideally no special precaution to make when creating a widge. 63 | 64 | - Assume using `bevy-ui-navigation` 65 | - Assume using `bevy_sprite` 66 | 67 | It should be natural to define the `Widge`, ideally, the `Focusable` component 68 | is added by `Prefab::spawn`. 69 | 70 | If `Focusable` is a widge, then it can handle handle itself maintaining the 71 | relationship. -------------------------------------------------------------------------------- /design_doc/richtext/cresus_linebreak.md: -------------------------------------------------------------------------------- 1 | # How to handle line breaks in cresus text? 2 | 3 | **Problem:** When separating text sections into individual entities, layouting 4 | go bad. No return to new line on `\n`. 5 | 6 | Options: 7 | 8 | - Create a vertial container where each element is a line of text 9 | 10 | Don't see anything better. 11 | 12 | ## How to manage nested container 13 | 14 | So we'd have only a single container. Since the system to query and update items 15 | is customizable, we can introduce our own. 16 | 17 | The question should be: "How to detect line ending and split on that?" 18 | 19 | Issue: consider a \n in the middle of a section, should we split the section 20 | in two? Then this would screw with the resolver, we shouldn't! 21 | 22 | However consider this: if the \n is in the middle of the section. 23 | 24 | ```sh 25 | this is {|some text} foo bar 26 | ↓ 27 | ["this is " "some text" " foo bar"] 28 | ``` 29 | 30 | ```sh 31 | this is {|some text}\nfoo bar 32 | ↓ 33 | ["this is " "some text"] 34 | ["foo bar"] 35 | ``` 36 | 37 | ```sh 38 | this is {|some\ntext} foo bar 39 | ↓ 40 | ["this is " "some\ntext" " foo bar"] # !!!!!!! (1) 41 | ↓ 42 | some 43 | this is text foo bar 44 | # should be: 45 | this is some 46 | text foo bar 47 | ``` 48 | 49 | (1) causes issue. I don't think I've a solution for this. But already supporting 50 | the rest would be enough. 51 | 52 | I could raise an error if we detect a \n neither at the beginning or end of 53 | a section, this would tell the user to split their text in section, so that 54 | 55 | What about using `chop` to split on line ending? It can only work right? 56 | **SOLVED**. 57 | 58 | ## Shape 59 | 60 | ``` 61 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 62 | ┃ NODE ┃ 63 | ┃ direction: Vertical ┃ 64 | ┃ ┃ 65 | ┃ ┌───────────────────────────────┐ ┃ 66 | ┃ │ NODE │ ┃ 67 | ┃ │ direction: Horizontal │ ┃ 68 | ┃ │ │ ┃ 69 | ┃ │ ┌────┐ ┌────┐ ┌────┐ ┌─┐┌┐┌┐│ ┃ 70 | ┃ │ │NODE│ │NODE│ │NODE│ │ ││││││ ┃ 71 | ┃ │ │Text│ │Text│ │Text│ │ ││││││ ┃ 72 | ┃ │ └────┘ └────┘ └────┘ └─┘└┘└┘│ ┃ 73 | ┃ └───────────────────────────────┘ ┃ 74 | ┃ ┃ 75 | ┃ ┌───────────────────────────────┐ ┃ 76 | ┃ │ NODE │ ┃ 77 | ┃ │ direction: Horizontal │ ┃ 78 | ┃ │ │ ┃ 79 | ┃ │ ┌────┐ ┌────┐ ┌────┐ ┌─┐┌┐┌┐│ ┃ 80 | ┃ │ │NODE│ │NODE│ │NODE│ │ ││││││ ┃ 81 | ┃ │ │Text│ │Text│ │Text│ │ ││││││ ┃ 82 | ┃ │ └────┘ └────┘ └────┘ └─┘└┘└┘│ ┃ 83 | ┃ └───────────────────────────────┘ ┃ 84 | ┃ ┃ 85 | ┃ ┌───────────────────────────────┐ ┃ 86 | ┃ │ │ ┃ 87 | ┃ └───────────────────────────────┘ ┃ 88 | ┃ ┃ 89 | ┃ ┌───────────────────────────────┐ ┃ 90 | ┃ └───────────────────────────────┘ ┃ 91 | ┃ ┌───────────────────────────────┐ ┃ 92 | ┃ └───────────────────────────────┘ ┃ 93 | ┃ ┌───────────────────────────────┐ ┃ 94 | ┃ └───────────────────────────────┘ ┃ 95 | ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 96 | ``` 97 | -------------------------------------------------------------------------------- /richtext/src/modifiers/cresus_impl.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ops::Deref}; 2 | 3 | use bevy::prelude::*; 4 | use bevy_fab::Items; 5 | use bevy_layout_offset::UiOffset; 6 | use fab::{impl_modify, Modify}; 7 | 8 | use super::{GetFont, ModifyBox}; 9 | 10 | pub type ModifierQuery = (&'static mut UiOffset, &'static mut Text); 11 | pub type ModifierItem<'a> = (&'a mut UiOffset, &'a mut Text); 12 | 13 | #[derive(Component)] 14 | pub struct Sections(pub Box<[Entity]>); 15 | 16 | impl Deref for Sections { 17 | type Target = [Entity]; 18 | fn deref(&self) -> &Self::Target { 19 | &self.0[..] 20 | } 21 | } 22 | 23 | /// Operations on bevy [`TextSection`]s. 24 | /// 25 | /// You can create your own operations. At the cost of storing them as a [`ModifyBox`] 26 | /// and having to be careful about what you update. You create such a `Modifier` 27 | /// using [`Modifier::Dynamic`]. 28 | #[impl_modify(cuicui_fab_path = fab, no_derive(Debug))] 29 | #[derive(PartialEq)] 30 | impl Modify for Modifier { 31 | type Context<'a> = GetFont<'a>; 32 | type MakeItem = (UiOffset, Text); 33 | type Item<'a> = ModifierItem<'a>; 34 | type Items<'a, 'b, 'c> = Items<'a, 'b, 'c, Sections, ModifierQuery>; 35 | 36 | /// Set the font to provided `path`. 37 | #[modify(context(get_font), write(.1.sections[0].style.font))] 38 | pub fn font(path: &Cow<'static, str>, get_font: &GetFont) -> Handle { 39 | trace!("Apply =font=: {path:?}"); 40 | get_font.get(path).unwrap_or_default() 41 | } 42 | /// Increase the font size relative to the current section. 43 | #[modify(read_write(.1.sections[0].style.font_size))] 44 | pub fn rel_size(relative_size: f32, font_size: &mut f32) { 45 | trace!("Apply :rel_size: {relative_size:?}"); 46 | *font_size *= relative_size; 47 | } 48 | /// Set font size to `size`. 49 | #[modify(write(.1.sections[0].style.font_size))] 50 | pub fn font_size(size: f32) -> f32 { 51 | size 52 | } 53 | /// Set the color of the [`TextSection`] to `statik`. 54 | #[modify(write(.1.sections[0].style.color))] 55 | pub fn color(statik: Color) -> Color { 56 | trace!("Apply ~COLOR~: {statik:?}"); 57 | statik 58 | } 59 | /// Offset the color's Hue by `offset`. 60 | #[modify(read_write(.1.sections[0].style.color))] 61 | pub fn hue_offset(offset: f32, color: &mut Color) { 62 | trace!("Apply ~HueOffset~: {offset:?}"); 63 | let mut hsl = color.as_hsla_f32(); 64 | hsl[0] = (hsl[0] + offset) % 360.0; 65 | *color = Color::hsla(hsl[0], hsl[1], hsl[2], hsl[3]); 66 | } 67 | /// Set the text content of the [`TextSection`] to `statik`. 68 | #[modify(write_mut(.1.sections[0].value))] 69 | pub fn content(statik: &Cow<'static, str>, value: &mut String) { 70 | trace!("Apply $CONTENT$: {statik:?}"); 71 | value.clear(); 72 | value.push_str(statik); 73 | } 74 | /// Use an arbitrary [`ModifyBox`] to modify this section. 75 | #[modify(dynamic_read_write(depends, changes, item), context(ctx))] 76 | pub fn dynamic(boxed: &ModifyBox, ctx: &GetFont, item: ModifierItem) { 77 | boxed.apply(ctx, item); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /fab/examples/impl_modify.rs: -------------------------------------------------------------------------------- 1 | use cuicui_fab::{impl_modify, modify::Modify}; 2 | use pretty_assertions::assert_eq; 3 | 4 | pub struct MyContext { 5 | admin: usize, 6 | closest_city: &'static str, 7 | } 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct Person { 11 | name: &'static str, 12 | surname: &'static str, 13 | age: usize, 14 | } 15 | #[derive(Debug, Clone)] 16 | struct Street { 17 | no: usize, 18 | name: &'static str, 19 | people: usize, 20 | } 21 | #[derive(Debug, Clone)] 22 | struct Administration { 23 | mayor: Person, 24 | secretary: Person, 25 | lawyer: Person, 26 | } 27 | #[derive(Debug, Clone)] 28 | pub struct City { 29 | streets: Vec, 30 | name: &'static str, 31 | admin: Administration, 32 | } 33 | 34 | /// Modify a [`City`]. 35 | #[impl_modify] 36 | impl Modify for ModifyCity { 37 | type Context<'a> = MyContext; 38 | type Item<'a> = &'a mut City; 39 | type MakeItem = City; 40 | type Items<'a, 'b, 'c> = Vec; 41 | 42 | /// This adds to the mayor's age to that of `additional_year` plus the city's name 43 | #[modify(read(admin_name = .admin.mayor.surname), read(.name), write(.admin.mayor.age))] 44 | fn add_mayor_age(additional_years: usize, admin_name: &str, name: &str) -> usize { 45 | admin_name.len() + name.len() + additional_years 46 | } 47 | 48 | /// This sets the name of the secretary to `set_name` 49 | #[modify(write_mut(.admin.secretary.name))] 50 | pub fn secretary_name(set_name: &'static str, name: &mut &'static str) { 51 | *name = set_name; 52 | } 53 | 54 | #[modify(write(.admin.secretary.age))] 55 | pub fn secretary_age(set_age: usize) -> usize { 56 | set_age 57 | } 58 | 59 | #[modify(write(.admin.lawyer))] 60 | pub fn lawyer(set_lawyer: &Person) -> Person { 61 | set_lawyer.clone() 62 | } 63 | 64 | #[modify(write(.admin.secretary))] 65 | pub fn set_secretary(to_set: &Person) -> Person { 66 | to_set.clone() 67 | } 68 | 69 | #[modify(context(.closest_city), write(.name))] 70 | pub fn read_context(closest_city: &'static str) -> &'static str { 71 | closest_city 72 | } 73 | 74 | #[modify(context(general_context), write(.streets[0].people))] 75 | pub fn read_context_general(general_context: &MyContext) -> usize { 76 | general_context.admin 77 | } 78 | 79 | /// Always set name of the third street to that of the city secretary. 80 | #[modify(read(.admin.secretary.name), write(street = .streets[3].name))] 81 | fn name_street_after_secretary(name: &'static str) -> &'static str { 82 | name 83 | } 84 | 85 | #[modify(read_write(.admin.mayor.age))] 86 | fn make_mayor_older(by_age: usize, age: &mut usize) { 87 | *age += by_age; 88 | } 89 | 90 | #[modify(dynamic_read_write(reewd, uwurites))] 91 | fn arbitrary_changes(inner: usize, item: &mut City) { 92 | item.streets[inner].no = inner; 93 | } 94 | } 95 | 96 | fn main() { 97 | let secretary_fields = ModifyCity::set_secretary_changes(); 98 | let age_fields = ModifyCity::secretary_age_changes(); 99 | let name_fields = ModifyCity::name_street_after_secretary_depends(); 100 | assert_eq!(age_fields | name_fields, secretary_fields); 101 | } 102 | -------------------------------------------------------------------------------- /fab/src/resolve/make/is_static.rs: -------------------------------------------------------------------------------- 1 | use enumset::EnumSet; 2 | use smallvec::SmallVec; 3 | 4 | use super::{MakeModify, ModifyKind}; 5 | use crate::modify::{FieldsOf, Modify}; 6 | 7 | pub(super) struct CheckStatic { 8 | parents_range_end: SmallVec<[u32; 4]>, 9 | static_parent_fields: SmallVec<[FieldsOf; 4]>, 10 | all_static_fields: FieldsOf, 11 | } 12 | impl CheckStatic { 13 | pub(super) fn new() -> Self { 14 | CheckStatic { 15 | parents_range_end: SmallVec::default(), 16 | static_parent_fields: SmallVec::default(), 17 | all_static_fields: EnumSet::EMPTY, 18 | } 19 | } 20 | fn ends_before_last(&self, modify: &MakeModify) -> bool { 21 | self.parents_range_end 22 | .last() 23 | .map_or(true, |end| modify.range.end < *end) 24 | } 25 | fn pop_parent(&mut self) { 26 | if let Some(to_reset) = self.static_parent_fields.pop() { 27 | // SAFETY: `parents_range_end` and `static_parent_fields` always 28 | // have the same size 29 | unsafe { self.parents_range_end.pop().unwrap_unchecked() }; 30 | 31 | self.all_static_fields -= to_reset; 32 | } 33 | } 34 | fn push_parent(&mut self, modify: &MakeModify) { 35 | let changes = modify.changes(); 36 | let old_changes = self.all_static_fields; 37 | let modify_changes = changes - old_changes; 38 | 39 | // There is no changes, therefore nothing new to track 40 | if modify_changes.is_empty() { 41 | return; 42 | } 43 | self.all_static_fields |= changes; 44 | if self.ends_before_last(modify) { 45 | // Keep track of fields we added to `all_static_fields` so that 46 | // we can remove them later 47 | self.static_parent_fields.push(modify_changes); 48 | self.parents_range_end.push(modify.range.end); 49 | } else { 50 | // SAFETY: never fails because of `ends_before_last` is only false 51 | // when there is at least one element in static_parent_fields 52 | let last_changes = unsafe { self.static_parent_fields.last_mut().unwrap_unchecked() }; 53 | *last_changes |= changes; 54 | } 55 | } 56 | fn update_parents(&mut self, modify: &MakeModify) { 57 | let end = modify.range.end; 58 | // any parent that has an end smaller than modify is actually not a parent, 59 | // so we pop them. 60 | let first_real_parent = self 61 | .parents_range_end 62 | .iter() 63 | // TODO(clean): Is this right? Shouldn't it be < over <=? 64 | .rposition(|p_end| *p_end <= end); 65 | let len = self.parents_range_end.len(); 66 | 67 | let pop_count = len - first_real_parent.unwrap_or(0); 68 | for _ in 0..pop_count { 69 | self.pop_parent(); 70 | } 71 | } 72 | pub(super) fn is_static(&mut self, modify: &MakeModify) -> bool { 73 | self.update_parents(modify); 74 | 75 | let mut depends = modify.depends().iter(); 76 | let no_deps = depends.all(|dep| self.all_static_fields.contains(dep)); 77 | let is_binding = matches!(&modify.kind, ModifyKind::Bound { .. }); 78 | 79 | let is_static = no_deps && !is_binding; 80 | 81 | if is_static { 82 | self.push_parent(modify); 83 | } 84 | is_static 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /datazoo/src/enum_multimap.rs: -------------------------------------------------------------------------------- 1 | //! A [multimap] optimized for [`EnumSetType`] keys. 2 | //! 3 | //! [multimap]: https://en.wikipedia.org/wiki/Multimap 4 | 5 | use std::{fmt, marker::PhantomData, mem::size_of}; 6 | 7 | use enumset::{EnumSet, EnumSetType}; 8 | 9 | use crate::{jagged_const_row_array, JaggedConstRowArray}; 10 | 11 | /// A [multimap] stored in a [`JaggedConstRowArray`]. 12 | /// 13 | /// The key set need to be bound and exhaustively known at compile time, 14 | /// ie: it must be an enum derived with `#[derive(EnumSetType)]`. 15 | /// 16 | /// Use it as follow: 17 | /// `EnumMultimap` 18 | /// 19 | /// [multimap]: https://en.wikipedia.org/wiki/Multimap 20 | pub struct EnumMultimap { 21 | inner: JaggedConstRowArray, 22 | _key: PhantomData, 23 | } 24 | impl fmt::Debug for EnumMultimap { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | f.debug_tuple("EnumMultimap").field(&self.inner).finish() 27 | } 28 | } 29 | impl EnumMultimap { 30 | pub fn all_rows(&self, set: EnumSet) -> impl Iterator { 31 | self.inner.all_rows(set) 32 | } 33 | #[must_use] 34 | pub fn row(&self, key: K) -> &[V] { 35 | let index = key.enum_into_u32() as usize; 36 | self.inner.row(index) 37 | } 38 | /// Get `V` at exact `direct_index` ignoring row sizes, 39 | /// acts as if the whole array was a single row. 40 | /// 41 | /// `None` when `direct_index` is out of bound. 42 | #[must_use] 43 | pub fn get(&self, direct_index: usize) -> Option<&V> { 44 | self.inner.get(direct_index) 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone)] 49 | pub struct Builder { 50 | pub rows: Vec>, 51 | _key: PhantomData, 52 | } 53 | impl Default for Builder { 54 | fn default() -> Self { 55 | Self::new() 56 | } 57 | } 58 | impl Builder { 59 | #[must_use] 60 | pub fn new() -> Self { 61 | Builder { rows: Vec::with_capacity(CLM), _key: PhantomData } 62 | } 63 | pub fn insert(&mut self, key: K, values: impl Iterator) { 64 | let row = key.enum_into_u32() as usize; 65 | self.rows.insert(row, values.collect()); 66 | } 67 | pub fn build(self) -> Result, jagged_const_row_array::Error> { 68 | // Compile time error when `CLM` is not the correct value. 69 | // This works around a limitation of rust' type system, 70 | // where it is impossible to use associated constants in generic const position. 71 | assert!(K::BIT_WIDTH as usize == CLM + 1); 72 | assert!(size_of::() >= size_of::()); 73 | 74 | let mut end = 0; 75 | let mut ends = Box::new([0; CLM]); 76 | let mut data = Vec::new(); 77 | for (i, values) in self.rows.into_iter().enumerate() { 78 | end += values.len() as u32; 79 | data.extend(values.into_vec()); 80 | if i < CLM { 81 | ends[i] = end; 82 | } 83 | } 84 | Ok(EnumMultimap { 85 | inner: JaggedConstRowArray::new(ends, data.into_boxed_slice())?, 86 | _key: PhantomData, 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /design_doc/richtext/system-as-modify.md: -------------------------------------------------------------------------------- 1 | # Systems as `Modify` 2 | 3 | - `impl_modify` is a real weird macro 4 | - `Modify` relies extensively on `impl_modify` for dependency detection 5 | - `Modify` is not extensible: Users can't add their own modifiers (unless using `dyn TextModifier`) 6 | 7 | Individual `Modify` could be "systems"; Rather, functions with a special 8 | argument called `Path![Component; .field]`. `Path` would have following properties: 9 | 10 | - Things it reads from 11 | - Things it writes to 12 | - Component accessed (to convert to `WorldQuery` so that it can be queried) 13 | 14 | There is a few challenges yet: 15 | 16 | 1. How do bindings work? 17 | 2. How to build the static modifier list? 18 | 3. How to build the field list? 19 | 4. How to operate on a list within a component? 20 | 5. How to compile time error on attempt to access mutably same field? 21 | 22 | (5) we would need to panic at runtime. 23 | 24 | (2) Could have a builder method on `App`, that has `.add_modifier(modifer)` 25 | and `.finish()` to "commit" the final set of modifiers 26 | 27 | **Problem:** This would prevent extensiblility right? Maybe we can provide 28 | `add_modifier_list` and export modifier lists? (**solved**) 29 | 30 | (1): bindings work by mutating a `Modify`. If `Modify` are systems, they can't 31 | be mutated. 32 | 33 | The `Modify` systems could accept an additional "local state" parameter. 34 | Store the local state outside of the system, and pass it to the system when 35 | it runs. 36 | 37 | **Problem:** The enum generated by `impl_modify` has the advantage of locality, 38 | since it's just an enum. When `Resolver::update` we iterate over a list of them. 39 | 40 | This might be costly, if the state is stored in a list of erased values 41 | (like a `Box<[Box]>`), bunch of pointer chasing, and memory fragmentation. 42 | 43 | Note that we could also store them in the ECS. But how? Not as `Resource` since 44 | we want multiple instances of the same type runnng. 45 | 46 | Probably in a way similar to how I did it with `bvyfst_hollow_scene`, an heterogenous list. 47 | It would accumulate the type as new builder methods are invocked, and erase it on `.finish()`. 48 | --> This doesn't work because we need to store state per _modifier instance in the format string_ 49 | rather than one per system modifier. 50 | --> I wonder if it's possible to create heterogenous lists with `bevy_ptr`? 51 | --> Maybe create an internal `World` and store the states in `Component`s. 52 | Each instance of a `Modify` would have its own entity. 53 | we could require users to derive a `ModifyState`, which itself would derive `Component` 54 | (**unsolved**) 55 | 56 | (3) currently relies on generating an `enum` with all the possible fields. 57 | I guess I could replace the `EnumMultimap` 58 | by something more reasonable. (**solved**) 59 | 60 | (4) If the `Modify::Items` individual `Item` has greater section accuracy than 61 | individual entities, we can't operate in `Item` within a `Vec` within a `Component`. 62 | This would throw out of the window `RichText` and potentially many unknown future use-cases. 63 | 64 | Could it be possible to make `Path!` accept a special syntax to "splice" an inner 65 | `Vec` into multiple items? 66 | 67 | - Like in `jq`, we could do `Path![Text; .sections[].style]` 68 | to 69 | - But then we'd have to handle multiple-level access like: 70 | 71 | ```rust 72 | fn print_alignment(align: Path![Text; .alignment], content: Path![Text; .sections[].value]) { 73 | // ... 74 | } 75 | ``` 76 | 77 | Worse! What about a component with two distinct `Vec` fields? This needs design. 78 | (**unknown**) -------------------------------------------------------------------------------- /datazoo/src/index_multimap.rs: -------------------------------------------------------------------------------- 1 | //! A [multimap] that goes from an integer to multiple integers. 2 | //! 3 | //! [multimap]: https://en.wikipedia.org/wiki/Multimap 4 | use std::marker::PhantomData; 5 | 6 | use crate::{BitMatrix, Index}; 7 | 8 | /// A [multimap] that goes from an integer to multiple integers. 9 | /// 10 | /// This is a 1-to-N mapping, see [`RawIndexMap`] for 1-to-(1|0) mapping. 11 | /// [`JaggedBitset`] is an alternative in case you expect the largest 12 | /// row to be way larger than the smaller ones. 13 | /// 14 | /// The size in bytes of this `struct` is the lowest multiple of 4 over 15 | /// `max(K) * max(V) / 8` 16 | /// 17 | /// You'll notice the size is not dependent on the number of values stored 18 | /// (in fact, [`IndexMultimap`] **does not** store any value). But rather the 19 | /// values being stored themselves. 20 | /// 21 | /// It is not recommended to use this data structure if you expect to have 22 | /// large values in your key/value space. 23 | /// 24 | /// [`IndexMultimap`] might be a good solution if you have an index to a small 25 | /// array or an incrementing counter. 26 | /// 27 | /// # Example 28 | /// 29 | /// ``` 30 | /// use cuicui_datazoo::IndexMultimap; 31 | /// 32 | /// let multimap: IndexMultimap = [ 33 | /// (0, 1), (0, 5), (0, 2), (0, 2), 34 | /// (1, 7), (1, 0), (1, 1), 35 | /// (2, 32), (2, 0), (2, 12), (2, 2), (2, 11), (2, 10), (2, 13), (2, 4), 36 | /// (4, 1) 37 | /// ].into_iter().collect(); 38 | /// let rows: [&[usize]; 5] = [ 39 | /// &[1, 2, 5], 40 | /// &[0, 1, 7], 41 | /// &[0, 2, 4, 10, 11, 12, 13, 32], 42 | /// &[], 43 | /// &[1], 44 | /// ]; 45 | /// for (i, row) in rows.iter().enumerate() { 46 | /// let multimap_row: Box<[usize]> = multimap.get(&i).collect(); 47 | /// assert_eq!(*row, &*multimap_row, "{i}"); 48 | /// } 49 | /// ``` 50 | /// 51 | /// [multimap]: https://en.wikipedia.org/wiki/Multimap 52 | /// [`RawIndexMap`]: crate::RawIndexMap 53 | /// [`JaggedBitset`]: crate::JaggedBitset 54 | #[derive(Debug, Clone)] 55 | pub struct IndexMultimap> { 56 | assocs: BitMatrix, 57 | value_count: usize, 58 | _idx_ty: PhantomData, 59 | } 60 | impl> IndexMultimap { 61 | /// Get the values associated with given `K` 62 | pub fn get<'a>(&'a self, key: &K) -> impl Iterator + 'a { 63 | let index = key.get(); 64 | let max_index = self.assocs.height(self.value_count); 65 | (max_index > index) 66 | .then(|| self.assocs.row(self.value_count, index).map(|i| V::from(i))) 67 | .into_iter() 68 | .flatten() 69 | } 70 | } 71 | impl + Index> FromIterator<(K, V)> for IndexMultimap { 72 | /// Create a [`IndexMultimap`] with all associations. 73 | /// 74 | /// Note that `K` and `V` will be dropped. 75 | fn from_iter>(iter: T) -> Self { 76 | let mut max_value = 0; 77 | let mut max_key = 0; 78 | 79 | let key_values = iter 80 | .into_iter() 81 | .map(|(k, v)| { 82 | max_key = max_key.max(k.get() + 1); 83 | max_value = max_value.max(v.get() + 1); 84 | (k, v) 85 | }) 86 | .collect::>(); 87 | 88 | let (width, height) = (max_value, max_key); 89 | let mut assocs = BitMatrix::new_with_size(width, height); 90 | 91 | for (key, value) in key_values.iter() { 92 | assocs.enable_bit(width, value.get(), key.get()).unwrap(); 93 | } 94 | IndexMultimap { assocs, value_count: width, _idx_ty: PhantomData } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /widges/src/widge/action_button.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bevy::{ 4 | ecs::system::{EntityCommands, SystemParamItem}, 5 | prelude::*, 6 | }; 7 | use bevy_mod_sysfail::quick_sysfail; 8 | use bevy_ui_navigation::prelude::*; 9 | 10 | use crate::{Prefab, Widge, WorldValue}; 11 | 12 | #[derive(Component)] 13 | pub struct ActionButton { 14 | pub action: Box, NavRequest), Out = ()>>, 15 | } 16 | impl ActionButton { 17 | pub fn new, NavRequest), (), P>>(action: F) -> Self { 18 | ActionButton { action: Box::new(IntoSystem::into_system(action)) } 19 | } 20 | } 21 | pub struct ActionButtonPrefab, NavRequest), (), P> + Clone> { 22 | pub action: S, 23 | _params: PhantomData, 24 | } 25 | impl, NavRequest), (), P> + Clone> Prefab 26 | for ActionButtonPrefab 27 | { 28 | type Param = (); 29 | 30 | fn spawn(&self, mut commands: EntityCommands, _: &mut ()) { 31 | commands.insert(ActionButton::new(self.action.clone())); 32 | } 33 | } 34 | impl, NavRequest), (), P> + Clone> WorldValue 35 | for ActionButtonPrefab 36 | { 37 | type Value = (); 38 | 39 | type ReadParam<'w, 's> = (); 40 | 41 | fn read(_: Entity, _: &SystemParamItem>) -> Option { 42 | Some(()) 43 | } 44 | } 45 | impl, NavRequest), (), P> + Clone> Widge 46 | for ActionButtonPrefab 47 | { 48 | } 49 | 50 | #[quick_sysfail] 51 | fn check_action_button_valid( 52 | world: &World, 53 | buttons: Query<(&ActionButton, Entity, Option<&Name>), Changed>, 54 | ) { 55 | let action_button_id = world.component_id::()?; 56 | for (button, entity, maybe_name) in &buttons { 57 | let access = button.action.component_access(); 58 | if access.has_read(action_button_id) { 59 | let name = maybe_name.map_or(format!("{entity:?}"), |n| n.to_string()); 60 | panic!( 61 | "{name}'s ActionButton's system accesses the ActionButton component! \ 62 | It shouldn't, otherwise this would cause conflicting access. \ 63 | Please do not query for ActionButton in actions" 64 | ); 65 | } 66 | } 67 | } 68 | fn activate_button( 69 | world: &World, 70 | mut actions: Query<&mut ActionButton>, 71 | mut nav_events: EventReader, 72 | ) { 73 | for nav_event in nav_events.iter() { 74 | if let NavEvent::NoChanges { from, request } = nav_event { 75 | let Ok(mut button) = actions.get_mut(*from.first()) else { continue; }; 76 | let input = (from.to_vec(), *request); 77 | // SAFETY: 78 | // - This is an exclusive system, therefore the only concurrent 79 | // access we have to worry about is &mut ActionButton. 80 | // - We check in check_action_button_valid that the system doesn't 81 | // access in any way ActionButton. 82 | // Ok, this is unsafe, because it's not guarenteed, you need to 83 | // make sure the only place activate_button is used, it is added 84 | // with a `at_end()` descriptor. 85 | unsafe { 86 | button.action.run_unsafe(input, world); 87 | } 88 | } 89 | } 90 | } 91 | pub struct Plug; 92 | impl Plugin for Plug { 93 | fn build(&self, app: &mut App) { 94 | app.add_system(check_action_button_valid) 95 | .add_system(activate_button.at_end().after(check_action_button_valid)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /fab/src/resolve/minimal.rs: -------------------------------------------------------------------------------- 1 | //! A minimal resolver that only support binding to individual sections 2 | //! and ignores all other features. 3 | 4 | use std::iter; 5 | 6 | use datazoo::Index; 7 | use log::{error, warn}; 8 | use nonmax::NonMaxU32; 9 | 10 | use super::{MakeModify, ModifyKind, Resolver}; 11 | use crate::binding::View; 12 | use crate::modify::{Changing, Indexed, MakeItem, Modify}; 13 | 14 | /// A resolver with minimal overhead and functionalities. 15 | /// 16 | /// What this can do: 17 | /// - Generate a default `Vec` based on static modifiers in `modifiers` 18 | /// - Store and trigger bindings that modify a single sections. Unlike [`DepsResolver`] 19 | /// this is a simple array lookup. 20 | /// 21 | /// [`DepsResolver`]: super::DepsResolver 22 | pub struct MinResolver { 23 | indices: Box<[Option]>, 24 | } 25 | impl Resolver for MinResolver { 26 | fn new M::MakeItem>( 27 | modifiers: Vec>, 28 | default_section: F, 29 | ctx: &M::Context<'_>, 30 | ) -> (Self, Vec) { 31 | let warn_range = "Skipping bindings touching more than a single sections in MinResolver."; 32 | 33 | let Some(section_count) = modifiers.iter().map(|m| m.range.end).max() else { 34 | return (MinResolver { indices: Box::new([])}, vec![]) 35 | }; 36 | let mut sections = iter::repeat_with(&default_section) 37 | .take(section_count as usize) 38 | .collect::>(); 39 | let mut bindings = Vec::new(); 40 | 41 | for modifier in modifiers.into_iter() { 42 | match modifier.kind { 43 | ModifyKind::Bound { binding, .. } => { 44 | if modifier.range.end != modifier.range.start + 1 { 45 | warn!("{warn_range}"); 46 | } else { 47 | bindings.push((binding.get(), modifier.range.start)) 48 | } 49 | } 50 | ModifyKind::Modify(modify) => { 51 | let range = modifier.range.start as usize..modifier.range.end as usize; 52 | 53 | // SAFETY: `sections`'s `len` is `max(range.end)` meaning we are always 54 | // within bounds. 55 | let sections = unsafe { sections.get_unchecked_mut(range) }; 56 | 57 | sections.iter_mut().for_each(|section| { 58 | if let Err(err) = modify.apply(ctx, section.as_item()) { 59 | error!("Error occured when applying modify: {err}"); 60 | } 61 | }); 62 | } 63 | } 64 | } 65 | let mut indices = if let Some(max_binding) = bindings.iter().map(|b| b.0).max() { 66 | vec![None; max_binding].into_boxed_slice() 67 | } else { 68 | return (MinResolver { indices: Box::new([]) }, sections); 69 | }; 70 | for &(binding, section) in &bindings { 71 | indices[binding] = Some(NonMaxU32::new(section).unwrap()); 72 | } 73 | (MinResolver { indices }, sections) 74 | } 75 | 76 | fn update<'a>( 77 | &'a self, 78 | to_update: &mut M::Items<'_, '_, '_>, 79 | _: &'a Changing, 80 | bindings: View<'a, M>, 81 | ctx: &M::Context<'_>, 82 | ) { 83 | bindings.changed().for_each(|(binding, modify)| { 84 | let Some(Some(index)) = self.indices.get(binding.get()) else { return; }; 85 | let index = index.get() as usize; 86 | 87 | let Some(section) = to_update.get_mut(index) else { return; }; 88 | if let Err(err) = modify.apply(ctx, section) { 89 | error!("Error occured when applying modify: {err}"); 90 | } 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /datazoo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::nursery)] 2 | #![allow(clippy::use_self)] 3 | #![doc = include_str!("../README.md")] 4 | 5 | pub mod bitmatrix; 6 | pub mod bitmultimap; 7 | pub mod bitset; 8 | pub mod enum_bitmatrix; 9 | pub mod enum_multimap; 10 | // pub mod index_map; 11 | pub mod index_multimap; 12 | pub mod jagged_bitset; 13 | pub mod jagged_const_row_array; 14 | pub mod jagged_vec; 15 | pub mod raw_index_map; 16 | pub mod sorted; 17 | 18 | /// Integer division rounded up. 19 | const fn div_ceil(lhf: usize, rhs: usize) -> usize { 20 | (lhf + rhs - 1) / rhs 21 | } 22 | const fn safe_n_mask(n: u32) -> u32 { 23 | // https://stackoverflow.com/questions/52573447/creating-a-mask-with-n-least-significant-bits-set 24 | match n { 25 | n if n >= u32::BITS => u32::MAX, 26 | n => (1 << n) - 1, 27 | } 28 | } 29 | trait MostSignificantBit { 30 | fn most_significant_bit(&self) -> u32; 31 | } 32 | impl MostSignificantBit for u32 { 33 | fn most_significant_bit(&self) -> u32 { 34 | u32::BITS - self.leading_zeros() 35 | } 36 | } 37 | impl MostSignificantBit for usize { 38 | fn most_significant_bit(&self) -> u32 { 39 | usize::BITS - self.leading_zeros() 40 | } 41 | } 42 | 43 | /// Get an `usize` from `Self`. 44 | #[rustfmt::skip] 45 | mod index { 46 | pub trait Index { fn get(&self) -> usize; } 47 | impl Index for usize { fn get(&self) -> usize { *self } } 48 | impl Index for u32 { fn get(&self) -> usize { *self as usize } } 49 | impl Index for u64 { fn get(&self) -> usize { *self as usize } } 50 | } 51 | 52 | pub use bitmatrix::BitMatrix; 53 | pub use bitmultimap::BitMultimap; 54 | pub use bitset::Bitset; 55 | pub use enum_bitmatrix::EnumBitMatrix; 56 | pub use enum_multimap::EnumMultimap; 57 | pub use index::Index; 58 | pub use index_multimap::IndexMultimap; 59 | pub use jagged_bitset::JaggedBitset; 60 | pub use jagged_const_row_array::JaggedConstRowArray; 61 | pub use jagged_vec::JaggedVec; 62 | pub use raw_index_map::RawIndexMap; 63 | pub use sorted_iter::assume::{AssumeSortedByItemExt, AssumeSortedByKeyExt}; 64 | pub use sorted_iter::{ 65 | sorted_iterator::SortedByItem, sorted_pair_iterator::SortedByKey, SortedIterator, 66 | SortedPairIterator, 67 | }; 68 | #[cfg(test)] 69 | mod tests { 70 | use super::*; 71 | 72 | #[test] 73 | fn msb() { 74 | assert_eq!(101_u32.most_significant_bit(), 7); 75 | assert_eq!(10_u32.most_significant_bit(), 4); 76 | assert_eq!(0b0000_u32.most_significant_bit(), 0); 77 | assert_eq!(0b0001_u32.most_significant_bit(), 1); 78 | assert_eq!(0b0010_u32.most_significant_bit(), 2); 79 | assert_eq!(0b0011_u32.most_significant_bit(), 2); 80 | assert_eq!(0b0100_u32.most_significant_bit(), 3); 81 | assert_eq!(0b0101_u32.most_significant_bit(), 3); 82 | assert_eq!(0b0110_u32.most_significant_bit(), 3); 83 | assert_eq!(0b0111_u32.most_significant_bit(), 3); 84 | assert_eq!(0b1000_u32.most_significant_bit(), 4); 85 | assert_eq!(0b1001_u32.most_significant_bit(), 4); 86 | assert_eq!(0b1010_u32.most_significant_bit(), 4); 87 | assert_eq!(0b1011_u32.most_significant_bit(), 4); 88 | assert_eq!(0b1100_u32.most_significant_bit(), 4); 89 | assert_eq!(0b1101_u32.most_significant_bit(), 4); 90 | assert_eq!(0b1110_u32.most_significant_bit(), 4); 91 | assert_eq!(0b1111_u32.most_significant_bit(), 4); 92 | assert_eq!(0b0100_0000_0000u32.most_significant_bit(), 11); 93 | assert_eq!(0b1000_0000_0000_0000u32.most_significant_bit(), 16); 94 | assert_eq!(0b0010_0000_0000_0000u32.most_significant_bit(), 14); 95 | assert_eq!(0xf000_0000u32.most_significant_bit(), 32); 96 | assert_eq!(0xffff_ffffu32.most_significant_bit(), 32); 97 | assert_eq!(0xffff_0000u32.most_significant_bit(), 32); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /bevy_fab/src/trait_extensions.rs: -------------------------------------------------------------------------------- 1 | //! Extensions to the `App` to 2 | use bevy::{app::App, prelude::Mut, reflect::Reflect}; 3 | use fab::binding::Entry; 4 | use fab_parse::Styleable; 5 | 6 | use crate::{ 7 | fmt_system::{FmtSystem, IntoFmtSystem}, 8 | track::UserFmt, 9 | BevyModify, Styles, WorldBindings, 10 | }; 11 | 12 | // Initially, I wanted to pass the styles to `RichTextPlugin` and insert them 13 | // at initialization. But it's impossible due to `Styles` containing `Box`, 14 | // which cannot be wrought to implement `Clone`. I temporarilly considered using 15 | // `Arc` instead. But then it is impossible to call the functions, since 16 | // `Arc` is immutable. So I opted to add the following 17 | 18 | /// Extension trait to add `alias` and `chop` modifiers to the string format parser. 19 | pub trait AppStylesExtension { 20 | /// Insert a new style before all others. 21 | fn overwrite_style(&mut self, style: F) -> &mut Self 22 | where 23 | F: FnMut(Styleable) -> Styleable + Send + Sync + 'static; 24 | /// Add a new style after existing ones. 25 | fn add_style(&mut self, style: F) -> &mut Self 26 | where 27 | F: FnMut(Styleable) -> Styleable + Send + Sync + 'static; 28 | } 29 | impl AppStylesExtension for App { 30 | fn overwrite_style(&mut self, style: F) -> &mut Self 31 | where 32 | F: FnMut(Styleable) -> Styleable + Send + Sync + 'static, 33 | { 34 | let Some(mut styles) = self.world.get_resource_mut::>() else { return self; }; 35 | styles.overwrite(style); 36 | self 37 | } 38 | fn add_style(&mut self, style: F) -> &mut Self 39 | where 40 | F: FnMut(Styleable) -> Styleable + Send + Sync + 'static, 41 | { 42 | let Some(mut styles) = self.world.get_resource_mut::>() else { return self; }; 43 | styles.add(style); 44 | self 45 | } 46 | } 47 | 48 | pub trait AppFormattersExtension { 49 | fn add_user_fmt(&mut self, name: impl AsRef, fmt: UserFmt) -> &mut Self; 50 | 51 | // Add a formatter that may READ (only) the world. 52 | fn add_sys_fmt>( 53 | &mut self, 54 | name: impl AsRef, 55 | fmt: impl IntoFmtSystem, 56 | ) -> &mut Self; 57 | 58 | // Add a simple function formatter. 59 | fn add_dyn_fn_fmt( 60 | &mut self, 61 | name: impl AsRef, 62 | fmt: impl Fn(&dyn Reflect, Entry) + Send + Sync + 'static, 63 | ) -> &mut Self { 64 | self.add_user_fmt(name, UserFmt::from_fn(fmt)) 65 | } 66 | 67 | // Add a simple function formatter, with a known type as input. 68 | fn add_fn_fmt( 69 | &mut self, 70 | name: impl AsRef, 71 | fmt: impl Fn(&T, Entry) + Send + Sync + 'static, 72 | ) -> &mut Self { 73 | self.add_user_fmt( 74 | name, 75 | UserFmt::from_fn(move |reflect, e| { 76 | let value = reflect.downcast_ref().unwrap(); 77 | fmt(value, e) 78 | }), 79 | ) 80 | } 81 | } 82 | impl AppFormattersExtension for App { 83 | fn add_user_fmt(&mut self, name: impl AsRef, fmt: UserFmt) -> &mut Self { 84 | let mut world_bindings = self.world.resource_mut::>(); 85 | world_bindings.add_user_fmt(name, fmt); 86 | self 87 | } 88 | fn add_sys_fmt>( 89 | &mut self, 90 | name: impl AsRef, 91 | fmt: impl IntoFmtSystem, 92 | ) -> &mut Self { 93 | self.world 94 | .resource_scope(|world, mut bindings: Mut>| { 95 | bindings.add_user_fmt(name, UserFmt::from_system(fmt, world)); 96 | }); 97 | self 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /reflect_query/src/custom_ref.rs: -------------------------------------------------------------------------------- 1 | //! Define [`Ref`], a custom version of bevy's [`Ref`](BRef) that can be 2 | //! constructed from `EntityRef` and mapped over. 3 | use std::ops::Deref; 4 | 5 | use bevy::prelude::{DetectChanges, Ref as BRef}; 6 | 7 | /// A custom version of bevy's [`Ref`](BRef) that can be 8 | /// constructed from `EntityRef` and mapped over. 9 | /// 10 | /// Due to a limitation in bevy, it's impossible to use the bevy `Ref` for this crate. 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 12 | pub struct Ref<'w, T: ?Sized> { 13 | pub(crate) value: &'w T, 14 | pub(crate) is_added: bool, 15 | pub(crate) is_changed: bool, 16 | } 17 | impl<'w, T: ?Sized> Ref<'w, T> { 18 | /// Was this added since last time the system this is called from ran? 19 | #[must_use] 20 | pub const fn is_added(&self) -> bool { 21 | self.is_added 22 | } 23 | /// Was this changed since last time the system this is called from ran? 24 | #[must_use] 25 | pub const fn is_changed(&self) -> bool { 26 | self.is_changed 27 | } 28 | /// Return the inner reference with its full lifetime. 29 | /// 30 | /// Rust's [`Deref`] trait can't produce references with a lifetime longer 31 | /// than that of the `Ref` itself (with `&'a Ref<'w>`, the lifetime will 32 | /// always be `'a`). 33 | /// 34 | /// This can become an issue in certain scenarios, this is why this method 35 | /// exists. 36 | /// 37 | /// Note that since `Ref` is `Copy`, this **doesn't** consume the value, you 38 | /// can keep using it. 39 | #[must_use] 40 | pub const fn into_inner(self) -> &'w T { 41 | self.value 42 | } 43 | /// Apply a function `f` returning a result to the inner value, 44 | /// and get a `Ref` with the return value of that function. 45 | /// Returning `Err` if `f` returns `Err`. 46 | /// 47 | /// # Errors 48 | /// Returns `Err` if `f` returns `Err`. 49 | pub fn map_failable( 50 | self, 51 | f: impl FnOnce(&T) -> Result<&U, E>, 52 | ) -> Result, E> { 53 | Ok(Ref { 54 | value: f(self.value)?, 55 | is_added: self.is_added, 56 | is_changed: self.is_changed, 57 | }) 58 | } 59 | /// Apply a function `f` to the inner value, 60 | /// and get a `Ref` with the return value of that function. 61 | #[must_use] 62 | pub fn map(self, f: impl FnOnce(&T) -> &U) -> Ref<'w, U> { 63 | Ref { 64 | value: f(self.value), 65 | is_added: self.is_added, 66 | is_changed: self.is_changed, 67 | } 68 | } 69 | /// Convert a bevy [`Ref`](BRef) into a `reflect_query` `Ref`, applying 70 | /// a function while converting it. 71 | /// 72 | /// You can pass `|i| i` as function if you don't wish to convert the value. 73 | #[must_use] 74 | pub fn map_from(bevy: BRef<'w, U>, f: impl FnOnce(&U) -> &T) -> Self { 75 | Ref { 76 | is_added: bevy.is_added(), 77 | is_changed: bevy.is_changed(), 78 | value: f(bevy.into_inner()), 79 | } 80 | } 81 | } 82 | impl<'w, T: ?Sized> From> for Ref<'w, T> { 83 | fn from(value: BRef<'w, T>) -> Self { 84 | Ref::map_from(value, |i| i) 85 | } 86 | } 87 | impl<'w, T: ?Sized> AsRef for Ref<'w, T> { 88 | fn as_ref(&self) -> &T { 89 | self.value 90 | } 91 | } 92 | impl<'w, T: ?Sized> Deref for Ref<'w, T> { 93 | type Target = T; 94 | 95 | fn deref(&self) -> &Self::Target { 96 | self.value 97 | } 98 | } 99 | impl<'w, 'a, T> IntoIterator for &'a Ref<'w, T> 100 | where 101 | &'a T: IntoIterator, 102 | { 103 | type Item = <&'a T as IntoIterator>::Item; 104 | type IntoIter = <&'a T as IntoIterator>::IntoIter; 105 | 106 | fn into_iter(self) -> Self::IntoIter { 107 | self.value.into_iter() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /datazoo/src/enum_bitmatrix.rs: -------------------------------------------------------------------------------- 1 | //! A bitset similar to [`BitMatrix`][super::BitMatrix], 2 | //! indexed by [`EnumSetType`]. 3 | 4 | use std::{any, fmt, marker::PhantomData, mem, ops::Range}; 5 | 6 | use enumset::{EnumSet, EnumSetType}; 7 | use sorted_iter::{assume::AssumeSortedByItemExt, sorted_iterator::SortedByItem, SortedIterator}; 8 | 9 | use crate::{div_ceil, Bitset}; 10 | 11 | // TODO(clean): Manual impl of Debug using braile to show internal state. 12 | /// A bitset similar to [`BitMatrix`][super::BitMatrix], 13 | /// but with a fixed column and row count, indexed by `R` [`EnumSetType`]. 14 | #[derive(Clone, PartialEq, Eq)] 15 | pub struct EnumBitMatrix(Bitset>, PhantomData); 16 | impl fmt::Debug for EnumBitMatrix { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | f.debug_tuple("EnumBitMatrix") 19 | .field(&any::type_name::()) 20 | .field(&self.0) 21 | .finish() 22 | } 23 | } 24 | 25 | impl EnumBitMatrix { 26 | /// Create a new [`EnumBitMatrix`]. 27 | /// 28 | /// # Panics 29 | /// 30 | /// When `width * #Variant(T) > u32::MAX`. 31 | #[must_use] 32 | pub fn new(width: u32) -> Self { 33 | let len = width.checked_mul(R::BIT_WIDTH).unwrap() as usize; 34 | let data = vec![0; div_ceil(len, mem::size_of::())]; 35 | Self(Bitset(data.into_boxed_slice()), PhantomData) 36 | } 37 | /// Enable bits from `iter` for given `row`. 38 | /// 39 | /// Note that items of `iter` not within [`bit_width`](Self::bit_width) are ignored, 40 | /// and already enabled bits stay enabled. 41 | pub fn set_row(&mut self, row: R, iter: impl Iterator) { 42 | let row = row.enum_into_u32(); 43 | let width = self.bit_width(); 44 | 45 | let start = row * width; 46 | 47 | for to_set in iter.filter(|i| *i < width).map(|i| i + start) { 48 | // SAFETY: to_set is always within range, as we `*i < width` 49 | unsafe { self.0.enable_bit(to_set as usize).unwrap_unchecked() }; 50 | } 51 | } 52 | /// The width in bits of individual rows of this [`EnumBitMatrix`]. 53 | pub const fn bit_width(&self) -> u32 { 54 | self.0 .0.len() as u32 / R::BIT_WIDTH 55 | } 56 | /// Iterate over enabled bits in `row`, limited to provided `range`. 57 | /// 58 | /// If the range doesn't fit within [`0..bit_width`](Self::bit_width), 59 | /// it will be truncated to fit within that range. 60 | pub fn row(&self, row: R, mut range: Range) -> impl SortedIterator + '_ { 61 | let row = row.enum_into_u32(); 62 | let width = self.bit_width(); 63 | 64 | range.end = range.end.min(range.start + width); 65 | range.start = range.start.min(range.end); 66 | 67 | let start = row * width; 68 | 69 | let subrange_start = (start + range.start) as usize; 70 | let subrange_end = (start + range.end) as usize; 71 | 72 | self.0 73 | .ones_in_range(subrange_start..subrange_end) 74 | .map(move |i| i - start) 75 | .assume_sorted_by_item() 76 | } 77 | 78 | /// Iterate over enabled bits in all `rows`, limited to provided `range`. 79 | /// 80 | /// [`Rows`] is a sorted iterator. 81 | pub const fn rows(&self, rows: EnumSet, range: Range) -> Rows { 82 | Rows { range, rows, bitset: self } 83 | } 84 | } 85 | 86 | /// Iterator from [`EnumBitMatrix::rows`]. 87 | #[derive(Debug, Clone, PartialEq, Eq)] 88 | pub struct Rows<'a, R: EnumSetType> { 89 | range: Range, 90 | rows: EnumSet, 91 | 92 | bitset: &'a EnumBitMatrix, 93 | } 94 | impl<'a, R: EnumSetType> Iterator for Rows<'a, R> { 95 | type Item = u32; 96 | 97 | fn next(&mut self) -> Option { 98 | if self.range.is_empty() { 99 | return None; 100 | } 101 | let range = self.range.clone(); 102 | self.range.start += 1; 103 | 104 | self.rows 105 | .iter() 106 | .find_map(|row| self.bitset.row(row, range.clone()).next()) 107 | } 108 | } 109 | impl SortedByItem for Rows<'_, R> {} 110 | -------------------------------------------------------------------------------- /datazoo/src/index_map.rs: -------------------------------------------------------------------------------- 1 | //! An associative array without hashing. 2 | 3 | use std::marker::PhantomData; 4 | 5 | use crate::{Bitset, Index}; 6 | 7 | /// An [associative arrays] of small integers. 8 | /// 9 | /// This is a 1-to-(1|0) mapping, see [`IndexMultimap`] for N-to-M mapping. 10 | /// 11 | /// The size in bytes of this `struct` is the lowest multiple of 4 over 12 | /// `max(K) * log₂(len(V) + 1) / 8` 13 | /// 14 | /// You'll notice the size depends on the max value of `K`. 15 | /// 16 | /// It is not recommended to use this data structure if you expect to have 17 | /// large values in your key space. 18 | /// 19 | /// # Example 20 | /// 21 | /// ``` 22 | /// todo!() 23 | /// ``` 24 | /// 25 | /// [`IndexMultimap`]: crate::IndexMultimap 26 | pub struct IndexMap { 27 | /// A matrix of `max(K)` rows of `log₂(len(V) + 1)` bits, each row represents 28 | /// an index. 29 | /// 30 | /// If all the bits of the row are set, then it means the row is **empty**. 31 | indices: Bitset>, 32 | values: Vec, 33 | _tys: PhantomData, 34 | } 35 | impl IndexMap { 36 | pub fn new() -> Self { 37 | IndexMap { 38 | indices: Bitset(Vec::new()), 39 | values: Vec::new(), 40 | _tys: PhantomData, 41 | } 42 | } 43 | fn offset_width(&self) -> usize { 44 | (u32::BITS - self.values.len().leading_zeros()) as usize 45 | } 46 | fn offset_of(&self, key: &K) -> usize { 47 | key.get() * self.offset_width() 48 | } 49 | fn value_mask(&self) -> u32 { 50 | (1 << self.offset_width()) - 1 51 | } 52 | fn get_index(&self, key: &K) -> Option { 53 | let value = self.indices.u32_at(self.offset_of(key))?; 54 | let value = value & self.value_mask(); 55 | // != means the row is not empty 56 | (value != self.value_mask()).then_some(value as usize) 57 | } 58 | fn push(&mut self, key: &K, value: V) { 59 | let offset = self.offset_of(key); 60 | let iter = Ones::from_single(value).map(|v| v + offset as u32); 61 | self.indices 62 | .disable_range(offset..offset + self.value_width); 63 | self.indices.extend(iter); 64 | } 65 | /// Get the value associated with `index`, `None` if there isn't. 66 | pub fn get<'a>(&'a self, key: &K) -> Option<&'a V> { 67 | let index = self.get_index(key)?; 68 | // TODO(perf): may be able to assume Some 69 | self.values.get(index) 70 | } 71 | /// Remove value associated with `key`. Afterward, calling `map.get(key)` 72 | /// will return `None`. 73 | pub fn remove(&mut self, key: &K) -> Option { 74 | let index = self.get_index(index)?; 75 | let offset = self.offset_of(key); 76 | self.indices.extend(offset..offset + self.value_width); 77 | } 78 | /// Set value of `key` to `value`. 79 | /// 80 | /// Returns the previous value if any. 81 | /// 82 | /// This is fairly costly if `key` didn't already have 83 | pub fn set(&mut self, key: &K, value: V) -> Option { 84 | match self.get_index(index) { 85 | Some(pre_existing) => mem::replace(&mut self.values[pre_existing], value), 86 | None => self.push(key, value), 87 | } 88 | } 89 | } 90 | impl FromIterator for IndexMap { 91 | /// Create a [`IndexMap`] where value at `k` will be `value` in `(key, value)` 92 | /// the last item where `key == k`. 93 | /// 94 | /// Note that all `K`s and duplicate `V`s will be dropped. 95 | fn from_iter>(iter: T) -> Self { 96 | let mut max_value = 0; 97 | let mut max_key = 0; 98 | 99 | let key_values = iter 100 | .into_iter() 101 | .map(|(k, v)| { 102 | max_key = max_key.max(k.get()); 103 | max_value = max_value.max(v.get()); 104 | (k, v) 105 | }) 106 | .collect::>(); 107 | 108 | let max_value = u32::try_from(max_value).unwrap(); 109 | let mut map = IndexMap::new_with_size(max_value, max_key); 110 | 111 | for (key, value) in key_values.iter() { 112 | map.set(key, value); 113 | } 114 | map 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /design_doc/richtext/dynamic_format.md: -------------------------------------------------------------------------------- 1 | # Dynamic format 2 | 3 | Problem: 4 | 5 | We want to specify an access path to a `Reflect` + a way to translate 6 | it into a `Modify`. 7 | 8 | Currently, we only have a "binding", which means we push to format string the 9 | "already digested" `Modify`, format string doesn't care about processing. 10 | 11 | But now that we have instructions on what to extract and how to display, this 12 | is a different story. 13 | 14 | ## The primitive idea 15 | 16 | The idea was to "simply" add a modifier called `Format` orsmth. But this doesn't 17 | work. 18 | 19 | Because `Modify` doesn't allow reading other `Modify`, and in any case this would 20 | require sorting/associating `Modify`es which seems bad. 21 | 22 | ## The better idea 23 | 24 | So instead of having `Format` be a separate `Modify`, we have it be part of 25 | `Dynamic`. 26 | 27 | This has implications for the syntax of the format string. 28 | 29 | If we merge `Dynamic` with `Format`, we need to declare them together. Well 30 | _need_ is a bad word. More like "this finally let us". 31 | 32 | But we need to ask ourselves: should we change the syntax? 33 | See, currently we have: 34 | 35 | ```rust 36 | #[derive(Resource, Reflect)] 37 | #[reflect(Resource)] 38 | struct Options { 39 | audio_volume: f32, 40 | } 41 | ``` 42 | 43 | ``` 44 | {Content: $Res.Options.audio_volume, Format: {:.0} } 45 | ``` 46 | 47 | With the format and path merged, we get: 48 | 49 | ``` 50 | {Content: $Res.Options.audio_volume:.0 } 51 | ``` 52 | 53 | Fine, but we kinda lose the relationship between the path and the formating, 54 | so what about: 55 | 56 | ``` 57 | {Content: {Res.Options.audio_volume:.0} } 58 | ``` 59 | 60 | Since we have named formatters, we can also do this, also for completness, let's 61 | see what it looks like if we modify the already existing syntax: 62 | 63 | ``` 64 | {Content: $Res.Options.audio_volume:show_audio } // Old syntax 65 | {Content: {Res.Options.audio_volume:show_audio} } // New syntax 66 | 67 | {Content: $name_binding } 68 | {Content: {name_binding} } 69 | ``` 70 | 71 | Alternatively, we could do a bash-like `${foobar:baz}`. A major downside is that 72 | now the synax for section is different in content and metadata value position. 73 | 74 | This mirrors the rust format syntax, cool. However, it can also be misleading, 75 | as we could think that we can use sections in metadata value position, which 76 | is false; This is a thing entirely different from a section (when in metadata 77 | position), while it's a section in non-metadata position. 78 | 79 | It's also a bit extra tricky, because it could be interpreted as a section 80 | with a single metadata field. 81 | 82 | I'm not sure what is the best approach. Let's weight the two sides: 83 | 84 | **PROS**: 85 | 86 | - We can re-use pre-existing syntax, not have to introduce a sigil like `$` 87 | - The grouping between the format element and the binding path is self-evident. 88 | - Can re-use rust knowledge (a bit) to use it 89 | - By re-using, we can take advantage of concept similarity. 90 | 91 | **CONS**: 92 | 93 | - It does look like rust format strings, but has a few significant differences. 94 | - It does look like a section, but isn't really one 95 | - It might be difficult to parse in a way that allows discriminating with a 96 | section. 97 | 98 | **Idea**: What about _forcing_ a space between `:` and the metadata value in 99 | sections, so that it's always clearly distinct from formats 100 | 101 | **Idea 2**: It causes too many parsing woes. I opted to prefix bindings + formats 102 | by `fmt:`, this way the grammar is unambiguous, although weird for users, and 103 | misleading because now we use "format string" both for the whole text and the 104 | special case of bindings that have formatting applied to them. 105 | 106 | **Idea 3**: Just `peek`ing for `}` and only accepting if we close immediately. 107 | This fixes our parsing woes. 108 | 109 | ## Implementation 110 | 111 | Making `Dynamic` a struct with a `format` and `access` field doesn't work. As 112 | `Dynamic` **only** accesses bindings by name at runtime (in fact we could 113 | change this to access by an interned ID of sort). 114 | 115 | What we really want is a way to say to `RichTextPartial` to register some trackers. 116 | 117 | We have things: 118 | 119 | - The binding name in `RichText`, it is as before, `modifiers::Dynamic::ByName`. 120 | - A `Tracker` the datastructure that reads from `&World` and updates `modify::Bindings`. 121 | 122 | `RichText` builder should return both. Then, the user of `RichText` can add 123 | themselves the tracker to the ECS. 124 | -------------------------------------------------------------------------------- /assets/fonts/FiraMono-LICENSE: -------------------------------------------------------------------------------- 1 | Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. 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 | -------------------------------------------------------------------------------- /design_doc/fab/track_a_field.md: -------------------------------------------------------------------------------- 1 | # Track individual fields 2 | 3 | Problem: We want to modify only fields of `Modify` implementors, and declare 4 | those changes statically, not in a separate method. 5 | 6 | ### Idea 1: a macro 7 | 8 | ```rust 9 | #[impl_modify] 10 | impl Modify for CustomModify { 11 | type Context<'a> = GetFont<'a>; 12 | 13 | // Variant 1: Works a bit like wgsl, but it is fairly verbose and likely to 14 | // mess with RA and syntax highlight 15 | fn shift_hue( 16 | offset: f32, 17 | #[path(item.style.color)] 18 | color: Color, 19 | ) -> #[path(item.style.color)] Color 20 | { 21 | let hsl = color.as_hsla_f32(); 22 | hsl[0] = (hsl[0] + offset) % 360.0; 23 | Color::hsla(hsl[0], hsl[1], hsl[2], hsl[3]) 24 | } 25 | // Variant 2: declare in #[…] which fields are read, how to pass them 26 | // to the function. The argument with the last field name will be given 27 | // the field value. 28 | // - If read_write, it is passed as Mut<_>: Potential advantage is we could 29 | // let users declare they didn't change the value. 30 | // - If need to rename field, can use syntax `foo = item.path.to.foozz`. 31 | // - The `writes` is where the value is written to. By comparing the previous 32 | // and new value, we could reduce update rate. 33 | // - Might be harder to implement 34 | #[reads(item.style.color)] 35 | #[writes(item.style.color)] 36 | fn shift_hue(offset: f32, color: Color) -> Color; 37 | 38 | #[read_write(item.style.color)] 39 | #[read_write(color = item.style.color)] 40 | fn shift_hue(offset: f32, color: Mut); 41 | 42 | // Variant 3: 43 | // The arguments are passed implicitly here, not declared as method arguments. 44 | // might mess with RA, and more difficult to understand 45 | #[reads(.style.color)] // #[reads(.style.color as color)] 46 | #[writes(.style.color)] 47 | fn shift_hue(offset: f32) -> Color; 48 | 49 | fn color(color: Color) -> #[path(.style.color)] Color { 50 | color 51 | } 52 | 53 | pub fn font(name: String, ctx: #[ctx] GetFont) -> #[path(.style.font)] Handle { 54 | trace!("Apply =Font=: {:?}", self.0); 55 | ctx.get_font(&self.0).unwrap() 56 | } 57 | } 58 | ``` 59 | 60 | Should generate 61 | 62 | ```rust 63 | #[derive(EnumSetType)] 64 | enum CustomModifyField { 65 | StyleColor, 66 | StyleFont, 67 | } 68 | enum CustomModify { 69 | ShiftHue { offset: f32 }, 70 | Color { color: Color }, 71 | Font { name: String }, 72 | } 73 | impl Modify for CustomModify { 74 | type Field = CustomModifyField; 75 | type Context<'a> = GetFont<'a>; 76 | 77 | fn apply(&self, ctx: &Self::Context<'_>, prefab: &mut TextSection) -> anyhow::Result<()> { 78 | match self { 79 | Self::ShiftHue { offset } => prefab.style.color = { 80 | let color = prefab.style.color; 81 | let hsl = color.as_hsla_f32(); 82 | hsl[0] = (hsl[0] + offset) % 360.0; 83 | Color::hsla(hsl[0], hsl[1], hsl[2], hsl[3]) 84 | }, 85 | Self::Color { color } => prefab.style.color = { 86 | color 87 | }, 88 | Self::Font { name } => prefab.style.font = { 89 | let ctx = ctx; 90 | trace!("Apply =Font=: {:?}", self.0); 91 | ctx.get_font(&self.0).unwrap() 92 | } 93 | } 94 | fn depends(&self) -> EnumSet { 95 | match self { 96 | Self::ShiftHue { .. } => Self::Field::StyleColor, 97 | Self::Color { .. } => EnumSet::EMPTY, 98 | Self::Font { .. } => EnumSet::EMPTY, 99 | } 100 | } 101 | fn changes(&self) -> EnumSet { 102 | match self { 103 | Self::ShiftHue { .. } => Self::Field::StyleColor, 104 | Self::Color { .. } => Self::Field::StyleColor, 105 | Self::Font { .. } => Self::Field::StyleFont, 106 | } 107 | } 108 | } 109 | impl CustomModify { 110 | const fn shift_hue(offset: f32) -> Self { 111 | Self::ShiftHue { offset } 112 | } 113 | const fn color(color: Color) -> Self { 114 | Self::Color { color } 115 | } 116 | pub const fn font(name: String) -> Self { 117 | Self::Font { name } 118 | } 119 | } 120 | ``` 121 | 122 | ### The fors and againsts 123 | 124 | The bevy community will _hate_ this use of macros, since it's very magic, 125 | even though the code it generates is relatively trivial. 126 | 127 | I can't concieve of how to avoid it though. Central elements of this macro are: 128 | 129 | 1. Creating the `_FOO_Field` enum, to list all things it can access 130 | 2. Declaring which fields are accessed by each modify. It guarentees that only 131 | the bits that are declared as modified are indeed modified. 132 | 3. Automatically building `_FOO_` as an `enum` where each variant is a separate 133 | operation on the item. 134 | - No need to define each variants and have 3 different matches -------------------------------------------------------------------------------- /fab_derive/README.md: -------------------------------------------------------------------------------- 1 | # cuici fab derive marcro 2 | 3 | This crate implements the `impl_modify` macro. 4 | 5 | `impl_modify` is an attribute macro to define `Modify` with correct 6 | change tracking without too much headache. 7 | 8 | # Syntax 9 | 10 | See the next section for a detailed explanation with semantic information. 11 | 12 | `#[impl_modify]` only works on `impl` block, it should be formatted as a 13 | trait implementation for `Modify` as follow: 14 | 15 | Consider the following structs: 16 | 17 | ```text 18 | struct Color([f32;4]); 19 | impl Color { 20 | fn as_hsla(&self) -> [f32;4] { self.0 } 21 | fn hsla(inner: [f32;4]) -> Self { Color(inner) } 22 | } 23 | 24 | struct TextStyle { 25 | color: Color, 26 | } 27 | 28 | struct TextSection { 29 | value: String, 30 | style: TextStyle, 31 | } 32 | ``` 33 | 34 | ```text 35 | use cuicui_fab_derive::impl_modify; 36 | 37 | #[impl_modify] 38 | impl Modify for CustomModify { 39 | // ... 40 | ``` 41 | 42 | The first item (declaration) within that `impl` block should be a type 43 | definition for `Context`. The type definition accepts either 1 or 0 lifetime 44 | parameters: 45 | 46 | ```text 47 | // ... 48 | type Context<'a> = (); 49 | // ... 50 | ``` 51 | 52 | All other items are function declarations. They can be documented. You can 53 | decorate them with the `modify` attributes. 54 | 55 | ```text 56 | // ... 57 | #[modify(read_write(.style.color))] 58 | fn shift_hue(hue_offset: f32, color: &mut Color) { 59 | let mut hsl = color.as_hsla_f32(); 60 | hsl[0] = (hsl[0] + hue_offset) % 360.0; 61 | *color = Color::hsla(hsl); 62 | } 63 | // ... 64 | } 65 | ``` 66 | 67 | ## Attributes 68 | 69 | All the `modify` attributes are: 70 | 71 | - `#[modify(context(value))]`: Pass the context to the parameter named `value`. 72 | It must be of the same type as `type Context`. 73 | - `#[modify(read(it.path.to.value))]`: Which field of the item (`it` stands 74 | for item) to read. The last field name (here `value`) is the argument 75 | used to pass thie value to the function. 76 | - `#[modify(write(it.path.to.value))]`: which field of the item to update 77 | with the return value of this function, a function can only have a 78 | single `write` attribute, and this excludes the use of `read_write`. 79 | - `#[modify(read_write(it.path.to.value))]`: 80 | - `#[modify(write_mut(it.path.to.value))]`: This works like `read_write` 81 | (the argument to modify is passed as a `&mut`) but we assume that you do 82 | not read or use the content of the value in the return value. This is 83 | typically useful when you want to overwrite a string value without 84 | allocating anything. 85 | 86 | ```text 87 | #[modify(read(.style.font_size))] 88 | fn some_change(font_size: f32) { 89 | // ... 90 | } 91 | ``` 92 | 93 | You might want to rename the field name before passing it as argument. 94 | To do so, use the following syntax: 95 | 96 | ```text 97 | #[modify(read(size = .style.font_size))] 98 | fn some_change(size: f32) { 99 | // ... 100 | } 101 | ``` 102 | 103 | # How does it look? 104 | 105 | You define a `Modify` operating on an arbitrary item, here, we will use the 106 | bevy `TextSection`, element of a `Text` component. 107 | 108 | `Modify` itself is a trait, you need a type on which to implement it. Here, 109 | we chose to name our `Modify` type `CustomModify` (ik very creative). 110 | 111 | **Do not define `CustomModify` yourself**, `impl_modify` will implement it 112 | for you. See the next section to learn how `CustomModify` looks like. 113 | 114 | `CustomModify` operations are defined as functions in the `impl` block. 115 | Those functions have two kinds of arguments: 116 | 117 | 1. Internal parameters: will be constructors of `CustomModify`. 118 | 2. Passed parameters: they are fields of the modiify item `I`, 119 | here `TextSection` 120 | 121 | ```text 122 | #[impl_modify] 123 | impl Modify for CustomModify { 124 | type Context<'a> = GetFont<'a>; 125 | 126 | } 127 | ``` 128 | 129 | # How does it work? 130 | 131 | `impl_modify` creates two enums: 132 | 133 | - `CustomModify`: One variant per free function defined in the `impl_modify` 134 | block. 135 | - `CustomModifyField`: each accessed field, implements `EnumSetType`, 136 | to be used as `Modify::Field` of `CustomModify`. 137 | 138 | `CustomModify` implements `Modify` and has one constructor per 139 | defined function in `impl_modify`. In our last example, we defined: 140 | 141 | - `shift_hue(offset: f32)` 142 | - `color(set_to: Color)` 143 | - `font(name: String)` 144 | 145 | Therefore, our `CustomModify` will look as follow: 146 | 147 | ```text 148 | enum CustomModify { 149 | ShiftHue { offset: f32 }, 150 | Color { set_to: Color }, 151 | Font { name: String }, 152 | } 153 | impl CustomModify { 154 | const fn shift_hue(offset: f32) -> Self { 155 | Self::ShiftHue { offset } 156 | } 157 | const fn color(set_to: Color) -> Self { 158 | Self::Color { set_to } 159 | } 160 | pub const fn font(name: String) -> Self { 161 | Self::Font { name } 162 | } 163 | } 164 | ``` -------------------------------------------------------------------------------- /bevy_fab/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::new_without_default)] 2 | //! Integrate the [`fab`] crate with bevy. 3 | 4 | pub mod fmt_system; 5 | mod local; 6 | mod make; 7 | mod track; 8 | pub mod trait_extensions; 9 | mod world; 10 | 11 | use std::ops::Deref; 12 | use std::{fmt::Arguments, marker::PhantomData}; 13 | 14 | use bevy::app::{App, CoreSet, Plugin}; 15 | use bevy::ecs::prelude::*; 16 | use bevy::ecs::query::WorldQuery; 17 | use bevy::ecs::system::{EntityCommands, StaticSystemParam, SystemParam, SystemParamItem}; 18 | use fab::modify::{FieldsOf, Indexed}; 19 | use fab_parse::Parsable; 20 | use reflect_query::predefined::QueryablePlugin; 21 | 22 | pub use fmt_system::{FmtSystem, IntoFmtSystem}; 23 | pub use local::LocalBindings; 24 | pub use make::{parse_into_resolver_system, ParseFormatString}; 25 | pub use reflect_query::ReflectQueryable; 26 | pub use track::UserFmt; 27 | pub use world::{update_hooked, Hook, StyleFn, Styles, WorldBindings}; 28 | 29 | pub trait MakeMut<'a, I: 'a> { 30 | fn make_mut(self) -> I; 31 | } 32 | impl<'a, T1, T2> MakeMut<'a, (&'a mut T1, &'a mut T2)> for (Mut<'a, T1>, Mut<'a, T2>) { 33 | fn make_mut(self) -> (&'a mut T1, &'a mut T2) { 34 | (self.0.into_inner(), self.1.into_inner()) 35 | } 36 | } 37 | 38 | // omg please don't look at this, I swear this is temporary 39 | /// A [`fab::Modify`] that works on a bevy component and can be inserted in the ECS. 40 | pub trait BevyModify: Parsable + Send + Sync + 'static { 41 | type Param: SystemParam; 42 | type ItemsCtorData: Send + Sync; 43 | 44 | fn set_content(&mut self, s: Arguments); 45 | fn init_content(s: Arguments) -> Self; 46 | 47 | fn context<'a>(param: &'a SystemParamItem) -> Self::Context<'a>; 48 | 49 | fn spawn_items( 50 | extra: &Self::ItemsCtorData, 51 | items: Vec, 52 | cmds: &mut EntityCommands, 53 | ); 54 | fn add_update_system(app: &mut App); 55 | } 56 | 57 | pub struct Items<'a, 'w, 's, C, It: WorldQuery> { 58 | children: Option<&'a C>, 59 | query: Query<'w, 's, It>, 60 | } 61 | 62 | impl<'a, 'w, 's, C: Component + Deref, It: WorldQuery> Items<'a, 'w, 's, C, It> { 63 | pub fn new(children: Option<&'a C>, query: Query<'w, 's, It>) -> Self { 64 | Items { children, query } 65 | } 66 | } 67 | impl<'a, 'w, 's, C, Wq, M> Indexed for Items<'a, 'w, 's, C, Wq> 68 | where 69 | C: Component + Deref, 70 | Wq: WorldQuery, 71 | M: BevyModify, 72 | for<'b> Wq::Item<'b>: MakeMut<'b, M::Item<'b>>, 73 | { 74 | #[inline] 75 | fn get_mut(&mut self, index: usize) -> Option> { 76 | let &entity = self.children?.get(index)?; 77 | Some(self.query.get_mut(entity).ok()?.make_mut()) 78 | } 79 | } 80 | 81 | pub fn update_children_system( 82 | mut query: Query<(&mut LocalBindings, Option<&C>)>, 83 | mut world_bindings: ResMut>, 84 | ctx_params: StaticSystemParam, 85 | items_query: Query, 86 | ) where 87 | C: Component + Deref, 88 | BM: for<'a, 'w, 's> Parsable = Items<'a, 'w, 's, C, Wq>>, 89 | for<'b> Wq::Item<'b>: MakeMut<'b, BM::Item<'b>>, 90 | FieldsOf: Sync + Send, 91 | { 92 | let context = BM::context(&ctx_params); 93 | let mut items = Items { children: None, query: items_query }; 94 | for (mut local_data, children) in &mut query { 95 | items.children = children; 96 | local_data.update(&mut items, &world_bindings, &context); 97 | } 98 | world_bindings.bindings.reset_changes(); 99 | } 100 | 101 | pub fn update_component_items( 102 | mut query: Query<(&mut LocalBindings, &mut BM::Items<'_, '_, '_>)>, 103 | mut world_bindings: ResMut>, 104 | params: StaticSystemParam, 105 | ) where 106 | for<'a, 'b, 'c> BM::Items<'a, 'b, 'c>: Component, 107 | FieldsOf: Sync + Send, 108 | { 109 | let context = BM::context(¶ms); 110 | for (mut local_data, mut items) in &mut query { 111 | local_data.update(&mut items, &world_bindings, &context); 112 | } 113 | world_bindings.bindings.reset_changes(); 114 | } 115 | 116 | /// Manages [`BevyModify`] living in the ECS as [`LocalBindings`] and a global 117 | /// [`WorldBindings`]. Also [`Hook`]s to automatically update reflection-based 118 | /// bindings. 119 | pub struct FabPlugin(PhantomData); 120 | impl FabPlugin 121 | where 122 | FieldsOf: Sync + Send, 123 | { 124 | pub fn new() -> Self { 125 | FabPlugin(PhantomData) 126 | } 127 | } 128 | impl Plugin for FabPlugin 129 | where 130 | FieldsOf: Sync + Send, 131 | { 132 | fn build(&self, app: &mut App) { 133 | use CoreSet::PostUpdate; 134 | app.add_plugin(QueryablePlugin) 135 | .init_resource::>() 136 | .init_resource::>() 137 | .add_system(update_hooked::.in_base_set(PostUpdate)) 138 | .add_system(parse_into_resolver_system::); 139 | BM::add_update_system(app); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /bevy_fab/src/track/write.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, TryLockError}; 2 | use std::{fmt, sync::Mutex}; 3 | 4 | use bevy::prelude::World; 5 | use bevy::{reflect::Reflect, utils::HashMap}; 6 | use fab::binding::{self, Id}; 7 | use fab_parse::{hook::Format, RuntimeFormat}; 8 | use thiserror::Error; 9 | 10 | use crate::fmt_system::IntoFmtSystem; 11 | use crate::{fmt_system::FmtSystem, BevyModify}; 12 | 13 | #[derive(Debug, Error)] 14 | pub enum UserWriteError { 15 | #[error("Can't lock a fmt system, this is a cuicui bug, please open an issue")] 16 | Locked, 17 | #[error("Somehow a thread locking on 'formatter' panicked, this is a cuicui bug, please open an issue")] 18 | Poisoned, 19 | } 20 | impl From> for UserWriteError { 21 | fn from(value: TryLockError) -> Self { 22 | match value { 23 | TryLockError::Poisoned(_) => UserWriteError::Poisoned, 24 | TryLockError::WouldBlock => UserWriteError::Locked, 25 | } 26 | } 27 | } 28 | 29 | /// A writer defined by the user, it allows converting arbitrary values into `M` modifiers. 30 | pub enum UserFmt { 31 | // TODO(feat): Allow failure 32 | System(Arc>>), 33 | Function(Arc) + Send + Sync>), 34 | } 35 | impl UserFmt { 36 | pub fn from_system>( 37 | system: impl IntoFmtSystem, 38 | world: &mut World, 39 | ) -> Self { 40 | UserFmt::System(Arc::new(Mutex::new(system.into_fmt_system(world)))) 41 | } 42 | pub fn from_fn( 43 | dyn_fn: impl Fn(&dyn Reflect, binding::Entry) + Send + Sync + 'static, 44 | ) -> Self { 45 | UserFmt::Function(Arc::new(dyn_fn)) 46 | } 47 | fn arc_clone(&self) -> Self { 48 | match self { 49 | UserFmt::System(sys) => UserFmt::System(Arc::clone(sys)), 50 | UserFmt::Function(dyn_fn) => UserFmt::Function(Arc::clone(dyn_fn)), 51 | } 52 | } 53 | fn run_system( 54 | &self, 55 | value: &dyn Reflect, 56 | entry: binding::Entry, 57 | world: &World, 58 | ) -> Result<(), UserWriteError> 59 | where 60 | M: 'static, 61 | { 62 | match self { 63 | UserFmt::System(locked_sys) => locked_sys.try_lock()?.run(value, entry, world), 64 | UserFmt::Function(dyn_fn) => dyn_fn(value, entry), 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | #[derive(Debug, Error)] 71 | pub enum Error { 72 | #[error("Formatter not found: {0:?}")] 73 | NotFormatter(Id), 74 | } 75 | 76 | // TODO(perf): use a `IndexMap` when I get around to implement it. 77 | pub(crate) struct UserFmts(HashMap>); 78 | 79 | impl UserFmts { 80 | fn get(&self, binding: &Id) -> Option> { 81 | self.0.get(binding).map(UserFmt::arc_clone) 82 | } 83 | pub fn new() -> Self { 84 | UserFmts(HashMap::new()) 85 | } 86 | pub fn insert(&mut self, binding: Id, value: UserFmt) -> Option> { 87 | self.0.insert(binding, value) 88 | } 89 | } 90 | 91 | /// Turn a [`&dyn Reflect`] into a [`BevyModify`]. 92 | pub enum Write { 93 | /// Print the [`Reflect`] as a [`BevyModify::set_content`] displayed with the 94 | /// given format specification. 95 | Format(RuntimeFormat), 96 | 97 | /// An arbitrary function to run on the [`Reflect`]. 98 | Arbitrary(UserFmt), 99 | 100 | /// Print the [`Reflect`] as a [`BevyModify::set_content`] displayed with 101 | /// [`Reflect::debug`]. 102 | Debug, 103 | } 104 | impl Write { 105 | pub fn modify(&self, world: &World, value: &dyn Reflect, entry: binding::Entry) { 106 | match self { 107 | Write::Format(fmt) => set_content(entry, &DisplayReflect(value, Some(fmt))), 108 | Write::Arbitrary(run) => run.run_system(value, entry, world).unwrap(), 109 | Write::Debug => set_content(entry, &DisplayReflect(value, None)), 110 | } 111 | } 112 | 113 | pub(crate) fn from_parsed( 114 | format: Option, 115 | provided: &UserFmts, 116 | ) -> Result { 117 | match format { 118 | None => Ok(Write::Debug), 119 | Some(Format::Fmt(format)) => Ok(Write::Format(format)), 120 | Some(Format::UserDefined(binding)) => provided 121 | .get(&binding) 122 | .map(Write::Arbitrary) 123 | .ok_or(Error::NotFormatter(binding)), 124 | } 125 | } 126 | } 127 | fn set_content(entry: binding::Entry, s: &impl fmt::Display) { 128 | entry 129 | .modify(|m| m.set_content(format_args!("{s}"))) 130 | .or_insert_with(|| M::init_content(format_args!("{s}"))); 131 | } 132 | struct DisplayReflect<'a>(&'a dyn Reflect, Option<&'a RuntimeFormat>); 133 | impl fmt::Display for DisplayReflect<'_> { 134 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 135 | if let Some(fmt) = self.1 { 136 | if let Ok(()) = fmt.display(self.0.as_any()).fmt(f) { 137 | return Ok(()); 138 | } 139 | } 140 | self.0.debug(f) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /richtext/src/modifiers.rs: -------------------------------------------------------------------------------- 1 | //! [`Modify`] for richtext. 2 | 3 | #[cfg(feature = "cresustext")] 4 | mod cresus_impl; 5 | #[cfg(feature = "richtext")] 6 | mod rich_impl; 7 | 8 | use std::{any::Any, fmt}; 9 | 10 | use bevy::asset::HandleId; 11 | use bevy::prelude::{Assets, Handle}; 12 | use bevy::text::Font; 13 | use enumset::EnumSet; 14 | use fab_parse::{Deps, Parsable}; 15 | 16 | #[cfg(feature = "cresustext")] 17 | pub use cresus_impl::{Modifier, ModifierField, ModifierItem, ModifierQuery, Sections}; 18 | #[cfg(feature = "richtext")] 19 | pub use rich_impl::{Modifier, ModifierField}; 20 | 21 | /// A Boxed [`TextModify`]. This allows you to extend [`Modifier`] with your 22 | /// own modifiers. 23 | pub type ModifyBox = Box; 24 | 25 | #[derive(Default, Clone, Copy)] 26 | pub struct GetFont<'a>(Option<&'a Assets>); 27 | impl<'a> GetFont<'a> { 28 | pub fn new(assets: &'a Assets) -> Self { 29 | GetFont(Some(assets)) 30 | } 31 | pub fn get(&self, name: &str) -> Option> { 32 | self.0.map(|a| a.get_handle(HandleId::from(name))) 33 | } 34 | } 35 | 36 | impl fmt::Debug for Modifier { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | match self { 39 | Modifier::Font { path } => f.debug_tuple("Font").field(path).finish(), 40 | Modifier::RelSize { relative_size } => { 41 | f.debug_tuple("Size^").field(relative_size).finish() 42 | } 43 | Modifier::FontSize { size } => f.debug_tuple("FontSize").field(size).finish(), 44 | Modifier::Color { statik } => f.debug_tuple("Color").field(statik).finish(), 45 | Modifier::HueOffset { offset } => f.debug_tuple("Hue>").field(offset).finish(), 46 | Modifier::Content { statik } => f.debug_tuple("Text").field(statik).finish(), 47 | Modifier::Dynamic { boxed, .. } => write!(f, "{boxed:?}"), 48 | } 49 | } 50 | } 51 | impl Parsable for Modifier { 52 | type Err = anyhow::Error; 53 | 54 | /// Returns the (depends, changes) field set of modifier named `name`. 55 | fn dependencies_of(name: &str) -> Deps { 56 | let mut depends = EnumSet::EMPTY; 57 | 58 | let changes = match name { 59 | "Font" => Modifier::font_changes(), 60 | "Color" => Modifier::color_changes(), 61 | "Content" => Modifier::content_changes(), 62 | "FontSize" => Modifier::font_size_changes(), 63 | "RelSize" => { 64 | depends = Modifier::rel_size_depends(); 65 | Modifier::rel_size_changes() 66 | } 67 | "HueOffset" => { 68 | depends = Modifier::hue_offset_depends(); 69 | Modifier::hue_offset_changes() 70 | } 71 | _ => return Deps::NoneWithName, 72 | }; 73 | Deps::Some { changes, depends } 74 | } 75 | 76 | fn parse(name: &str, input: &str) -> Result { 77 | match name { 78 | "Font" => Ok(Modifier::font(input.to_string().into())), 79 | "FontSize" => Ok(Modifier::font_size(input.parse()?)), 80 | "RelSize" => Ok(Modifier::rel_size(input.parse()?)), 81 | "Color" => Ok(Modifier::color(crate::color::parse(input)?)), 82 | "HueOffset" => Ok(Modifier::hue_offset(input.parse()?)), 83 | "Content" => Ok(Modifier::content(input.to_string().into())), 84 | // TODO(err): nice struct instead of anyhow 85 | n => Err(anyhow::anyhow!(format!("{n} is not a parseable modifier"))), 86 | } 87 | } 88 | } 89 | 90 | impl From for Modifier { 91 | fn from(value: String) -> Self { 92 | Modifier::content(value.into()) 93 | } 94 | } 95 | impl From for Modifier { 96 | fn from(value: T) -> Self { 97 | Modifier::Dynamic { 98 | depends: value.depends(), 99 | changes: value.changes(), 100 | boxed: Box::new(value), 101 | } 102 | } 103 | } 104 | 105 | pub trait TextModify { 106 | #[cfg(feature = "richtext")] 107 | fn apply(&self, ctx: &GetFont, section: &mut bevy::text::TextSection); 108 | #[cfg(feature = "cresustext")] 109 | fn apply(&self, ctx: &GetFont, item: ModifierItem); 110 | fn depends(&self) -> EnumSet; 111 | fn changes(&self) -> EnumSet; 112 | 113 | fn as_any(&self) -> &dyn Any; 114 | fn eq_dyn(&self, other: &dyn TextModify) -> bool; 115 | fn debug_dyn(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; 116 | fn clone_dyn(&self) -> ModifyBox; 117 | } 118 | impl Clone for ModifyBox { 119 | fn clone(&self) -> Self { 120 | self.clone_dyn() 121 | } 122 | } 123 | impl PartialEq for ModifyBox { 124 | fn eq(&self, other: &Self) -> bool { 125 | self.eq_dyn(&**other) 126 | } 127 | } 128 | impl PartialEq<&Self> for ModifyBox { 129 | fn eq(&self, other: &&Self) -> bool { 130 | self.eq_dyn(&***other) 131 | } 132 | } 133 | impl fmt::Debug for ModifyBox { 134 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 135 | self.debug_dyn(f) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /design_doc/richtext/cresustext.md: -------------------------------------------------------------------------------- 1 | # Cresus text design 2 | 3 | Cresus text is rich text that works on entities instead of mere text sections. 4 | 5 | The items are not element of a `Vec` but `Query` items. 6 | 7 | This can't just be plugged on top of existing `Modifier` trait for a few reasons: 8 | 9 | 1. `Modifier::Item` can't be `Clone`, `Send` or `Sync` anymore. Why are those trait 10 | necessary currently? 11 | - In `resolve/make.rs`, to create the initial sections. 12 | - This could be replaced by a `MakeItem`? 13 | 2. By virtue of being query items, `Item` must admit a lifetime. 14 | - Currently needs to be `'static` due to storing default value in `MakeRichtext` 15 | component. This is also why `Send` and `Sync` 16 | 17 | ## Goal 18 | 19 | Have `Items` be: 20 | 21 | ```rust 22 | struct Param<'w, 's, Ctx: SystemParam, It: WorldQuery> { 23 | context: Ctx, 24 | query: Query<'w, 's, It> 25 | } 26 | impl<'w, 's, Ctx: SystemParam, It: WorldQuery> Param<'w, 's, Ctx, It> { 27 | fn split<'a>(&'a mut self, children: &'a Children) -> (Items<'a, 'w, 's, It>, Ctx<'a>) { 28 | let Self { context, query } = self; 29 | (Items { children, query }, context) 30 | } 31 | } 32 | struct Items<'a, 'w, 's, It: WorldQuery> { 33 | children: &'a Children, 34 | query: &'a mut Query<'w, 's, It> 35 | } 36 | impl<'a, 'w, 's, It: WorldQuery, M> Indexed for Items<'a, 'w, 's, It> 37 | where M: for<'b> Modify = It::Item<'b>> 38 | { 39 | fn get_mut<'b>(&'b mut self, index: usize) -> Option> { 40 | let entity = self.children.get(index)?; 41 | query.get_mut(entity).ok() 42 | } 43 | } 44 | impl Modify { 45 | type Items = Items<'a, 'w, 's, QueryItem>; 46 | type Item: QueryItem< 47 | } 48 | ``` 49 | 50 | Have `Item: WorldQuery`. 51 | 52 | What if 53 | 54 | ```rust 55 | trait Indexed { 56 | fn change(&mut self, index: usize, f: impl FnOnce(&mut M::Item)) 57 | } 58 | ``` 59 | 60 | ## What data? 61 | 62 | - `M::Items`: 63 | - `Children` comp 64 | - `Query`: Get individual items 65 | 66 | ## What am I struggling with? 67 | 68 | It's 69 | 70 | - turning `MakeItem` into `Item` 71 | - Passing the QueryItem to `apply` 72 | 73 | `MakeItem` 2 `Item` hard because we have two representations 74 | 75 | - Owned (that can be stored and used as "root" when root updated) 76 | - By ref (that can be queryed from the world) 77 | 78 | The QueryItem is a `(Mut, &Y, Option>)` while the owned thing is 79 | `(X, Y, Option)`. 80 | 81 | Is owned needed? 82 | 83 | - We use it to initialize the format string, it's pretty fundamental. 84 | - We use `apply` on this, which requires a `Item`, but since `MakeItem` is 85 | not `Item`, we try to work around this with `MakeItem: AsMut`, but 86 | it's still a problem, since ref version in `WorldQuery` is not a `&mut`, so 87 | we work around this by saying `&'a mut MakeItem: Into>`, but then the 88 | trait bounds are so ridiculous it's impratical, it's also fairly error prone, 89 | since the goal is to store the mutable references into the target item, so that 90 | it's possible to update them transparently. 91 | - Also for root updates 92 | 93 | So we need a `MakeItem` that: 94 | 95 | - Can be written to _as_ a `Item` (so that we can `apply` on it) 96 | - Can take an `Item` and update its value. 97 | 98 | Why not have a `make_default(item: &mut Item)`? Because the default changes per 99 | instances. 100 | 101 | Could be a method on a `MakeItem` trait? 102 | 103 | ## This is ridiculous! 104 | 105 | New difficulty: Trying to get the old-style system working with the new one. 106 | 107 | I'm getting close to getting something working, but it basically looks like a 108 | giant hack that only exists to make `update_items_system` work with two completey 109 | different things. 110 | 111 | - `Richtext`: wants `Modify::Items` to be a component (`Text`), and `Modify::Item` an element 112 | of this component (`TextSection`). 113 | `update_items_system` iterates over all entities with a 114 | `LocalBindings` and `Modify::Items` component, and updates `Modify::Items` based 115 | on `LocalBindings` 116 | - `Cresustext`: `Modify::Items` is a `Query` + `WorldQuery` (`&Children`), 117 | `Modify::Item` is return item of the `Query` in `Modify::Items` for each child. 118 | 119 | System in question that causes soooo much trouble is: 120 | 121 | ```rust 122 | let (context, mut items) = BM::context(params.into_inner()); 123 | for (mut local_data, wq_item) in &mut query { 124 | BM::set_local_items(&mut items, wq_item); 125 | local_data.update(&mut items, &world_bindings, &context); 126 | } 127 | world_bindings.bindings.reset_changes(); 128 | ``` 129 | 130 | You notice `BM::context` and `BM::set_local_items` only exist for this system. 131 | If we left definition of the system to implementor, we don't need to infect 132 | the trait with so much nonsense. 133 | 134 | In fact, this is also true of types `BM::Wq` and `BM::Param`! 135 | 136 | Can't define system as an associated method or constant of `BevyModify`. 137 | We _could_ just leave the responsability of adding an update system to the 138 | end-user. But that seems error-prone. 139 | 140 | Instead, we'll add a required method called `add_update_system`. Which will 141 | force the implementor to be careful to add the method in question. 142 | 143 | We'll provide specialized systems, so that the user can directly use them 144 | in `add_update_system`. 145 | 146 | -------------------------------------------------------------------------------- /fab/src/modify.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[cfg(doc)] 4 | use crate::resolve::DepsResolver; 5 | use crate::resolve::Resolver; 6 | 7 | use enumset::{EnumSet, EnumSetType}; 8 | 9 | /// An owned view of `I` that can be read as or written to a mutably borrowed `I`. 10 | pub trait MakeItem<'a, I: 'a> { 11 | /// Write `Self` into a pre-existing `I`. 12 | /// 13 | /// This is used by [`DepsResolver`] when the root item changes. 14 | fn make_item<'b, 'c>(&'b self, item: &'c mut I) 15 | where 16 | 'a: 'c; 17 | 18 | /// Turn `Self` into `I` so that it can be written to. 19 | /// 20 | /// Used by [`Resolver`] for the initial build of the format string. 21 | /// 22 | /// Otherwise, [`Resolver`] couldn't run [`Modify::apply`] on the list of 23 | /// [`Modify::MakeItem`] to create this initial list. 24 | fn as_item(&'a mut self) -> I; 25 | } 26 | impl<'a, T: Clone> MakeItem<'a, &'a mut T> for T { 27 | fn make_item<'b, 'c>(&'b self, item: &'c mut &'a mut T) 28 | where 29 | 'a: 'c, 30 | { 31 | **item = self.clone(); 32 | } 33 | fn as_item(&mut self) -> &mut T { 34 | self 35 | } 36 | } 37 | impl<'a, T1: Clone, T2: Clone> MakeItem<'a, (&'a mut T1, &'a mut T2)> for (T1, T2) { 38 | fn make_item<'b, 'c>(&self, item: &'c mut (&'a mut T1, &'a mut T2)) 39 | where 40 | 'a: 'c, 41 | { 42 | *item.0 = self.0.clone(); 43 | *item.1 = self.1.clone(); 44 | } 45 | fn as_item(&mut self) -> (&mut T1, &mut T2) { 46 | let (t1, t2) = self; 47 | (t1, t2) 48 | } 49 | } 50 | 51 | pub trait Indexed { 52 | fn get_mut(&mut self, index: usize) -> Option>; 53 | } 54 | impl Modify = &'a mut T>> Indexed for [T] { 55 | fn get_mut(&mut self, index: usize) -> Option> { 56 | <[T]>::get_mut(self, index) 57 | } 58 | } 59 | impl Modify = &'a mut T>> Indexed for Vec { 60 | fn get_mut(&mut self, index: usize) -> Option> { 61 | <[T]>::get_mut(self, index) 62 | } 63 | } 64 | 65 | /// Several [`Modify::Field`]s. 66 | pub type FieldsOf = EnumSet<::Field>; 67 | 68 | /// A set of operations on `Item`. 69 | /// 70 | /// A `Modify` value declares which fields of `Modify::Item` it will [read] and [update]. 71 | /// [`Modify::apply`] takes an `Item` and updates its. 72 | /// This allows fine grained update propagation in [`Resolver`]. 73 | /// 74 | /// `cuicui_fab` provides the [`impl_modify!`] macro to define [`Modify`] 75 | /// more concisely and with less footguns. 76 | /// 77 | /// [read]: Modify::depends 78 | /// [update]: Modify::changes 79 | /// [`impl_modify!`]: crate::impl_modify 80 | pub trait Modify: Clone + fmt::Debug { 81 | type MakeItem: for<'a> MakeItem<'a, Self::Item<'a>> + Clone + fmt::Debug + Send + Sync; 82 | 83 | /// The type on which `Modify` operates 84 | type Item<'a>; 85 | 86 | /// The underlying [`Self::Item`] storage. 87 | type Items<'a, 'b, 'c>: Indexed + Send + Sync 88 | where 89 | Self: 'a; 90 | 91 | /// The [set](EnumSet) of fields that `Self` accesses on `Item`. 92 | type Field: EnumSetType + fmt::Debug + Send + Sync; 93 | 94 | // TODO(perf): Change detection on context as well. 95 | /// An additional context **outside of `Item`** that is relevant to operations on `Item`. 96 | type Context<'a> 97 | where 98 | Self: 'a; 99 | 100 | /// The [`Resolver`] used for this `Modify`. 101 | type Resolver: Resolver + fmt::Debug + Send + Sync; 102 | 103 | /// Apply this modifier to the [`Self::Item`]. 104 | /// 105 | /// It is important that `apply`: 106 | /// 107 | /// - only reads [`Self::Field`]s returned by [`Self::depends`]. 108 | /// - only updates [`Self::Field`]s returned by [`Self::changes`]. 109 | /// 110 | /// Otherwise, [`Resolver`] will fail to work properly. 111 | fn apply(&self, ctx: &Self::Context<'_>, item: Self::Item<'_>) -> anyhow::Result<()>; 112 | 113 | /// On what data in [`Self::Item`] does this modifier depends? 114 | fn depends(&self) -> EnumSet; 115 | 116 | /// What data in [`Self::Item`] does this `Modify` changes? 117 | fn changes(&self) -> EnumSet; 118 | } 119 | 120 | /// Holds a [`Modify::Item`] and keeps track of changes to it. 121 | /// 122 | /// You need to use [`Changing::update`] to access the `Item`, this will keep 123 | /// track of the updated fields. 124 | /// 125 | /// To reset the field update tracking, use [`Changing::reset_updated`]. 126 | pub struct Changing { 127 | pub(crate) updated: EnumSet, 128 | pub(crate) value: T, 129 | } 130 | impl Changing { 131 | /// Store this `value` in a `Changing`, with no updated field. 132 | pub fn new(value: T) -> Self { 133 | Self { updated: EnumSet::EMPTY, value } 134 | } 135 | /// Update `self` with `f`, declaring that `update` is changed. 136 | /// 137 | /// If you change fields other than the ones in `updated`, they won't be 138 | /// tracked as changed. So make sure to properly declare which fields 139 | /// you are changing. 140 | pub fn update(&mut self, updated: F, f: impl FnOnce(&mut T)) { 141 | self.updated |= updated; 142 | f(&mut self.value); 143 | } 144 | /// Reset the change tracker state. 145 | pub fn reset_updated(&mut self) { 146 | self.updated = EnumSet::EMPTY; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /bevy_fab/src/make.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, mem}; 2 | 3 | use bevy::ecs::{prelude::*, system::SystemState}; 4 | use fab::{modify::FieldsOf, resolve::Resolver}; 5 | use fab_parse::hook as parse; 6 | use log::error; 7 | 8 | #[cfg(doc)] 9 | use crate::world; 10 | use crate::{BevyModify, LocalBindings, Styles, WorldBindings}; 11 | 12 | #[derive(Component)] 13 | pub struct ParseFormatString { 14 | pub format_string: String, 15 | pub default_item: BM::MakeItem, 16 | pub items_extra: Option, 17 | _p: PhantomData, 18 | } 19 | impl ParseFormatString { 20 | pub fn new( 21 | format_string: String, 22 | default_item: BM::MakeItem, 23 | items_extra: BM::ItemsCtorData, 24 | ) -> Self { 25 | let _p = PhantomData; 26 | let items_extra = Some(items_extra); 27 | Self { format_string, default_item, items_extra, _p } 28 | } 29 | /// Drain all fields from a `&mut Self` to get an owned value. 30 | fn take(&mut self) -> (BM::ItemsCtorData, BM::MakeItem, String) { 31 | ( 32 | self.items_extra.take().unwrap(), 33 | self.default_item.clone(), 34 | mem::take(&mut self.format_string), 35 | ) 36 | } 37 | } 38 | 39 | /// Create a [`Resolver`] by parsing `format_string`. 40 | /// 41 | /// Effects: 42 | /// 43 | /// - Returns `Vec`: The list of items the [`Resolver`] will work on. 44 | /// - Returns `BM::Resolver`: The resolver containing the parsed [`BevyModify`]. 45 | /// - Returns `Vec>`: The parsed but not created [`world::Hook`]s 46 | /// used in the format string. It has the lifetime of `format_string`. 47 | /// - Interns in [`WorldBindings`] bindings found in `format_string`. 48 | /// 49 | /// [`Resolver`]: fab::resolve::Resolver 50 | fn mk<'fstr, BM: BevyModify>( 51 | bindings: &mut WorldBindings, 52 | style: &mut Styles, 53 | default_item: &BM::MakeItem, 54 | context: &BM::Context<'_>, 55 | format_string: &'fstr str, 56 | ) -> anyhow::Result<(Vec, BM::Resolver, Vec>)> { 57 | let mut new_hooks = Vec::new(); 58 | 59 | let tree = fab_parse::format_string(format_string)?; 60 | let tree = style.process(tree.transform()); 61 | let parsed = tree.finish(&mut bindings.bindings, &mut new_hooks); 62 | let parsed: Vec<_> = parsed.into_iter().collect::>()?; 63 | 64 | let (resolver, items) = BM::Resolver::new(parsed, || default_item.clone(), context); 65 | 66 | Ok((items, resolver, new_hooks)) 67 | } 68 | 69 | /// Replaces [`ParseFormatString`] with [`LocalBindings`], 70 | /// updating [`WorldBindings`]. 71 | /// 72 | /// This is an exclusive system, as it requires access to the [`World`] to generate 73 | /// the [`world::Hook`]s specified in the format string. 74 | pub fn parse_into_resolver_system( 75 | world: &mut World, 76 | mut to_make: Local)>>, 77 | mut cache: Local< 78 | SystemState<( 79 | Commands, 80 | ResMut>, 81 | ResMut>, 82 | BM::Param, 83 | )>, 84 | >, 85 | ) where 86 | FieldsOf: Sync + Send, 87 | { 88 | // The `format_string` are field of `ParseFormatString`, components of the ECS. 89 | // we use `ParseFormatString::take` to extract them from the ECS, and own them 90 | // in this system in `to_make`. 91 | let to_make: Vec<_> = to_make 92 | .iter_mut(world) 93 | .map(|(e, mut r)| (e, r.take())) 94 | .collect(); 95 | 96 | if to_make.is_empty() { 97 | return; 98 | } 99 | 100 | // The `parse::Hook`s returned by `mk` 101 | // have a lifetime dependent on the `format_string` used. 102 | // 103 | // parse::Hook's reference here points to String within MakeRichText in 104 | // the `to_make` variable. 105 | let mut new_hooks: Vec<_> = Vec::new(); 106 | 107 | // Furthermore, `richtext::mk` needs mutable access to WorldBindings and 108 | // immutable to the context, so we use the SystemState to extract them. 109 | { 110 | let (mut cmds, mut styles, mut world_bindings, params) = cache.get_mut(world); 111 | 112 | let context = BM::context(¶ms); 113 | 114 | // TODO(perf): batch commands update. 115 | for (entity, (ctor_data, item, fmt)) in to_make.iter() { 116 | match mk(&mut world_bindings, &mut styles, item, &context, fmt) { 117 | Ok((items, resolver, mut hooks)) => { 118 | new_hooks.append(&mut hooks); 119 | 120 | let mut cmds = cmds.entity(*entity); 121 | cmds.insert(LocalBindings::::new(resolver, item.clone())); 122 | BM::spawn_items(ctor_data, items, &mut cmds); 123 | } 124 | Err(err) => { 125 | error!("Error '{err}' when building '''{fmt}'''") 126 | } 127 | } 128 | cmds.entity(*entity).remove::>(); 129 | } 130 | } 131 | cache.apply(world); 132 | 133 | // To convert the parse::Hook into an actual world::Hook that goes into world::Hooks, 134 | // we need excluisve world access. 135 | world.resource_scope(|world, mut bindings: Mut>| { 136 | let parse_hook = |&hook| bindings.parse_hook(hook, world); 137 | new_hooks.iter().for_each(parse_hook); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /reflect_query/src/predefined.rs: -------------------------------------------------------------------------------- 1 | //! Add `ReflectQueryable` to the app for all pre-existing bevy components. 2 | 3 | use bevy::prelude::{App, Plugin}; 4 | 5 | use crate::ReflectQueryable; 6 | 7 | macro_rules! register_reflect_query { 8 | (@just_type $registry:expr, $( $to_register:ty ),* $(,)?) => { 9 | $( $registry.register_type::<$to_register>() );* 10 | }; 11 | ($registry:expr, $( $to_register:ty ),* $(,)?) => { 12 | $( $registry.register_type_data::<$to_register, ReflectQueryable>() );* 13 | }; 14 | } 15 | 16 | /// Add [`ReflectQueryable`] registration for all base bevy components. _All_. 17 | pub struct QueryablePlugin; 18 | impl Plugin for QueryablePlugin { 19 | fn build(&self, app: &mut App) { 20 | add_all_reflect_query(app); 21 | } 22 | } 23 | 24 | // Allow: There is basically one item per line in this function, the line count 25 | // is not a symptom of complexity. 26 | #[allow(clippy::too_many_lines)] 27 | fn add_all_reflect_query(app: &mut App) { 28 | { 29 | use bevy::prelude::{Children, GlobalTransform, Name, Parent, Transform, Window}; 30 | use bevy::window::PrimaryWindow; 31 | 32 | register_reflect_query!(@just_type app, PrimaryWindow); 33 | register_reflect_query![ 34 | app, 35 | Children, 36 | GlobalTransform, 37 | Name, 38 | Parent, 39 | PrimaryWindow, 40 | Transform, 41 | Window, 42 | ]; 43 | } 44 | #[cfg(feature = "register_core_pipeline")] 45 | { 46 | use bevy::core_pipeline::{ 47 | bloom::BloomSettings, core_2d::Camera2d, core_3d::Camera3d, fxaa::Fxaa, 48 | prepass::DepthPrepass, prepass::NormalPrepass, tonemapping::DebandDither, 49 | tonemapping::Tonemapping, 50 | }; 51 | register_reflect_query!(@just_type app, Fxaa); 52 | register_reflect_query![ 53 | app, 54 | BloomSettings, 55 | Camera2d, 56 | Camera3d, 57 | DebandDither, 58 | DepthPrepass, 59 | Fxaa, 60 | NormalPrepass, 61 | Tonemapping 62 | ]; 63 | } 64 | #[cfg(feature = "register_pbr")] 65 | { 66 | use bevy::pbr::{ 67 | CascadeShadowConfig, Cascades, CascadesVisibleEntities, ClusterConfig, 68 | CubemapVisibleEntities, DirectionalLight, EnvironmentMapLight, FogSettings, 69 | NotShadowCaster, NotShadowReceiver, PointLight, SpotLight, 70 | }; 71 | register_reflect_query!(@just_type app, FogSettings, NotShadowCaster, NotShadowReceiver); 72 | register_reflect_query![ 73 | app, 74 | Cascades, 75 | CascadeShadowConfig, 76 | CascadesVisibleEntities, 77 | ClusterConfig, 78 | CubemapVisibleEntities, 79 | DirectionalLight, 80 | EnvironmentMapLight, 81 | FogSettings, 82 | NotShadowCaster, 83 | NotShadowReceiver, 84 | PointLight, 85 | SpotLight, 86 | ]; 87 | } 88 | #[cfg(feature = "register_sprite")] 89 | { 90 | use bevy::sprite::{Anchor, Mesh2dHandle, Sprite, TextureAtlasSprite}; 91 | register_reflect_query!(@just_type app, TextureAtlasSprite); 92 | register_reflect_query![app, Anchor, Mesh2dHandle, Sprite, TextureAtlasSprite]; 93 | } 94 | #[cfg(feature = "register_render")] 95 | { 96 | use bevy::render::{ 97 | camera::CameraRenderGraph, 98 | mesh::skinning::SkinnedMesh, 99 | prelude::{ 100 | Camera, ComputedVisibility, OrthographicProjection, PerspectiveProjection, 101 | Projection, Visibility, 102 | }, 103 | primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum}, 104 | view::{ColorGrading, RenderLayers, VisibleEntities}, 105 | }; 106 | register_reflect_query![ 107 | app, 108 | Aabb, 109 | Camera, 110 | CameraRenderGraph, 111 | CascadesFrusta, 112 | ColorGrading, 113 | ComputedVisibility, 114 | CubemapFrusta, 115 | Frustum, 116 | OrthographicProjection, 117 | PerspectiveProjection, 118 | Projection, 119 | RenderLayers, 120 | SkinnedMesh, 121 | Visibility, 122 | VisibleEntities, 123 | ]; 124 | } 125 | #[cfg(feature = "register_ui")] 126 | { 127 | use bevy::ui::{ 128 | prelude::{Button, CalculatedClip, CalculatedSize, Label, Node, Style, UiImage}, 129 | BackgroundColor, FocusPolicy, Interaction, RelativeCursorPosition, ZIndex, 130 | }; 131 | register_reflect_query!(@just_type app, CalculatedClip, RelativeCursorPosition, ZIndex); 132 | register_reflect_query![ 133 | app, 134 | BackgroundColor, 135 | Button, 136 | CalculatedClip, 137 | CalculatedSize, 138 | FocusPolicy, 139 | Interaction, 140 | Label, 141 | Node, 142 | RelativeCursorPosition, 143 | Style, 144 | UiImage, 145 | ZIndex 146 | ]; 147 | } 148 | #[cfg(feature = "register_text")] 149 | { 150 | use bevy::text::{Text, Text2dBounds}; 151 | register_reflect_query!(@just_type app, Text2dBounds); 152 | register_reflect_query![app, Text2dBounds, Text]; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /datazoo/src/jagged_vec.rs: -------------------------------------------------------------------------------- 1 | //! An extensible (ie: can add more rows) [jagged array]. 2 | //! 3 | //! [jagged array]: https://en.wikipedia.org/wiki/Jagged_array 4 | 5 | use std::mem::size_of; 6 | 7 | use thiserror::Error; 8 | 9 | /// [`JaggedVec::new`] construction error. 10 | #[derive(Debug, Error)] 11 | pub enum Error { 12 | /// An `end` in `ends` was lower than a previous one. 13 | #[error( 14 | "Cannot build JaggedVec: `ends` represents the end of each row in `data`, \ 15 | it should be monotonically increasing. \ 16 | Found `end` at position {i} lower than `end` at position {}", .i - 1 17 | )] 18 | BadEnd { i: usize }, 19 | /// An `end` in `ends` was too large. 20 | #[error( 21 | "Cannot build JaggedVec: `ends` represents the end of each row in `data`, \ 22 | Yet, `end` at position {i} ({end}) is larger than the length of data ({len})" 23 | )] 24 | TooLongEnd { i: usize, len: u32, end: u32 }, 25 | } 26 | 27 | /// An extensible (ie: can add more rows) [jagged array]. 28 | #[derive(Debug, PartialEq, Eq, Clone)] 29 | pub struct JaggedVec { 30 | ends: Vec, 31 | data: Vec, 32 | } 33 | impl JaggedVec { 34 | pub fn push_row(&mut self, row: impl IntoIterator) { 35 | self.ends.push(self.data.len() as u32); 36 | self.data.extend(row); 37 | } 38 | /// How many cells are contained in this `JaggedVec`. 39 | pub fn len(&self) -> usize { 40 | self.data.len() 41 | } 42 | /// Is this vector empty (no cells, may have several empty rows). 43 | pub fn is_empty(&self) -> bool { 44 | self.data.is_empty() 45 | } 46 | /// How many rows this `JaggedVec` has. 47 | pub fn height(&self) -> usize { 48 | self.ends.len() + 1 49 | } 50 | /// Create a [`JaggedVec`] of `ends.len() + 1` rows, values of `ends` are the 51 | /// end indicies (exclusive) of each row in `data`. 52 | /// 53 | /// Note that the _last index_ should be elided. 54 | /// The last row will be the values between the last `end` in `ends` and 55 | /// the total size of the `data` array. 56 | /// 57 | /// Returns `Err` if: 58 | /// 59 | /// - An `ends[i] > data.len()` 60 | /// - An `ends[i+1] < ends[i]` 61 | /// 62 | /// # Example 63 | /// 64 | /// ```rust 65 | /// use cuicui_datazoo::JaggedVec; 66 | /// 67 | /// let ends = [0, 0, 3, 4, 7, 9, 10, 10]; // len = 8 68 | /// let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 23]; 69 | /// let jagged = JaggedVec::new(ends.to_vec(), data.to_vec()).unwrap(); 70 | /// let iliffe = jagged.into_vecs(); 71 | /// assert_eq!( 72 | /// iliffe, 73 | /// vec![ 74 | /// vec![], 75 | /// vec![], 76 | /// vec![0, 1, 2], 77 | /// vec![3], 78 | /// vec![4, 5, 6], 79 | /// vec![7, 8], 80 | /// vec![9], 81 | /// vec![], 82 | /// vec![11, 23], 83 | /// ], // len = 9 84 | /// ); 85 | /// ``` 86 | pub fn new(ends: Vec, data: Vec) -> Result { 87 | assert!(size_of::() >= size_of::()); 88 | 89 | let mut previous_end = 0; 90 | let last_end = data.len() as u32; 91 | for (i, end) in ends.iter().enumerate() { 92 | if *end > last_end { 93 | return Err(Error::TooLongEnd { i, len: last_end, end: *end }); 94 | } 95 | if *end < previous_end { 96 | return Err(Error::BadEnd { i }); 97 | } 98 | previous_end = *end; 99 | } 100 | Ok(Self { ends, data }) 101 | } 102 | /// Get slice to row at given `index`. 103 | /// 104 | /// # Panics 105 | /// 106 | /// When `index > self.height()` 107 | /// 108 | /// # Example 109 | /// 110 | /// ```rust 111 | /// use cuicui_datazoo::JaggedVec; 112 | /// 113 | /// let ends = [0, 0, 3, 4, 7, 9, 10, 10]; 114 | /// let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 115 | /// let jagged = JaggedVec::new(ends.to_vec(), data.to_vec()).unwrap(); 116 | /// 117 | /// assert_eq!(jagged.row(4), &[4, 5, 6]); 118 | /// ``` 119 | #[inline] 120 | pub fn row(&self, index: usize) -> &[T] { 121 | assert!(index <= self.ends.len()); 122 | // TODO(perf): verify generated code elides bound checks. 123 | let get_end = |end: &u32| *end as usize; 124 | 125 | let start = index.checked_sub(1).map_or(0, |i| self.ends[i]) as usize; 126 | let end = self.ends.get(index).map_or(self.data.len(), get_end); 127 | &self.data[start..end] 128 | } 129 | /// Get `V` at exact `direct_index` ignoring row sizes, 130 | /// acts as if the whole array was a single row. 131 | /// 132 | /// `None` when `direct_index` is out of bound. 133 | /// 134 | /// # Example 135 | /// 136 | /// ```rust 137 | /// use cuicui_datazoo::JaggedVec; 138 | /// 139 | /// let ends = [0, 0, 3, 4, 7, 9, 10, 10]; 140 | /// let data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 141 | /// let jagged = JaggedVec::new(ends.to_vec(), data.to_vec()).unwrap(); 142 | /// 143 | /// assert_eq!(jagged.get(4), Some(&4)); 144 | /// ``` 145 | #[inline] 146 | pub fn get(&self, direct_index: usize) -> Option<&T> { 147 | self.data.get(direct_index) 148 | } 149 | /// Turn this compact jagged array into a sparse representation. 150 | /// 151 | /// The returned `Vec>` is an [Iliffe vector]. Iterating over it will 152 | /// be much slower than iterating over `JaggedVec`, but extending individual 153 | /// rows is much less costly. 154 | /// 155 | /// [Iliffe vector]: https://en.wikipedia.org/wiki/Iliffe_vector 156 | pub fn into_vecs(self) -> Vec> { 157 | let Self { ends, mut data } = self; 158 | 159 | let mut iliffe = Vec::with_capacity(ends.len() + 1); 160 | let mut last_end = 0; 161 | 162 | // TODO(perf): this is slow as heck because each drain needs to move 163 | // forward the end of the `data` vec, if we reverse ends here, we can 164 | // skip the nonsense. 165 | for end in ends.into_iter() { 166 | let size = (end - last_end) as usize; 167 | iliffe.push(data.drain(..size).collect()); 168 | last_end = end; 169 | } 170 | // the last row. 171 | iliffe.push(data); 172 | iliffe 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /datazoo/src/bitmatrix.rs: -------------------------------------------------------------------------------- 1 | //! A [bitset](Bitset) with fixed-size rows. 2 | 3 | use std::{fmt, mem}; 4 | 5 | use crate::{div_ceil, Bitset}; 6 | 7 | /// A [bitset](Bitset) with fixed-size rows. 8 | /// 9 | /// Note that only the total size is tracked in `BitMatrix` and you must provide 10 | /// the `width` value when calling methods on `BitMatrix`. 11 | #[derive(Debug, Clone)] 12 | pub struct BitMatrix(Bitset>); 13 | impl BitMatrix { 14 | /// The height this matrix would have if it had given `width`. 15 | /// 16 | /// Note that this might be greater than the `height` given to [`Self::new_with_size`] 17 | /// due to `BitMatrix` discarding information about actual size. 18 | /// 19 | /// # Panics 20 | /// If `Self` is not empty **and** `width` equals `0` (division by zero) 21 | #[inline] 22 | pub fn height(&self, width: usize) -> usize { 23 | match self.0.bit_len() { 24 | 0 => 0, 25 | total => total / width, 26 | } 27 | } 28 | /// Iterate over active bits in given `column`. 29 | /// 30 | /// # Panics 31 | /// 32 | /// When `width = 0` (this would otherwise mean there is an infinite 33 | /// amount of columns) 34 | #[inline] 35 | pub fn active_rows_in_column(&self, width: usize, x: usize) -> Column { 36 | assert_ne!(width, 0); 37 | Column { data: &self.0 .0, width, current_cell: x } 38 | } 39 | pub fn row(&self, width: usize, y: usize) -> impl Iterator + '_ { 40 | let start = y * width; 41 | let end = (y + 1) * width; 42 | 43 | self.0 44 | .ones_in_range(start..end) 45 | .map(move |i| (i as usize) - start) 46 | } 47 | /// Enables bit at position `bit`. 48 | /// 49 | /// Returns `None` and does nothing if `bit` is out of range. 50 | /// 51 | /// When [`Bitset::bit(bit)`] will be called next, it will be `true` 52 | /// if this returned `Some`. 53 | #[inline] 54 | pub fn enable_bit(&mut self, width: usize, x: usize, y: usize) -> Option<()> { 55 | if width == 0 { 56 | return Some(()); 57 | } 58 | self.0.enable_bit(width * y + x) 59 | } 60 | /// Create a [`BitMatrix`] with given proportions. 61 | /// 62 | /// Note that the total size is the lowest multiple of 32 higher or equal to `width * height`. 63 | #[must_use] 64 | pub fn new_with_size(width: usize, height: usize) -> Self { 65 | let bit_size = width * height; 66 | let u32_size = div_ceil(bit_size, mem::size_of::()); 67 | BitMatrix(Bitset(vec![0; u32_size].into_boxed_slice())) 68 | } 69 | 70 | /// `true` if bit at position `x, y` in matrix is enabled. 71 | /// 72 | /// `false` otherwise, included if `x, y` is outside of the matrix. 73 | pub fn bit(&self, width: usize, x: usize, y: usize) -> bool { 74 | x < width && self.0.bit(x + y * width) 75 | } 76 | 77 | /// Return a struct that, when printed with [`fmt::Display`] or [`fmt::Debug`], 78 | /// displays the matrix using unicode sextant characters([pdf]). 79 | /// 80 | /// [pdf]: https://unicode.org/charts/PDF/U1FB00.pdf 81 | pub const fn sextant_display(&self, width: usize, height: usize) -> SextantDisplay { 82 | SextantDisplay { matrix: self, width, height } 83 | } 84 | } 85 | 86 | /// Iterator over a single column of a [`BitMatrix`], 87 | /// see [`BitMatrix::active_rows_in_column`] documentation for details. 88 | pub struct Column<'a> { 89 | width: usize, 90 | current_cell: usize, 91 | data: &'a [u32], 92 | } 93 | impl Iterator for Column<'_> { 94 | type Item = usize; 95 | 96 | fn next(&mut self) -> Option { 97 | loop { 98 | let bit = self.current_cell; 99 | let row = self.current_cell / self.width; 100 | self.current_cell += self.width; 101 | 102 | let block = bit / u32::BITS as usize; 103 | let offset = bit % u32::BITS as usize; 104 | 105 | let is_active = |block: u32| block & (1 << offset) != 0; 106 | match self.data.get(block) { 107 | Some(block) if is_active(*block) => return Some(row), 108 | Some(_) => continue, 109 | None => return None, 110 | } 111 | } 112 | } 113 | #[inline] 114 | fn size_hint(&self) -> (usize, Option) { 115 | let upper = self.data.len().saturating_sub(self.current_cell) / self.width; 116 | (0, Some(upper)) 117 | } 118 | #[inline] 119 | fn nth(&mut self, n: usize) -> Option { 120 | self.current_cell = self.current_cell.saturating_add(n * self.width); 121 | self.next() 122 | } 123 | } 124 | 125 | /// Nice printing for [`BitMatrix`], see [`BitMatrix::sextant_display`] for details. 126 | #[derive(Copy, Clone)] 127 | pub struct SextantDisplay<'a> { 128 | matrix: &'a BitMatrix, 129 | width: usize, 130 | height: usize, 131 | } 132 | impl<'a> fmt::Debug for SextantDisplay<'a> { 133 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 134 | fmt::Display::fmt(self, f) 135 | } 136 | } 137 | impl<'a> fmt::Display for SextantDisplay<'a> { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | if self.height == 0 { 140 | write!(f, "\u{1fb74}\u{1fb70}")?; 141 | } 142 | for y in 0..div_ceil(self.height, 3) { 143 | if y != 0 { 144 | writeln!(f)?; 145 | } 146 | write!(f, "\u{1fb74}")?; 147 | for x in 0..div_ceil(self.width, 2) { 148 | let get_bit = |offset_x, offset_y| { 149 | let (x, y) = (x * 2 + offset_x, y * 3 + offset_y); 150 | self.matrix.bit(self.width, x, y) as u32 151 | }; 152 | let offset = get_bit(0, 0) 153 | | get_bit(1, 0) << 1 154 | | get_bit(0, 1) << 2 155 | | get_bit(1, 1) << 3 156 | | get_bit(0, 2) << 4 157 | | get_bit(1, 2) << 5; 158 | let character = match offset { 159 | 0b111111 => '\u{2588}', 160 | 0b000000 => ' ', 161 | offset => char::from_u32(0x1fb00 + offset - 1).unwrap(), 162 | }; 163 | write!(f, "{character}")?; 164 | } 165 | write!(f, "\u{1fb70}")?; 166 | } 167 | Ok(()) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /design_doc/richtext/informal_grammar.md: -------------------------------------------------------------------------------- 1 | # Parsing grammar for rich text 2 | 3 | ``` 4 | namespace = 5 | 'Res' '(' ')' 6 | | 'One' '(' ')' 7 | | 'Name' '(' ')' '.' 8 | | 'Marked' '(' ')' '.' 9 | 10 | : [:alpha:_][:alphanum:_]* "identifier respecting rust's identifier rules" 11 | : "text that doesn't contain FOO, unless prefixed by backslash `\` 12 | may be empty" 13 | scope = '{' inner '}' | '(' inner ')' | '[' inner ']' 14 | semi_exposed = 15 | inner = semi_exposed [scope semi_exposed]* 16 | exposed = 17 | balanced_text = exposed [scope exposed]* 18 | 19 | format_spec = 20 | format = namespace path ':' format_spec 21 | binding = format | path 22 | 23 | path = 24 | key = 25 | open_subsection = 26 | open_section = 27 | close_section = '{' closed '}' 28 | closed_element = key ':' metadata 29 | closed = binding | [closed_element],* ['|' bare_content]? 30 | metadata = '{' binding '}' | balanced_text 31 | bare_content = open_subsection [close_section open_subsection]* 32 | rich_text = open_section [close_section open_section]* 33 | ``` 34 | 35 | Rich text is composed of N sections. 36 | Sections are a collection of metadatas plus some content. 37 | Metadatas are values associated with some `key`. 38 | If the section is just an identifer between braces (`{like_this}`), 39 | then it is *dynamic* `Content`. 40 | 41 | If the metadata value is between braces (`{like_this}`), then it is *dynamic*. 42 | *Dynamic* metadata can be set and updated at runtime by the user. 43 | 44 | A section may end by a `|` followed by text. This represents the text content 45 | of the section. 46 | 47 | This text may contain sub-sections, sub-sections may contain other sub-sections. 48 | A subsection is defined similarly to a sections, 49 | but cannot contain the same metadata as the section that contains it, recursively. 50 | 51 | `balanced_text` have balanced `[]`, `{}` and `()`, to opt-out of balance 52 | checking for those delimiter, escape them with `\\`. 53 | 54 | Since metadata elements are separated by a comma, the `metadata` text must also 55 | escape `,`, otherwise it is considered the end of the value, 56 | unless there is an unclosed open parenthesis or braces. 57 | 58 | ### Examples 59 | 60 | Each line of the following code block represents a valid rich text string. 61 | 62 | ``` 63 | This is some text, it is just a single content section 64 | This one contains a single {dynamic_content} that can be replaced at runtime 65 | {Color:{color}|This is also just some non-dynamic text, commas need not be escaped} 66 | {Content: This may also work\, but commas need to be escaped} 67 | {dynamic_content} 68 | {Color: Blue | This text is blue} 69 | {Color: Blue | {dynamic_blue_content}} 70 | {Color: Blue | This is non-bold text: {Font:bold.ttf|now it is bold, you may also use {RelSize:1.3|{deeply_nested}} sections}, not anymore {b:_|yet again}!} 71 | {Color:Red| Some red text}, some default Color {dynamic_name}. {Color:pink|And pink, why not?} 72 | {Color:rgb(12, 34, 50),Font:bold.ttf|metadata values} can contain commas within parenthesis or square brackets 73 | You can escape \{ curly brackets \}. 74 | {Color: pink| even inside \{ a closed section \}}. 75 | {Color: {relevant_color} | Not only Content can be dynamic, also value of other metadata} 76 | {Content:{ident}} is equivalent to {ident} also { ident }. 77 | ``` 78 | 79 | Note that spaces surrounding metadata delimiters are trimmed from the output. 80 | 81 | - after `{` 82 | - after `|` 83 | - after `,` 84 | 85 | ### Counter examples 86 | 87 | More importantly, we should provide high quality error messages when things do 88 | not go as planned. 89 | 90 | We should also avoid accepting text strings that do something very different 91 | from what the user expects. 92 | 93 | All the following strings should result in an error: 94 | 95 | ``` 96 | {some, text, with comma} 97 | ``` 98 | 99 | ## Why a custom markup language? 100 | 101 | ### Why not HTML 102 | 103 | - HTML is **HARD** to parse. It is an evolved markup language, it formalizes 104 | parsing quirks of browser versions from several decades ago. 105 | This means you are either doing **fake HTML**: breaking users' expectation 106 | with your quirky parser or pull in a **massive dependency** that solves the 107 | thousands of edge cases you have to consider when parsing HTML. 108 | - If you opt for the fake HTML route, you force your users to keep in mind at all 109 | time what differences your markup has with HTML, what they can and cannot do. 110 | With a clearly distinct markup language, you don't have to adjust expectations. 111 | - HTML is associated with web technologies. Providing HTML implicitly tells our 112 | users the names of attributes to use, what they are capable of, what they do. 113 | We will necessarily break those implicit promises in cuicui, so we should not 114 | make them. 115 | \ 116 | Take the example of the `
` element. Should I do with that? 117 | Interpret it as a line break? Why not let the user add a line break to their 118 | input string instead? 119 | \ 120 | And this isn't to mention stuff like the `style` or `onclick` attributes. 121 | 122 | Overall, we are trying to solve a different problem than what HTML is solving. 123 | We just want to display some text with basic styling in bevy, 124 | HTML is not appropriate for that. 125 | 126 | ### Why cuicui_richtext's markup language 127 | 128 | The markup language wasn't designed as a paragon of language design perfection, 129 | in fact, you could say it wasn't designed at all! 130 | 131 | But it works and is the perfect fit for the rich text format string. 132 | 133 | Unlike HTML, our markup language is not widely known, people aren't already 134 | familiar with it. However, this is a non-issue. 135 | 136 | In fact, people _are_ already familiar with it: 137 | 138 | - If you've seen JSON before, you understand the concept of 139 | `a series {of: key, values: within|brackets}`. 140 | - Previous HTML familiarity helps understand distinction 141 | `{between: attributes|and text}`. 142 | - It also `helps {understand:nested|collections{of:content|that}follow each} other` 143 | - The only trully new and "weird" bit are empty bindings and the `|` to 144 | start an inline text section. 145 | - If you have a math background, you've already seen `{predicate|declaration}` 146 | 147 | I hope cuicui's markup is less complex than HTML. 148 | 149 | - Unlike XML, people feel the need to close opened "nodes" by using `}`. 150 | - metadata is consistent, and always declared at the same position. 151 | 152 | So I've no doubt people will be able to pick it up quickly. 153 | --------------------------------------------------------------------------------