├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets └── FiraMono-Medium.ttf ├── examples ├── readme_example.rs ├── simple.rs ├── todomvc.rs └── widgets.rs ├── src ├── animation.rs ├── childable │ ├── mod.rs │ ├── tracked.rs │ └── tracked │ │ └── vec.rs ├── ctx.rs ├── dom │ ├── layout.rs │ ├── mod.rs │ └── render.rs ├── input.rs ├── insertable.rs ├── lens.rs ├── lib.rs ├── observer │ ├── component.rs │ ├── has_component.rs │ ├── mod.rs │ ├── opt_component.rs │ ├── res.rs │ └── single.rs ├── plugin.rs ├── runtime.rs ├── tutorial │ └── mod.rs └── widgets │ ├── button.rs │ ├── draggable.rs │ ├── mod.rs │ └── textbox.rs └── ui4-macros ├── Cargo.toml └── src └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: build 18 | run: cargo build --verbose 19 | 20 | test: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: test 25 | run: cargo test --tests 26 | 27 | doctest: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: test 32 | run: cargo test --doc 33 | 34 | fmt: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Check Formatting 39 | run: cargo fmt --all -- --check 40 | 41 | clippy: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Cargo clippy installation 46 | run: rustup component add clippy 47 | - name: Cargo clippy check 48 | run: cargo clippy --all --tests -- -D warnings 49 | 50 | rustdoc: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | - name: rustdoc 55 | run: cargo rustdoc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /.vscode 4 | *.code-workspace 5 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ui4" 3 | version = "0.1.3" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | description = "A reactive vdom-free ui library for the bevy game engine" 7 | repository = "https://github.com/TheRawMeatball/ui4" 8 | keywords = ["ui", "bevy"] 9 | 10 | [features] 11 | default = [] 12 | nightly = [] 13 | 14 | [dependencies] 15 | ui4-macros = { path = "./ui4-macros", version = "0.1.0" } 16 | 17 | # used for parallel collection of updatefunc 18 | dashmap = "5" 19 | ahash = "0.7" 20 | 21 | # tracked vec 22 | crossbeam-channel = "0.5.1" 23 | 24 | # layout 25 | morphorm = "0.3" # core layout algorithm 26 | derive_more = "0.99.16" # used for Deref impls on layout elements, and in examples 27 | concat-idents = "1.1.3" # used in generating impls 28 | 29 | # various optimizations 30 | smallvec = "1" 31 | 32 | # mapping mutex 33 | parking_lot = "0.11.2" 34 | 35 | bevy = { version = "0.6", default-features = false, features = ["render"] } 36 | 37 | bevy-inspector-egui = { git = "https://github.com/jakobhellermann/bevy-inspector-egui", rev = "f02c9d8", default-features = false } 38 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui4 2 | 3 | [![workflow](https://github.com/TheRawMeatball/ui4/actions/workflows/rust.yml/badge.svg)](https://github.com/TheRawMeatball/ui4/actions/workflows/rust.yml) 4 | 5 | ui4 is my fourth major attempt at making a UI dataflow library for the [Bevy](https://github.com/bevyengine/bevy) game engine. More specifically, it's a vdom-less UI library which uses fine-grained reactivity to keep your UI in sync with the rest of your game and itself. 6 | 7 | ## Warning 8 | 9 | This library is *incredibly* young and untested. Try at your own risk! 10 | 11 | ## Why ui4 12 | 13 | UI in bevy, as of 0.6, can get incredibly boilerplate-y. So can code made with this lib, but (hopefully) you'll find the same amount of boilerplate gets you much further with this crate :) 14 | 15 | More specifically, this lib offers a widget abstraction, reactivity, animations, and a collection of built-in widgets! 16 | 17 | ## Usage 18 | 19 | ```rust 20 | use bevy::prelude::*; 21 | use ui4::prelude::*; 22 | 23 | fn main() { 24 | let mut app = App::new(); 25 | app.add_plugins(DefaultPlugins) 26 | .add_plugin(Ui4Plugin) 27 | .add_plugin(Ui4Root(root)); 28 | 29 | app.world.spawn().insert_bundle(UiCameraBundle::default()); 30 | 31 | app.run() 32 | } 33 | 34 | fn root(ctx: Ctx) -> Ctx { 35 | #[derive(Component)] 36 | struct State(i32); 37 | 38 | let state = ctx.component(); 39 | let this = ctx.current_entity(); 40 | 41 | ctx.with(State(0)) 42 | .with(Top(Units::Pixels(50.))) 43 | .with(Left(Units::Pixels(50.))) 44 | .with(UiColor(Color::BLACK)) 45 | .with(Height(Units::Auto)) 46 | .with(Width(Units::Auto)) 47 | .child(text("Hello!").with(Height(Units::Pixels(30.)))) 48 | .child(|ctx| { 49 | ctx.with(Width(Units::Pixels(300.))) 50 | .with(Height(Units::Pixels(30.))) 51 | .with(LayoutType::Row) 52 | .child(button("Increment").with(OnClick::new(move |world| { 53 | world.get_mut::(this).unwrap().0 += 1; 54 | }))) 55 | .child(button("Decrement").with(OnClick::new(move |world| { 56 | world.get_mut::(this).unwrap().0 -= 1; 57 | }))) 58 | }) 59 | .child( 60 | text(state.map(|s: &State| format!("The number is {}", s.0))) 61 | .with(Height(Units::Pixels(30.))), 62 | ) 63 | } 64 | ``` 65 | 66 | Here's how that example looks: 67 | 68 | https://user-images.githubusercontent.com/6781733/151692143-50169d87-37fa-4bed-955f-28419dd72c2c.mov 69 | 70 | For more examples on how to use this library, look at the [examples](examples) folder, and also consider looking at the [tutorial module on docs.rs](https://docs.rs/ui4/latest/ui4/tutorial/index.html). 71 | 72 | Important note: This crate works around certain limitations of stable rust using boxing, so switching to nightly and enabling the `nightly` feature might improve performance, and is recommended. 73 | 74 | ## Help 75 | 76 | For help with using this lib, feel free to talk to @TheRawMeatball#9628 on [the bevy discord](https://discord.gg/bevy). I'm pretty active, so if you have questions ask away! And if you find a bug, a github issue would be appreciated :) 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /assets/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheRawMeatball/ui4/159ac9cef26a74d81b8fc5cac385d22ef8c8608f/assets/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /examples/readme_example.rs: -------------------------------------------------------------------------------- 1 | //! Keep this in sync with the readme 2 | 3 | use bevy::prelude::*; 4 | use ui4::prelude::*; 5 | 6 | fn main() { 7 | let mut app = App::new(); 8 | app.add_plugins(DefaultPlugins) 9 | .add_plugin(Ui4Plugin) 10 | .add_plugin(Ui4Root(root)); 11 | 12 | app.world.spawn().insert_bundle(UiCameraBundle::default()); 13 | 14 | app.run() 15 | } 16 | 17 | fn root(ctx: Ctx) -> Ctx { 18 | #[derive(Component)] 19 | struct State(i32); 20 | 21 | let state = ctx.component(); 22 | let this = ctx.current_entity(); 23 | 24 | ctx.with(State(0)) 25 | .with(Top(Units::Pixels(50.))) 26 | .with(Left(Units::Pixels(50.))) 27 | .with(UiColor(Color::BLACK)) 28 | .with(Height(Units::Auto)) 29 | .with(Width(Units::Auto)) 30 | .child(text("Hello!").with(Height(Units::Pixels(30.)))) 31 | .child(|ctx| { 32 | ctx.with(Width(Units::Pixels(300.))) 33 | .with(Height(Units::Pixels(30.))) 34 | .with(LayoutType::Row) 35 | .child(button("Increment").with(OnClick::new(move |world| { 36 | world.get_mut::(this).unwrap().0 += 1; 37 | }))) 38 | .child(button("Decrement").with(OnClick::new(move |world| { 39 | world.get_mut::(this).unwrap().0 -= 1; 40 | }))) 41 | }) 42 | .child( 43 | text(state.map(|s: &State| format!("The number is {}", s.0))) 44 | .with(Height(Units::Pixels(30.))), 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use derive_more::{Deref, DerefMut}; 3 | use ui4::prelude::*; 4 | 5 | fn main() { 6 | let mut app = App::new(); 7 | app.add_plugins(DefaultPlugins) 8 | .add_plugin(Ui4Plugin) 9 | .add_plugin(Ui4Root(root)); 10 | 11 | app.world.spawn().insert_bundle(UiCameraBundle::default()); 12 | 13 | app.run() 14 | } 15 | 16 | fn root(ctx: Ctx) -> Ctx { 17 | #[derive(Component)] 18 | struct State(i32); 19 | 20 | #[derive(Component, Default, DerefMut, Deref, Lens)] 21 | struct List(TrackedVec); 22 | 23 | #[derive(Component, Default, DerefMut, Deref, Lens)] 24 | struct EditedText(String); 25 | 26 | let state = ctx.component(); 27 | let list = ctx.component::(); 28 | let edited_text = ctx.component(); 29 | let this = ctx.current_entity(); 30 | 31 | ctx.with(State(0)) 32 | .with(UiColor(Color::BLACK)) 33 | .with(List::default()) 34 | .with(EditedText("".to_string())) 35 | .children(|ctx: &mut McCtx| { 36 | ctx.c(text("Hello!").with(Height(Units::Pixels(30.)))) 37 | .c(text("How are you doing?").with(Height(Units::Pixels(30.)))) 38 | .c(button("Increment").with(OnClick::new(move |world| { 39 | world.get_mut::(this).unwrap().0 += 1; 40 | }))) 41 | .c(button("Decrement").with(OnClick::new(move |world| { 42 | world.get_mut::(this).unwrap().0 -= 1; 43 | }))) 44 | .c( 45 | text(state.map(|s: &State| format!("The number is {}", s.0))) 46 | .with(Height(Units::Pixels(30.))), 47 | ) 48 | .c(textbox(edited_text.lens(EditedText::F0)).with(Height(Units::Pixels(30.)))); 49 | }) 50 | .child(|ctx: Ctx| { 51 | ctx.with(LayoutType::Row) 52 | .with(Height(Units::Pixels(30.))) 53 | .child(button("Add Hello".to_string()).with(OnClick::new(move |w| { 54 | w.get_mut::(this).unwrap().push("Hello".to_string()); 55 | }))) 56 | .child(button("Add Hoi".to_string()).with(OnClick::new(move |w| { 57 | w.get_mut::(this).unwrap().push("Hoi".to_string()); 58 | }))) 59 | .child( 60 | button("Remove last".to_string()).with(OnClick::new(move |w| { 61 | w.get_mut::(this).unwrap().pop(); 62 | })), 63 | ) 64 | .child( 65 | button("Remove first".to_string()).with(OnClick::new(move |w| { 66 | let mut list = w.get_mut::(this).unwrap(); 67 | if !list.is_empty() { 68 | list.remove(0); 69 | } 70 | })), 71 | ) 72 | }) 73 | .children( 74 | list.lens(List::F0) 75 | .each(|label: TrackedItemLens, _| { 76 | move |ctx: &mut McCtx| { 77 | ctx.c(counter(label.cloned())); 78 | } 79 | }), 80 | ) 81 | .children( 82 | res() 83 | .map(|time: &Time| time.seconds_since_startup() as usize % 2 == 0) 84 | .map_child(|b| { 85 | move |ctx: &mut McCtx| { 86 | if b { 87 | ctx.c( 88 | text("Now you see me".to_string()).with(Height(Units::Pixels(30.))) 89 | ); 90 | } 91 | } 92 | }), 93 | ) 94 | } 95 | 96 | fn counter(label: impl IntoObserver) -> impl FnOnce(Ctx) -> Ctx { 97 | #[derive(Component)] 98 | struct State(i32); 99 | 100 | move |ctx: Ctx| { 101 | let component = ctx.component(); 102 | let entity = ctx.current_entity(); 103 | ctx.with(LayoutType::Row) 104 | .with(State(0)) 105 | .with(Height(Units::Pixels(30.))) 106 | .children(move |ctx: &mut McCtx| { 107 | ctx.c(text(label)) 108 | .c(button("+".to_string()) 109 | .with(Width(Units::Pixels(50.))) 110 | .with(Height(Units::Pixels(30.))) 111 | .with(OnClick::new(move |w| { 112 | w.get_mut::(entity).unwrap().0 += 1; 113 | }))) 114 | .c(text(component.map(|x: &State| x.0.to_string())) 115 | .with(Width(Units::Pixels(50.))) 116 | .with(Height(Units::Pixels(30.)))) 117 | .c(button("-".to_string()) 118 | .with(Width(Units::Pixels(50.))) 119 | .with(Height(Units::Pixels(30.))) 120 | .with(OnClick::new(move |w| { 121 | w.get_mut::(entity).unwrap().0 -= 1; 122 | }))); 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/todomvc.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use derive_more::{Deref, DerefMut}; 3 | use std::sync::Arc; 4 | use ui4::{prelude::*, widgets::vscroll_view}; 5 | 6 | #[derive(Default, Deref, Lens)] 7 | struct EditedText(String); 8 | 9 | #[derive(Default, Deref, DerefMut, Lens)] 10 | struct TodoList(TrackedVec); 11 | 12 | #[derive(Clone)] 13 | struct Todo { 14 | text: Arc, 15 | done: bool, 16 | } 17 | 18 | fn main() { 19 | let mut app = App::new(); 20 | app.add_plugins(DefaultPlugins) 21 | .add_plugin(Ui4Plugin) 22 | .add_plugin(Ui4Root(root)) 23 | .init_resource::() 24 | .init_resource::(); 25 | 26 | app.world.spawn().insert_bundle(UiCameraBundle::default()); 27 | 28 | app.run() 29 | } 30 | 31 | fn root(ctx: Ctx) -> Ctx { 32 | ctx.with(UiColor(Color::BLACK)) 33 | .with(Right(Units::Stretch(1.))) 34 | .with(Left(Units::Stretch(1.))) 35 | .with(Width(Units::Pixels(400.))) 36 | .with(Height(Units::Pixels(600.))) 37 | .child( 38 | text("Todos") 39 | .with(TextSize(80.)) 40 | .with(Height(Units::Pixels(120.))) 41 | .with(TextAlign(TextAlignment { 42 | vertical: VerticalAlign::Top, 43 | horizontal: HorizontalAlign::Center, 44 | })), 45 | ) 46 | .child(|ctx: Ctx| { 47 | ctx.with(Height(Units::Auto)) 48 | .with(LayoutType::Row) 49 | .with(Left(Units::Pixels(5.))) 50 | .with(Right(Units::Pixels(5.))) 51 | .with(ColBetween(Units::Pixels(5.))) 52 | .with(Bottom(Units::Pixels(5.))) 53 | .child( 54 | textbox(res().lens(EditedText::F0)) 55 | .with(Width(Units::Stretch(9.))) 56 | .with(Height(Units::Pixels(30.))), 57 | ) 58 | .child( 59 | button("Add") 60 | .with(OnClick::new(|world: &mut World| { 61 | let text = std::mem::take( 62 | &mut world.get_resource_mut::().unwrap().0, 63 | ); 64 | world.get_resource_mut::().unwrap().push(Todo { 65 | text: text.into(), 66 | done: false, 67 | }); 68 | })) 69 | .with(Width(Units::Stretch(1.))) 70 | .with(Height(Units::Pixels(30.))), 71 | ) 72 | }) 73 | .child( 74 | vscroll_view(res::().lens(TodoList::F0).each(|item, index| { 75 | move |ctx: &mut McCtx| { 76 | ctx.c(todo(item, index)); 77 | } 78 | })) 79 | .with(Left(Units::Pixels(5.))) 80 | .with(Right(Units::Pixels(5.))), 81 | ) 82 | } 83 | 84 | fn todo(item: impl WorldLens, index: IndexObserver) -> impl FnOnce(Ctx) -> Ctx { 85 | move |ctx: Ctx| { 86 | ctx.with(Height(Units::Pixels(30.))) 87 | .with(LayoutType::Row) 88 | .with(Left(Units::Pixels(5.))) 89 | .with(Right(Units::Pixels(5.))) 90 | .with(Bottom(Units::Pixels(2.))) 91 | .with( 92 | item.map(|t: &Todo| t.done) 93 | .dedup() 94 | .map(|&done: &bool| if done { Color::GREEN } else { Color::NONE }) 95 | .map(UiColor), 96 | ) 97 | .child( 98 | text(item.map(|t: &Todo| t.text.to_string())) 99 | .with(TextSize(28.)) 100 | .with(TextAlign(TextAlignment { 101 | vertical: VerticalAlign::Top, 102 | horizontal: HorizontalAlign::Left, 103 | })), 104 | ) 105 | .children( 106 | item.map(|todo: &Todo| todo.done) 107 | .map_child(move |done: bool| { 108 | move |ctx: &mut McCtx| { 109 | if done { 110 | ctx.c(|ctx: Ctx| { 111 | ctx.with(Width(Units::Pixels(150.))) 112 | .with(Height(Units::Pixels(30.))) 113 | .with(LayoutType::Row) 114 | .with(ColBetween(Units::Pixels(5.))) 115 | .child( 116 | button("Unmark") 117 | .with(Width(Units::Stretch(1.))) 118 | .with(Height(Units::Pixels(30.))) 119 | .with(OnClick::new(move |world| { 120 | item.get_mut(world).done = false; 121 | })), 122 | ) 123 | .child( 124 | button("Remove") 125 | .with(button::HoverColor(Color::RED)) 126 | .with(Width(Units::Stretch(1.))) 127 | .with(Height(Units::Pixels(30.))) 128 | .with(index.dedup().map(|&i: &usize| { 129 | OnClick::new(move |world| { 130 | let mut list = world 131 | .get_resource_mut::() 132 | .unwrap(); 133 | list.remove(i); 134 | }) 135 | })), 136 | ) 137 | }); 138 | } else { 139 | ctx.c(button("Mark Complete") 140 | .with(Width(Units::Pixels(150.))) 141 | .with(Height(Units::Pixels(30.))) 142 | .with(OnClick::new(move |world| { 143 | item.get_mut(world).done = true; 144 | }))); 145 | } 146 | } 147 | }), 148 | ) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /examples/widgets.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use derive_more::{Deref, DerefMut}; 3 | use std::hash::Hash; 4 | use ui4::{dom::ManualRoot, prelude::*}; 5 | 6 | fn main() { 7 | let mut app = App::new(); 8 | app.add_plugins(DefaultPlugins) 9 | .add_plugin(Ui4Plugin) 10 | .add_plugin(Ui4Root(root)); 11 | 12 | app.world.spawn().insert_bundle(UiCameraBundle::default()); 13 | 14 | app.run() 15 | } 16 | 17 | fn root(ctx: Ctx) -> Ctx { 18 | #[derive(Component, Deref, DerefMut, Default, Lens)] 19 | struct TextboxText(String); 20 | 21 | #[derive(Component, Deref, DerefMut, Default, Lens)] 22 | struct CheckboxData(bool); 23 | 24 | #[derive(Component, Hash, Copy, Clone, PartialEq, Eq)] 25 | enum RadioButtonSelect { 26 | A, 27 | B, 28 | C, 29 | } 30 | 31 | #[derive(Component, Deref, Lens)] 32 | struct Slider(f32); 33 | 34 | let textbox_text = ctx.component(); 35 | let checkbox_data = ctx.component(); 36 | let radiobutton = ctx.component(); 37 | 38 | let slider_percent = ctx.component(); 39 | 40 | ctx.with(TextboxText::default()) 41 | .with(CheckboxData::default()) 42 | .with(RadioButtonSelect::A) 43 | .with(Slider(0.42)) 44 | .with(UiColor(Color::DARK_GREEN)) 45 | .children(|ctx: &mut McCtx| { 46 | ctx.c(labelled_widget( 47 | "Button", 48 | button("Click me!").with(OnClick::new(|_| println!("you clicked the button!"))), 49 | )) 50 | .c(labelled_widget( 51 | "Textbox", 52 | textbox(textbox_text.lens(TextboxText::F0)), 53 | )) 54 | .c(labelled_widget( 55 | "Checkbox", 56 | checkbox(checkbox_data.lens(CheckboxData::F0)), 57 | )) 58 | .c(labelled_widget("Radio buttons", |ctx| { 59 | ctx.with(Width(Units::Pixels(250.))) 60 | .with(Height(Units::Pixels(30.))) 61 | .with(LayoutType::Row) 62 | .with(ColBetween(Units::Stretch(1.))) 63 | .children(|ctx: &mut McCtx| { 64 | ctx.c(radio_button(RadioButtonSelect::A, radiobutton)) 65 | .c(text("A ")) 66 | .c(radio_button(RadioButtonSelect::B, radiobutton)) 67 | .c(text("B ")) 68 | .c(radio_button(RadioButtonSelect::C, radiobutton)) 69 | .c(text("C ")); 70 | }) 71 | })) 72 | .c(labelled_widget( 73 | "Dropdown", 74 | dropdown( 75 | [ 76 | (RadioButtonSelect::A, "A"), 77 | (RadioButtonSelect::B, "B"), 78 | (RadioButtonSelect::C, "C"), 79 | ], 80 | radiobutton, 81 | ), 82 | )) 83 | .c(labelled_widget( 84 | "Progress", 85 | progressbar(slider_percent.dereffed().copied()), 86 | )) 87 | .c(labelled_widget( 88 | "Slider", 89 | slider(slider_percent.lens(Slider::F0)), 90 | )) 91 | .c(labelled_widget( 92 | "Tweened", 93 | progressbar( 94 | textbox_text 95 | .map(|t: &TextboxText| t.0.parse::().unwrap_or(0.42).clamp(0., 1.)) 96 | .dedup() 97 | .copied() 98 | .tween(0.2), 99 | ), 100 | )) 101 | .c(|ctx| { 102 | ctx.with(ManualRoot) 103 | .child(draggable_window(|ctx: &mut McCtx| { 104 | ctx.c(text("This is a draggable window!")); 105 | 106 | ctx.c(button("You can click this button!")); 107 | })) 108 | }) 109 | .c(toggle(|| { 110 | toggle(|| text_fade("Hey!").with(Height(Units::Pixels(30.)))) 111 | })); 112 | }) 113 | } 114 | 115 | fn labelled_widget( 116 | label: &'static str, 117 | widget: impl FnOnce(Ctx) -> Ctx, 118 | ) -> impl FnOnce(Ctx) -> Ctx { 119 | move |ctx: Ctx| { 120 | ctx.with(Width(Units::Pixels(400.))) 121 | .with(Height(Units::Pixels(30.))) 122 | .with(LayoutType::Row) 123 | .children(|ctx: &mut McCtx| { 124 | ctx.c(text(label) 125 | .with(Width(Units::Pixels(150.))) 126 | .with(Height(Units::Pixels(30.)))) 127 | .c(widget); 128 | }) 129 | } 130 | } 131 | 132 | fn toggle Ctx>( 133 | child: impl Fn() -> F + Send + Sync + 'static, 134 | ) -> impl FnOnce(Ctx) -> Ctx { 135 | #[derive(Component, Deref, DerefMut, Default, Lens)] 136 | struct Toggle(bool); 137 | |ctx: Ctx| { 138 | let checked = ctx.component::(); 139 | ctx.with(Toggle(false)) 140 | .child(checkbox(checked.lens(Toggle::F0))) 141 | .children(checked.dereffed().copied().map_child(move |b| { 142 | let child = child(); 143 | move |ctx: &mut McCtx| { 144 | if b { 145 | ctx.c(child); 146 | } 147 | } 148 | })) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/animation.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicU32; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use bevy::core::Time; 5 | use bevy::ecs::prelude::*; 6 | use bevy::ecs::system::SystemState; 7 | use bevy::prelude::{Children, DespawnRecursiveExt}; 8 | 9 | use crate::dom::Control; 10 | use crate::observer::{Observer, UninitObserver}; 11 | use crate::runtime::{UiScratchSpace, UpdateFunc}; 12 | 13 | #[derive(Component)] 14 | pub(crate) struct ActiveTween { 15 | duration: f32, 16 | time_left: f32, 17 | start: f32, 18 | end: f32, 19 | arc: Arc, 20 | uf: UpdateFunc, 21 | } 22 | 23 | pub struct UninitTweenObserver { 24 | observer: UO, 25 | settings: TweenSettings, 26 | } 27 | 28 | struct TweenSettings { 29 | duration: f32, 30 | // interpolation_type: ? 31 | } 32 | 33 | pub struct TweenObserver { 34 | current_val: Arc, 35 | } 36 | 37 | pub trait TweenExt: Sized { 38 | fn tween(self, duration: f32) -> UninitTweenObserver { 39 | UninitTweenObserver { 40 | observer: self, 41 | settings: TweenSettings { duration }, 42 | } 43 | } 44 | } 45 | 46 | impl TweenExt for UO 47 | where 48 | UO: UninitObserver, 49 | O: for<'a> Observer<'a, Return = f32>, 50 | { 51 | } 52 | 53 | impl TweenObserver { 54 | fn new() -> (Self, Arc) { 55 | let arc = Arc::::default(); 56 | ( 57 | Self { 58 | current_val: arc.clone(), 59 | }, 60 | arc, 61 | ) 62 | } 63 | } 64 | 65 | impl UninitObserver for UninitTweenObserver 66 | where 67 | UO: UninitObserver, 68 | O: for<'a> Observer<'a, Return = f32>, 69 | { 70 | type Observer = TweenObserver; 71 | 72 | fn register_self UpdateFunc>( 73 | self, 74 | world: &mut World, 75 | uf: F, 76 | ) -> UpdateFunc { 77 | self.observer.register_self(world, |mut observer, world| { 78 | let (obs, arc) = TweenObserver::new(); 79 | let uf = uf(obs, world); 80 | let ufm = Arc::new(Mutex::new(None)); 81 | let ufmc = ufm.clone(); 82 | let mut first = true; 83 | let mut current = None; 84 | let (uf, marker) = UpdateFunc::new::<(), _>(move |world| { 85 | if uf.flagged() { 86 | ufmc.lock().unwrap().take(); 87 | return; 88 | } 89 | let (val, changed) = observer.get(world); 90 | if !changed && !first { 91 | return; 92 | } 93 | let old = if first { 94 | first = false; 95 | val 96 | } else { 97 | f32::from_bits(arc.load(std::sync::atomic::Ordering::SeqCst)) 98 | }; 99 | arc.store(f32::to_bits(val), std::sync::atomic::Ordering::SeqCst); 100 | 101 | if let Some(ct) = current { 102 | if let Some(mut current) = world.get_mut::(ct) { 103 | let intp = current.time_left / current.duration; 104 | current.start = 105 | (current.start - current.end).mul_add(intp.clamp(0., 1.), current.end); 106 | current.end = val; 107 | current.time_left = current.duration; 108 | return; 109 | } 110 | } 111 | current = Some( 112 | world 113 | .spawn() 114 | .insert(ActiveTween { 115 | duration: self.settings.duration, 116 | time_left: self.settings.duration, 117 | start: old, 118 | end: val, 119 | arc: arc.clone(), 120 | uf: uf.clone(), 121 | }) 122 | .id(), 123 | ); 124 | }); 125 | *ufm.lock().unwrap() = Some(marker); 126 | uf 127 | }) 128 | } 129 | } 130 | 131 | impl<'a> Observer<'a> for TweenObserver { 132 | type Return = f32; 133 | 134 | fn get(&'a mut self, _: &'a World) -> (Self::Return, bool) { 135 | let val = self.current_val.load(std::sync::atomic::Ordering::SeqCst); 136 | (f32::from_bits(val), true) 137 | } 138 | } 139 | 140 | pub(crate) fn tween_system( 141 | time: Res