├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── crab_counter.png └── logo.svg ├── crates ├── frui_core │ ├── Cargo.toml │ └── src │ │ ├── api │ │ ├── any_ext.rs │ │ ├── contexts │ │ │ ├── build_cx │ │ │ │ ├── mod.rs │ │ │ │ └── widget_state.rs │ │ │ ├── mod.rs │ │ │ └── render │ │ │ │ ├── ext.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── paint_cx.rs │ │ │ │ ├── parent_data.rs │ │ │ │ ├── render_cx.rs │ │ │ │ ├── render_state.rs │ │ │ │ └── types │ │ │ │ ├── constraints.rs │ │ │ │ ├── geometry.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── offset.rs │ │ │ │ └── size.rs │ │ ├── implementers │ │ │ ├── inherited.rs │ │ │ ├── mod.rs │ │ │ ├── render.rs │ │ │ └── view.rs │ │ ├── impls.rs │ │ ├── local_key.rs │ │ ├── mod.rs │ │ ├── pointer_events │ │ │ ├── context.rs │ │ │ ├── events.rs │ │ │ ├── mod.rs │ │ │ ├── pointer_listener.rs │ │ │ └── pointer_region.rs │ │ ├── structural_eq │ │ │ ├── mod.rs │ │ │ └── structural_eq.rs │ │ └── widget_ptr.rs │ │ ├── app │ │ ├── listeners │ │ │ ├── keyboard.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── runner │ │ │ ├── miri │ │ │ │ ├── mod.rs │ │ │ │ └── substitutes.rs │ │ │ ├── mod.rs │ │ │ ├── native │ │ │ │ └── mod.rs │ │ │ └── window_handler.rs │ │ └── tree │ │ │ ├── mod.rs │ │ │ └── pointer_handler.rs │ │ └── lib.rs ├── frui_macros │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── macros.rs │ │ └── macros │ │ ├── builder.rs │ │ ├── copy_trait_as.rs │ │ ├── impl_tuple_slice.rs │ │ ├── sealed.rs │ │ └── widget_impl │ │ ├── RawWidget.rs │ │ ├── StructuralEq.rs │ │ ├── WidgetDerive.rs │ │ └── mod.rs └── frui_widgets │ ├── Cargo.toml │ └── src │ ├── basic.rs │ ├── boxes.rs │ ├── container.rs │ ├── event_detectors │ ├── keyboard.rs │ └── mod.rs │ ├── flex │ ├── alignment.rs │ ├── center.rs │ ├── flex.rs │ ├── mod.rs │ └── stack.rs │ ├── lib.rs │ ├── painting │ ├── border_radius.rs │ ├── borders.rs │ ├── box_border.rs │ ├── decoration.rs │ ├── edge_insets.rs │ ├── mod.rs │ └── shadow.rs │ ├── scroll.rs │ ├── testing.rs │ ├── text.rs │ ├── transform.rs │ └── widget_list.rs ├── examples ├── boxes.rs ├── button.rs ├── column.rs ├── counter.rs ├── crab_counter.rs ├── decoration.rs ├── flex.rs ├── inherited_widget.rs ├── local_key.rs ├── misc │ ├── flex_children.rs │ ├── mod.rs │ ├── random_state.rs │ └── switch.rs ├── pointer_events.rs ├── preserve_state.rs ├── row.rs ├── stack.rs ├── switch_widgets.rs └── transform.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | /.idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frui" 3 | version = "0.0.1" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["iglak "] 6 | description = "A developer-friendly framework for building user interfaces in Rust" 7 | repository = "https://github.com/fruiframework/frui" 8 | readme = "README.md" 9 | categories = ["gui"] 10 | keywords = ["gui", "ui", "framework", "frui"] 11 | exclude = ["assets/", ".vscode/", ".github/"] 12 | edition = "2021" 13 | 14 | 15 | [workspace] 16 | exclude = ["examples/misc"] 17 | members = ["crates/*", "examples/*"] 18 | 19 | [dependencies] 20 | frui_core = { path = "crates/frui_core", version = "0.0.1" } 21 | frui_widgets = { path = "crates/frui_widgets", version = "0.0.1" } 22 | 23 | [dev-dependencies] 24 | log = "0.4.17" 25 | rand = "0.8.5" 26 | 27 | [features] 28 | miri = ["frui_core/miri", "frui_widgets/miri"] 29 | 30 | # To run tests in examples with Miri, use one of the following commands: 31 | # 32 | # MIRIFLAGS="-Zmiri-disable-isolation" cargo miri test --example [EXAMPLE] --features "miri" 33 | # MIRIFLAGS="-Zmiri-disable-isolation" cargo miri test --examples --features "miri" 34 | -------------------------------------------------------------------------------- /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 |

Frui

2 | 3 | *

Reading: "Fru–" as in "fruit" and "–i" as in "I am".

* 4 | 5 |

6 | Latest version 7 | MIT 8 | Apache 9 | Discord 10 |

11 | 12 | ## What is Frui? 13 | 14 | Frui is a developer-friendly UI framework that makes building user interfaces easy and productive. It's inspired by Flutter architecture and is written in Rust! 15 | 16 | For an introduction see the [announcement](https://github.com/fruiframework/frui-blog/blob/master/posts/001/0.0.1.md). 17 | 18 | ## Example 19 | 20 | ```rust 21 | #![feature(type_alias_impl_trait)] 22 | 23 | use frui::prelude::*; 24 | 25 | #[derive(ViewWidget)] 26 | struct App<'a>(&'a str); 27 | 28 | impl ViewWidget for App<'_> { 29 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 30 | Center::child(Text::new(format!("Hello, {}!", self.0))) 31 | } 32 | } 33 | 34 | fn main() { 35 | run_app(App("World")); 36 | } 37 | ``` 38 | 39 | ## Warning 40 | 41 | Frui is an experimental framework and is not suitable for building real applications. It is more of a proof of concept and an exploration of new ideas, rather than a fully-fledged and reliable tool. While it may have some interesting implications and possibilities, it is not yet ready for production use and the development efforts on it are rather sporadic. It is currently not optimized and some important features have not been implemented. 42 | 43 | To compile it you will need to install the **latest version of nightly Rust**. 44 | 45 | Feel free to try it out! 46 | 47 | ## Features 48 | 49 | *Ok, what's in there?* 50 | 51 | - Basic widgets: 52 | - `ViewWidget` 53 | - `InheritedWidget` 54 | - `RenderWidget` 55 | - Impls: 56 | - `WidgetState` 57 | - `RenderState` 58 | - `ParentData` 59 | - Preserving state: 60 | - `LocalKey` 61 | - Position in children list 62 | - Basic event detectors: 63 | - `KeyboardEventDetector` 64 | - `PointerListener` 65 | - `PointerRegion` 66 | - Basic widgets: 67 | - `Text` 68 | - `Center` 69 | - `Row` 70 | - `Column` 71 | - `Flex` 72 | - `Stack` 73 | - `SizedBox` 74 | - `LimitedBox` 75 | - `ConstrainedBox` 76 | 77 | 78 | For more features see `examples`. 79 | 80 | 81 | ## 🦀 Counter - Example 82 | 83 | Obligatory crab counter! From `examples/crab_counter.rs`. 84 | 85 | ```rust 86 | 87 | #![feature(type_alias_impl_trait)] 88 | 89 | use frui::prelude::*; 90 | 91 | mod misc; 92 | use misc::Button; 93 | 94 | #[derive(ViewWidget)] 95 | struct CrabCounter; 96 | 97 | impl WidgetState for CrabCounter { 98 | type State = isize; 99 | 100 | fn create_state(&self) -> Self::State { 0 } 101 | } 102 | 103 | impl ViewWidget for CrabCounter { 104 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 105 | Column::builder() 106 | .space_between(60.0) 107 | .main_axis_size(MainAxisSize::Max) 108 | .cross_axis_size(CrossAxisSize::Max) 109 | .main_axis_alignment(MainAxisAlignment::Center) 110 | .cross_axis_alignment(CrossAxisAlignment::Center) 111 | .children(( 112 | Text::new(format!("{} 🦀", *cx.state())) 113 | .size(100.0) 114 | .weight(FontWeight::BOLD), 115 | Row::builder() 116 | .space_between(10.0) 117 | .children(( 118 | Button { 119 | label: Text::new("+").size(30.), 120 | on_click: || *cx.state_mut() += 1, 121 | }, 122 | Button { 123 | label: Text::new("-").size(30.), 124 | on_click: || *cx.state_mut() -= 1, 125 | }, 126 | )), 127 | )) 128 | } 129 | } 130 | 131 | fn main() { 132 | run_app(CrabCounter); 133 | } 134 | ``` 135 | 136 |

screenshot of application running above code

137 | 138 | *

Crab counter running on MacOS

* 139 | 140 | ## Credits 141 | 142 | 143 | Frui was inspired by Flutter's widget architecture and API, and builds upon the work done in Druid, which powers much of the back-end and has influenced the implementation of many widgets. The contributions of both Flutter and Druid were essential to the development of Frui, and for those – thank you! 144 | 145 | ## License 146 | 147 | All code in this repository is dual-licensed under either: 148 | 149 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 150 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 151 | -------------------------------------------------------------------------------- /assets/crab_counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fruiframework/frui/480ce6018ecf7626187f56d6987b86b5cf3007c9/assets/crab_counter.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /crates/frui_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frui_core" 3 | version = "0.0.1" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["iglak "] 6 | description = "Core functionality of Frui UI framework" 7 | repository = "https://github.com/fruiframework/frui" 8 | edition = "2021" 9 | 10 | 11 | [dependencies] 12 | frui_macros = { path = "../frui_macros", version = "0.0.1" } 13 | 14 | log = "0.4.17" 15 | slotmap = "1.0.6" 16 | simplelog = "0.12.0" 17 | once_cell = "1.13.0" 18 | druid-shell = { git = "https://github.com/linebender/druid.git", rev = "ac3815114c65d46fd388431d3013a9412501916b" } 19 | 20 | [features] 21 | miri = [] 22 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/any_ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | marker::PhantomData, 4 | }; 5 | 6 | /// This trait allows us to acquire `TypeId` of any `T` (not just `T: 'static`), 7 | /// which is used to downcast trait objects containing non-static fields to a 8 | /// concrete type. 9 | pub trait AnyExt: AsAny { 10 | fn type_id(&self) -> TypeId; 11 | 12 | fn type_name(&self) -> &'static str; 13 | 14 | /// Helper function. 15 | fn as_any_ext(&self) -> &dyn AnyExt; 16 | } 17 | 18 | impl AnyExt for T { 19 | fn type_id(&self) -> TypeId { 20 | get_type_id::() 21 | } 22 | 23 | fn type_name(&self) -> &'static str { 24 | std::any::type_name::() 25 | } 26 | 27 | fn as_any_ext(&self) -> &dyn AnyExt { 28 | self 29 | } 30 | } 31 | 32 | impl<'a> dyn AnyExt + 'a { 33 | /// Downcasts reference `self` to `T` or returns `None`. 34 | /// 35 | /// # Safety 36 | /// 37 | /// Downcasted `&T` may contain references of lifetimes that are 38 | /// different between two structures even if `TypeId`s match. 39 | /// 40 | /// One must ensure that this cannot cause UB. 41 | /// 42 | /// # Example 43 | /// 44 | /// Using internal mutabilty one can swap `'a` and `'static` references 45 | /// causing dangling references and use-after-free. 46 | /// 47 | /// ``` 48 | /// struct Test<'a> { 49 | /// r: RefCell<&'a str>, 50 | /// } 51 | /// 52 | /// impl<'a> Test<'a> { 53 | /// fn swap(&'a self, other: &'a Test<'a>) { 54 | /// *self.r.borrow_mut() = *other.r.borrow(); 55 | /// } 56 | /// } 57 | /// 58 | /// let string = String::from("non 'static"); 59 | /// 60 | /// let static_ = Test { 61 | /// r: RefCell::new("'static str"), 62 | /// }; 63 | /// let non_static = Test { 64 | /// r: RefCell::new(&string), 65 | /// }; 66 | /// 67 | /// let static_any: &dyn AnyExt = &static_; 68 | /// let non_static_any: &dyn AnyExt = &non_static; 69 | /// 70 | /// fn uh_oh(static_: &dyn AnyExt, non_static: &dyn AnyExt) { 71 | /// unsafe { 72 | /// let static_ = static_.downcast_ref::().unwrap(); 73 | /// let non_static = non_static.downcast_ref::().unwrap(); 74 | /// 75 | /// static_.swap(non_static); 76 | /// } 77 | /// } 78 | /// 79 | /// uh_oh(static_any, non_static_any); 80 | /// 81 | /// drop(string); 82 | /// println!("{}", static_.r.borrow()); // uh-oh 83 | /// ``` 84 | pub unsafe fn downcast_ref(&self) -> Option<&T> { 85 | match AnyExt::type_id(self) == get_type_id::() { 86 | true => Some(&*(self as *const _ as *const T)), 87 | false => None, 88 | } 89 | } 90 | 91 | /// # Safety 92 | /// 93 | /// See `downcast_ref`. 94 | pub unsafe fn downcast_mut(&mut self) -> Option<&mut T> { 95 | match AnyExt::type_id(self) == get_type_id::() { 96 | true => Some(&mut *(self as *mut _ as *mut T)), 97 | false => None, 98 | } 99 | } 100 | } 101 | 102 | struct TypeIdKey(PhantomData); 103 | 104 | impl TypeIdKey { 105 | fn new() -> Self { 106 | TypeIdKey(PhantomData) 107 | } 108 | } 109 | 110 | fn get_type_id() -> TypeId { 111 | unsafe { 112 | let key = >::new(); 113 | 114 | // Safety: We cast &key to 'static to be able to cast it to `Any` to acquire TypeId. 115 | // This is because `TypeId::of::>()` won't work since T isn't 'static. 116 | // 117 | // That `&'static key` reference is not used any longer than it would normally be. 118 | let any = std::mem::transmute::<&dyn AsAny, &'static dyn AsAny>(&key); 119 | let any = any.as_any(); 120 | Any::type_id(any) 121 | } 122 | } 123 | 124 | /// Helper trait used in [`get_type_id`] above. 125 | pub trait AsAny { 126 | fn as_any(&'static self) -> &dyn Any; 127 | } 128 | 129 | impl AsAny for T { 130 | fn as_any(&'static self) -> &dyn Any { 131 | self 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use super::*; 138 | 139 | #[test] 140 | fn should_downcast() { 141 | unsafe { 142 | assert!((&16usize as &dyn AnyExt).downcast_ref::().is_some()); 143 | assert!((&String::new() as &dyn AnyExt) 144 | .downcast_ref::() 145 | .is_some()); 146 | assert!((&std::sync::Mutex::new(2u8) as &dyn AnyExt) 147 | .downcast_ref::>() 148 | .is_some()); 149 | } 150 | } 151 | 152 | #[test] 153 | fn should_not_downcast() { 154 | unsafe { 155 | assert!((&16usize as &dyn AnyExt).downcast_ref::().is_none()); 156 | assert!((&std::sync::Mutex::new(2u8) as &dyn AnyExt) 157 | .downcast_ref::>() 158 | .is_none()); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/build_cx/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::Any, 3 | cell::{Ref, RefMut}, 4 | marker::PhantomData, 5 | ops::{Deref, DerefMut}, 6 | sync::atomic::{AtomicBool, Ordering}, 7 | }; 8 | 9 | use crate::{ 10 | app::tree::{Node, NodeRef}, 11 | prelude::InheritedWidget, 12 | }; 13 | 14 | pub mod widget_state; 15 | pub use widget_state::WidgetState; 16 | 17 | /// Set by framework when accessing state mutably shouldn't register widget for 18 | /// state updates (e.g. in unmount/mount methods). 19 | pub(crate) static STATE_UPDATE_SUPRESSED: AtomicBool = AtomicBool::new(false); 20 | 21 | // `BuildCx` is borrowed to make it so that closures don't take ownership 22 | // of it, which would be inconvenient - user would have to clone `BuildCx` 23 | // before every closure, since otherwise the context would move. 24 | pub type BuildCx<'a, T> = &'a _BuildCx<'a, T>; 25 | 26 | #[repr(transparent)] 27 | pub struct _BuildCx<'a, T> { 28 | node: Node, 29 | _p: PhantomData<&'a T>, 30 | } 31 | 32 | impl<'a, T> _BuildCx<'a, T> { 33 | pub fn state(&self) -> StateGuard 34 | where 35 | T: WidgetState, 36 | { 37 | StateGuard { 38 | guard: Ref::map(self.node.inner.borrow(), |node| node.state.deref()), 39 | _p: PhantomData, 40 | } 41 | } 42 | 43 | pub fn state_mut(&self) -> StateGuardMut 44 | where 45 | T: WidgetState, 46 | { 47 | if !STATE_UPDATE_SUPRESSED.load(Ordering::SeqCst) { 48 | self.node_ref().mark_dirty(); 49 | } 50 | 51 | StateGuardMut { 52 | guard: RefMut::map(self.node.inner.borrow_mut(), |node| node.state.deref_mut()), 53 | _p: PhantomData, 54 | } 55 | } 56 | 57 | /// This method registers the widget of this [`BuildCx`] as a dependency of 58 | /// the closest [`InheritedWidget`] ancestor of type `W` in the tree. It 59 | /// then returns the state of that inherited widget or [`None`] if inherited 60 | /// ancestor doesn't exist. 61 | pub fn depend_on_inherited_widget(&self) -> Option> 62 | where 63 | W: InheritedWidget + WidgetState, 64 | { 65 | // Register and get inherited widget of specified key. 66 | let node = self 67 | .node_ref() 68 | .depend_on_inherited_widget_of_key::()?; 69 | 70 | Some(InheritedState { 71 | node, 72 | _p: PhantomData, 73 | }) 74 | } 75 | 76 | fn node_ref(&self) -> NodeRef { 77 | NodeRef { 78 | ptr: self.node.inner.borrow().is_alive.clone(), 79 | } 80 | } 81 | } 82 | 83 | pub struct StateGuard<'a, T: 'static> { 84 | pub(crate) guard: Ref<'a, dyn Any>, 85 | pub(crate) _p: PhantomData<&'a T>, 86 | } 87 | 88 | impl<'a, T: 'static> Deref for StateGuard<'a, T> { 89 | type Target = T; 90 | 91 | fn deref(&self) -> &Self::Target { 92 | self.guard.deref().downcast_ref().unwrap() 93 | } 94 | } 95 | 96 | pub struct StateGuardMut<'a, T: 'static> { 97 | pub(crate) guard: RefMut<'a, dyn Any>, 98 | pub(crate) _p: PhantomData<&'a T>, 99 | } 100 | 101 | impl<'a, T: 'static> Deref for StateGuardMut<'a, T> { 102 | type Target = T; 103 | 104 | fn deref(&self) -> &Self::Target { 105 | self.guard.deref().downcast_ref().unwrap() 106 | } 107 | } 108 | 109 | impl<'a, T: 'static> std::ops::DerefMut for StateGuardMut<'a, T> { 110 | fn deref_mut(&mut self) -> &mut Self::Target { 111 | self.guard.deref_mut().downcast_mut().unwrap() 112 | } 113 | } 114 | 115 | pub struct InheritedState<'a, T: 'static> { 116 | pub(crate) node: NodeRef, 117 | pub(crate) _p: PhantomData<&'a T>, 118 | } 119 | 120 | impl<'a, T: 'static> InheritedState<'a, T> { 121 | pub fn as_ref(&'a self) -> InheritedStateRef<'a, T> { 122 | InheritedStateRef { 123 | state: Ref::map(self.node.borrow(), |node| node.state.deref()), 124 | _p: PhantomData, 125 | } 126 | } 127 | 128 | pub fn as_mut(&'a mut self) -> InheritedStateRefMut<'a, T> { 129 | if !STATE_UPDATE_SUPRESSED.load(Ordering::SeqCst) { 130 | self.node.mark_dirty(); 131 | self.node.mark_dependent_widgets_as_dirty(); 132 | } 133 | 134 | InheritedStateRefMut { 135 | state: RefMut::map(self.node.borrow_mut(), |node| node.state.deref_mut()), 136 | _p: PhantomData, 137 | } 138 | } 139 | } 140 | 141 | pub struct InheritedStateRef<'a, T: 'static> { 142 | state: Ref<'a, dyn Any>, 143 | _p: PhantomData, 144 | } 145 | 146 | impl<'a, T> Deref for InheritedStateRef<'a, T> { 147 | type Target = T; 148 | 149 | fn deref(&self) -> &Self::Target { 150 | self.state.downcast_ref().unwrap() 151 | } 152 | } 153 | 154 | pub struct InheritedStateRefMut<'a, T: 'static> { 155 | state: RefMut<'a, dyn Any>, 156 | _p: PhantomData, 157 | } 158 | 159 | impl<'a, T> Deref for InheritedStateRefMut<'a, T> { 160 | type Target = T; 161 | 162 | fn deref(&self) -> &Self::Target { 163 | self.state.downcast_ref().unwrap() 164 | } 165 | } 166 | 167 | impl<'a, T> DerefMut for InheritedStateRefMut<'a, T> { 168 | fn deref_mut(&mut self) -> &mut Self::Target { 169 | self.state.downcast_mut().unwrap() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/build_cx/widget_state.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | use frui_macros::sealed; 4 | 5 | use crate::macro_exports::RawBuildCx; 6 | 7 | use super::{BuildCx, _BuildCx}; 8 | 9 | pub trait WidgetState: Sized { 10 | type State: 'static; 11 | 12 | fn create_state(&self) -> Self::State; 13 | 14 | /// Called when the widget is mounted into the tree (before build). 15 | /// 16 | /// Accessing `state_mut` of this [`BuildCx`] will not schedule rebuild. 17 | fn mount<'a>(&'a self, cx: BuildCx<'a, Self>) { 18 | let _ = cx; 19 | } 20 | 21 | /// Called when the widget is unmounted from the tree. At this point given 22 | /// widget may be dropped or mounted again with its configuration updated. 23 | /// 24 | /// Accessing `state_mut` of this [`BuildCx`] will not schedule rebuild. 25 | fn unmount<'a>(&'a self, cx: BuildCx<'a, Self>) { 26 | let _ = cx; 27 | } 28 | } 29 | 30 | #[sealed(crate)] 31 | pub trait WidgetStateOS { 32 | fn state_type_id(&self) -> TypeId; 33 | fn create_state(&self) -> Box; 34 | 35 | fn mount(&self, build_cx: &RawBuildCx); 36 | fn unmount(&self, build_cx: &RawBuildCx); 37 | } 38 | 39 | impl WidgetStateOS for T { 40 | default fn state_type_id(&self) -> TypeId { 41 | struct WidgetHasNoState; 42 | TypeId::of::() 43 | } 44 | 45 | default fn create_state(&self) -> Box { 46 | Box::new(()) 47 | } 48 | 49 | default fn mount(&self, _: &RawBuildCx) {} 50 | 51 | default fn unmount(&self, _: &RawBuildCx) {} 52 | } 53 | 54 | impl WidgetStateOS for T { 55 | fn state_type_id(&self) -> TypeId { 56 | TypeId::of::() 57 | } 58 | 59 | fn create_state(&self) -> Box { 60 | Box::new(T::create_state(&self)) 61 | } 62 | 63 | fn mount(&self, cx: &RawBuildCx) { 64 | let cx = unsafe { std::mem::transmute::<&RawBuildCx, &_BuildCx>(cx) }; 65 | 66 | T::mount(&self, cx) 67 | } 68 | 69 | fn unmount(&self, cx: &RawBuildCx) { 70 | let cx = unsafe { std::mem::transmute::<&RawBuildCx, &_BuildCx>(cx) }; 71 | 72 | T::unmount(&self, cx) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::tree::Node; 2 | 3 | pub mod build_cx; 4 | pub mod render; 5 | 6 | /// This context allows to access [`_BuildCx`] from anywhere in the build 7 | /// method, while making sure that given reference is valid for reads for the 8 | /// lifetime of that widget. 9 | /// 10 | /// Usually we would be able to just pass a simple structure which implements 11 | /// clone (e.g. `Rc`), but doing it that way makes it impossible to access 12 | /// context from multiple closures at once (since that context argument has to 13 | /// move). To fix this you could clone context for every single closure, but 14 | /// that gets tedious very fast. 15 | /// 16 | /// Instead we borrow that context ourselves, allowing consumers to share a 17 | /// single `cx` between every closure that appears in the build method (of which 18 | /// lifetime is <= widget node). 19 | /// 20 | /// [`_BuildCx`]: build_cx::_BuildCx 21 | #[repr(transparent)] 22 | pub struct RawBuildCx { 23 | pub(crate) _node: Node, 24 | } 25 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/ext.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefMut}, 3 | marker::PhantomData, 4 | ops::{Deref, DerefMut}, 5 | sync::atomic::Ordering, 6 | }; 7 | 8 | use crate::{ 9 | api::contexts::build_cx::{StateGuard, StateGuardMut, STATE_UPDATE_SUPRESSED}, 10 | app::tree::NodeRef, 11 | prelude::{Widget, WidgetState}, 12 | }; 13 | 14 | use super::{RenderState, Size}; 15 | 16 | pub trait RenderExt { 17 | #[doc(hidden)] 18 | fn node(&self) -> &NodeRef; 19 | 20 | fn widget_state(&self) -> StateGuard 21 | where 22 | W: WidgetState, 23 | { 24 | StateGuard { 25 | guard: Ref::map(self.node().borrow(), |node| node.state.deref()), 26 | _p: PhantomData, 27 | } 28 | } 29 | 30 | fn widget_state_mut(&self) -> StateGuardMut 31 | where 32 | W: WidgetState, 33 | { 34 | if !STATE_UPDATE_SUPRESSED.load(Ordering::SeqCst) { 35 | self.node().mark_dirty(); 36 | } 37 | 38 | StateGuardMut { 39 | guard: RefMut::map(self.node().borrow_mut(), |node| node.state.deref_mut()), 40 | _p: PhantomData, 41 | } 42 | } 43 | 44 | fn render_state(&self) -> Ref 45 | where 46 | W: RenderState, 47 | { 48 | Ref::map(self.node().borrow(), |node| { 49 | node.render_data.state.deref().downcast_ref().unwrap() 50 | }) 51 | } 52 | 53 | fn render_state_mut(&self) -> RefMut 54 | where 55 | W: RenderState, 56 | { 57 | RefMut::map(self.node().borrow_mut(), |node| { 58 | node.render_data.state.deref_mut().downcast_mut().unwrap() 59 | }) 60 | } 61 | } 62 | 63 | pub trait RenderOSExt { 64 | #[doc(hidden)] 65 | fn node(&self) -> &NodeRef; 66 | 67 | fn size(&self) -> Size { 68 | self.node().borrow().render_data.size 69 | } 70 | 71 | fn set_parent_data(&self, data: T) { 72 | self.node().borrow_mut().render_data.parent_data = Box::new(data); 73 | } 74 | 75 | fn try_parent_data(&self) -> Option> { 76 | // Check parent data type early. 77 | self.node() 78 | .borrow() 79 | .render_data 80 | .parent_data 81 | .downcast_ref::()?; 82 | 83 | Some(Ref::map(self.node().borrow(), |node| { 84 | node.render_data.parent_data.downcast_ref().unwrap() 85 | })) 86 | } 87 | 88 | fn try_parent_data_mut(&self) -> Option> { 89 | // Check parent data type early. 90 | self.node() 91 | .borrow_mut() 92 | .render_data 93 | .parent_data 94 | .downcast_mut::()?; 95 | 96 | Some(RefMut::map(self.node().borrow_mut(), |node| { 97 | node.render_data.parent_data.downcast_mut().unwrap() 98 | })) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod ext; 2 | mod paint_cx; 3 | mod parent_data; 4 | mod render_cx; 5 | mod render_state; 6 | mod types; 7 | 8 | pub use ext::*; 9 | pub use paint_cx::*; 10 | pub use parent_data::*; 11 | pub use render_cx::*; 12 | pub use render_state::*; 13 | pub use types::*; 14 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/paint_cx.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{ 4 | app::{runner::Canvas, tree::NodeRef}, 5 | prelude::Widget, 6 | }; 7 | 8 | use super::{ext::RenderExt, Offset, RenderOSExt}; 9 | 10 | pub struct PaintCx { 11 | cx: PaintCxOS, 12 | _p: PhantomData, 13 | } 14 | 15 | impl PaintCx { 16 | pub fn new(cx: PaintCxOS) -> Self { 17 | Self { 18 | cx, 19 | _p: PhantomData, 20 | } 21 | } 22 | } 23 | 24 | impl RenderExt for PaintCx { 25 | fn node(&self) -> &NodeRef { 26 | &self.node 27 | } 28 | } 29 | 30 | impl std::ops::Deref for PaintCx { 31 | type Target = PaintCxOS; 32 | 33 | fn deref(&self) -> &Self::Target { 34 | &self.cx 35 | } 36 | } 37 | 38 | impl std::ops::DerefMut for PaintCx { 39 | fn deref_mut(&mut self) -> &mut Self::Target { 40 | &mut self.cx 41 | } 42 | } 43 | 44 | #[derive(Clone)] 45 | pub struct PaintCxOS { 46 | node: NodeRef, 47 | // Following are used to correctly register local transformation of the 48 | // offset. It is used to automatically transform point during hit testing. 49 | /// (global) 50 | offset: Offset, 51 | /// (global) 52 | parent_offset: Offset, 53 | } 54 | 55 | impl RenderOSExt for PaintCxOS { 56 | fn node(&self) -> &NodeRef { 57 | &self.node 58 | } 59 | } 60 | 61 | impl PaintCxOS { 62 | pub(crate) fn new(node: NodeRef) -> Self { 63 | Self { 64 | node, 65 | offset: Offset::default(), 66 | parent_offset: Offset::default(), 67 | } 68 | } 69 | 70 | pub fn paint(&mut self, piet: &mut Canvas, offset: &Offset) { 71 | assert!( 72 | self.node.borrow().render_data.laid_out, 73 | "child was not laid out before paint" 74 | ); 75 | 76 | // Used to calculate local offset of self (see Drop impl). 77 | self.offset = offset.clone(); 78 | 79 | // Update local offset of this node. 80 | let local_offset = *offset - self.parent_offset; 81 | self.node.borrow_mut().render_data.local_offset = local_offset; 82 | 83 | self.node.widget().paint(self.clone(), piet, offset); 84 | } 85 | 86 | #[track_caller] 87 | pub fn child(&mut self, index: usize) -> PaintCxOS { 88 | let child = self 89 | .node 90 | .child(index) 91 | .expect("specified node didn't have child at that index"); 92 | 93 | PaintCxOS { 94 | node: child, 95 | offset: Offset::default(), 96 | parent_offset: self.offset.clone(), 97 | } 98 | } 99 | 100 | pub fn children<'a>(&'a mut self) -> impl Iterator + 'a { 101 | self.node.children().into_iter().map(|child| PaintCxOS { 102 | node: child, 103 | offset: Offset::default(), 104 | parent_offset: self.offset.clone(), 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/parent_data.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use frui_macros::sealed; 4 | 5 | pub trait ParentData { 6 | type Data: 'static; 7 | 8 | fn create_data(&self) -> Self::Data; 9 | } 10 | 11 | #[sealed(crate)] 12 | pub trait ParentDataOS { 13 | fn create_parent_data(&self) -> Box; 14 | } 15 | 16 | impl ParentDataOS for T { 17 | default fn create_parent_data(&self) -> Box { 18 | Box::new(()) 19 | } 20 | } 21 | 22 | impl ParentDataOS for T { 23 | fn create_parent_data(&self) -> Box { 24 | Box::new(::create_data(&self)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/render_cx.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use druid_shell::IdleToken; 4 | 5 | use super::{ 6 | ext::{RenderExt, RenderOSExt}, 7 | Constraints, Size, 8 | }; 9 | 10 | use crate::{ 11 | app::{runner::window_handler::APP_HANDLE, tree::NodeRef}, 12 | prelude::{InheritedState, InheritedWidget, Widget, WidgetState}, 13 | }; 14 | 15 | pub struct LayoutCx { 16 | cx: LayoutCxOS, 17 | _p: PhantomData, 18 | } 19 | 20 | impl LayoutCx { 21 | pub(crate) fn new(any: LayoutCxOS) -> Self { 22 | Self { 23 | cx: any, 24 | _p: PhantomData, 25 | } 26 | } 27 | 28 | pub fn depend_on_inherited_widget(&self) -> Option> 29 | where 30 | W: InheritedWidget + WidgetState, 31 | { 32 | // Register and get inherited widget of specified key. 33 | let node = self 34 | .node 35 | .depend_on_inherited_widget_of_key::()?; 36 | 37 | Some(InheritedState { 38 | node, 39 | _p: PhantomData, 40 | }) 41 | } 42 | } 43 | 44 | impl RenderExt for LayoutCx { 45 | fn node(&self) -> &NodeRef { 46 | &self.cx.node 47 | } 48 | } 49 | 50 | impl std::ops::Deref for LayoutCx { 51 | type Target = LayoutCxOS; 52 | 53 | fn deref(&self) -> &Self::Target { 54 | &self.cx 55 | } 56 | } 57 | 58 | impl std::ops::DerefMut for LayoutCx { 59 | fn deref_mut(&mut self) -> &mut Self::Target { 60 | &mut self.cx 61 | } 62 | } 63 | 64 | #[derive(Clone)] 65 | pub struct LayoutCxOS { 66 | node: NodeRef, 67 | } 68 | 69 | impl RenderOSExt for LayoutCxOS { 70 | fn node(&self) -> &NodeRef { 71 | &self.node 72 | } 73 | } 74 | 75 | impl LayoutCxOS { 76 | pub(crate) fn new(node: NodeRef) -> Self { 77 | Self { node } 78 | } 79 | 80 | pub fn layout(&self, constraints: Constraints) -> Size { 81 | let widget = self.node.widget(); 82 | 83 | let size = widget.layout(self.clone(), constraints); 84 | 85 | if cfg!(debug_assertions) { 86 | if size > constraints.biggest() { 87 | if widget.debug_name_short() != "DebugContainer" { 88 | log::warn!("`{}` overflowed", widget.debug_name_short()); 89 | } 90 | } 91 | } 92 | 93 | let render_data = &mut self.node.borrow_mut().render_data; 94 | 95 | render_data.size = size; 96 | render_data.laid_out = true; 97 | render_data.constraints = constraints; 98 | 99 | size 100 | } 101 | 102 | pub fn child(&self, index: usize) -> LayoutCxOS { 103 | self.try_child(index) 104 | .expect("specified node didn't have any children") 105 | } 106 | 107 | pub fn children(&self) -> LayoutCxIter { 108 | LayoutCxIter { 109 | child_idx: 0, 110 | parent_cx: self, 111 | } 112 | } 113 | 114 | fn try_child(&self, index: usize) -> Option { 115 | let child = self.node.child(index)?; 116 | 117 | Some(LayoutCxOS::new(child)) 118 | } 119 | 120 | pub fn schedule_layout(&mut self) { 121 | APP_HANDLE.with(|handle| { 122 | handle 123 | .borrow_mut() 124 | .as_mut() 125 | .expect("APP_HANDLE wasn't set") 126 | .schedule_idle(IdleToken::new(0)); 127 | }); 128 | } 129 | } 130 | 131 | pub struct LayoutCxIter<'a> { 132 | child_idx: usize, 133 | parent_cx: &'a LayoutCxOS, 134 | } 135 | 136 | impl<'a> LayoutCxIter<'a> { 137 | pub fn len(&self) -> usize { 138 | self.parent_cx.node.children().len() 139 | } 140 | } 141 | 142 | impl Clone for LayoutCxIter<'_> { 143 | fn clone(&self) -> Self { 144 | Self { 145 | // Reset iterator. 146 | child_idx: 0, 147 | parent_cx: self.parent_cx, 148 | } 149 | } 150 | } 151 | 152 | impl<'a> Iterator for LayoutCxIter<'a> { 153 | type Item = LayoutCxOS; 154 | 155 | fn next(&mut self) -> Option { 156 | let r = self.parent_cx.try_child(self.child_idx); 157 | self.child_idx += 1; 158 | r 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/render_state.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use frui_macros::sealed; 4 | 5 | pub trait RenderState { 6 | type State: 'static; 7 | 8 | fn create_state(&self) -> Self::State; 9 | } 10 | 11 | #[sealed(crate)] 12 | pub trait RenderStateOS { 13 | fn create_render_state(&self) -> Box; 14 | } 15 | 16 | impl RenderStateOS for T { 17 | default fn create_render_state(&self) -> Box { 18 | Box::new(()) 19 | } 20 | } 21 | 22 | impl RenderStateOS for T { 23 | fn create_render_state(&self) -> Box { 24 | Box::new(T::create_state(self)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/types/constraints.rs: -------------------------------------------------------------------------------- 1 | //! Adapted from Flutter. 2 | 3 | use super::Size; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq)] 6 | pub struct Constraints { 7 | pub min_width: f64, 8 | pub max_width: f64, 9 | pub min_height: f64, 10 | pub max_height: f64, 11 | } 12 | 13 | impl Constraints { 14 | pub const ZERO: Constraints = Constraints { 15 | min_width: 0.0, 16 | max_width: 0.0, 17 | min_height: 0.0, 18 | max_height: 0.0, 19 | }; 20 | 21 | pub fn new(min_width: f64, max_width: f64, min_height: f64, max_height: f64) -> Self { 22 | Self { 23 | min_width, 24 | max_width, 25 | min_height, 26 | max_height, 27 | } 28 | } 29 | 30 | pub fn new_loose(size: Size) -> Self { 31 | Self { 32 | min_width: 0.0, 33 | max_width: size.width, 34 | min_height: 0.0, 35 | max_height: size.height, 36 | } 37 | } 38 | 39 | pub fn new_tight(size: Size) -> Self { 40 | Self { 41 | min_width: size.width, 42 | max_width: size.width, 43 | min_height: size.height, 44 | max_height: size.height, 45 | } 46 | } 47 | 48 | pub fn new_tight_for(width: Option, height: Option) -> Self { 49 | Self { 50 | min_width: width.unwrap_or(0.0), 51 | max_width: width.unwrap_or(f64::INFINITY), 52 | min_height: height.unwrap_or(0.0), 53 | max_height: height.unwrap_or(f64::INFINITY), 54 | } 55 | } 56 | 57 | /// Returns new constraints that remove the minimum width and height 58 | /// requirements. 59 | pub fn loosen(&self) -> Self { 60 | Self { 61 | min_width: 0.0, 62 | max_width: self.max_width, 63 | min_height: 0.0, 64 | max_height: self.max_height, 65 | } 66 | } 67 | 68 | /// Returns new tight constraints with width and/or height as close to the 69 | /// given width and height as possible while still respecting the original 70 | /// constraints. 71 | #[rustfmt::skip] 72 | pub fn tighten(&self, width: Option, height: Option) -> Self { 73 | Self { 74 | min_width: width.map_or(self.min_width, |v| v.clamp(self.min_width, self.max_width)), 75 | max_width: width.map_or(self.max_width, |v| v.clamp(self.min_width, self.max_width)), 76 | min_height: height.map_or(self.min_height, |v| v.clamp(self.min_height, self.max_height)), 77 | max_height: height.map_or(self.max_height, |v| v.clamp(self.min_height, self.max_height)), 78 | } 79 | } 80 | 81 | /// Returns new constraints that respect the given constraints while being 82 | /// as close as possible to the original constraints. 83 | #[rustfmt::skip] 84 | pub fn enforce(&self, constraints: Constraints) -> Self { 85 | Self { 86 | min_width: self.min_width.clamp(constraints.min_width, constraints.max_width), 87 | max_width: self.max_width.clamp(constraints.min_width, constraints.max_width), 88 | min_height: self.min_height.clamp(constraints.min_height, constraints.max_height), 89 | max_height: self.max_height.clamp(constraints.min_height, constraints.max_height), 90 | } 91 | } 92 | 93 | /// The smallest size that satisfies the constraints. 94 | pub fn smallest(&self) -> Size { 95 | Size::new(self.min_width, self.min_height) 96 | } 97 | 98 | /// The biggest size that satisfies the constraints. 99 | pub fn biggest(&self) -> Size { 100 | Size::new(self.max_width, self.max_height) 101 | } 102 | 103 | /// Returns the size that both satisfies the constraints and is as close as 104 | /// possible to the given size. 105 | pub fn constrain(&self, size: Size) -> Size { 106 | Size { 107 | width: self.constrain_width(size.width), 108 | height: self.constrain_height(size.height), 109 | } 110 | } 111 | 112 | /// Returns the width that both satisfies the constraints and is as close as 113 | /// possible to the given width. 114 | pub fn constrain_width(&self, width: f64) -> f64 { 115 | width.clamp(self.min_width, self.max_width) 116 | } 117 | 118 | /// Returns the height that both satisfies the constraints and is as close as 119 | /// possible to the given height. 120 | pub fn constrain_height(&self, height: f64) -> f64 { 121 | height.clamp(self.min_height, self.max_height) 122 | } 123 | 124 | pub fn has_tight_width(&self) -> bool { 125 | self.min_width >= self.max_width 126 | } 127 | 128 | pub fn has_tight_height(&self) -> bool { 129 | self.min_height >= self.max_height 130 | } 131 | 132 | pub fn is_tight(&self) -> bool { 133 | self.has_tight_width() && self.has_tight_height() 134 | } 135 | 136 | pub fn has_bounded_width(&self) -> bool { 137 | self.max_width < f64::INFINITY 138 | } 139 | 140 | pub fn has_bounded_height(&self) -> bool { 141 | self.max_height < f64::INFINITY 142 | } 143 | 144 | pub fn has_infinite_width(&self) -> bool { 145 | self.min_width >= f64::INFINITY 146 | } 147 | 148 | pub fn has_infinite_height(&self) -> bool { 149 | self.min_height >= f64::INFINITY 150 | } 151 | 152 | pub fn is_satisfied_by(&self, size: Size) -> bool { 153 | (self.min_width..=self.max_width).contains(&size.width) 154 | && (self.min_height..=self.max_height).contains(&size.height) 155 | } 156 | } 157 | 158 | impl Default for Constraints { 159 | fn default() -> Self { 160 | Self { 161 | min_width: 0.0, 162 | max_width: f64::INFINITY, 163 | min_height: 0.0, 164 | max_height: f64::INFINITY, 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod constraints; 2 | mod geometry; 3 | mod offset; 4 | mod size; 5 | 6 | pub use constraints::*; 7 | pub use geometry::*; 8 | pub use offset::*; 9 | pub use size::*; 10 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/types/offset.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Mul, Sub}; 2 | 3 | use druid_shell::kurbo::Point; 4 | 5 | use super::Size; 6 | 7 | #[derive(Debug, Clone, Copy, Default, PartialEq)] 8 | pub struct Offset { 9 | pub x: f64, 10 | pub y: f64, 11 | } 12 | 13 | impl Offset { 14 | pub fn new(x: f64, y: f64) -> Self { 15 | Offset { x, y } 16 | } 17 | } 18 | 19 | impl Add for Offset { 20 | type Output = Self; 21 | 22 | fn add(self, rhs: Self) -> Self::Output { 23 | Self { 24 | x: self.x + rhs.x, 25 | y: self.y + rhs.y, 26 | } 27 | } 28 | } 29 | 30 | impl Sub for Offset { 31 | type Output = Self; 32 | 33 | fn sub(self, rhs: Self) -> Self::Output { 34 | Self { 35 | x: self.x - rhs.x, 36 | y: self.y - rhs.y, 37 | } 38 | } 39 | } 40 | 41 | impl Mul for Offset { 42 | type Output = Self; 43 | 44 | fn mul(self, rhs: f64) -> Self::Output { 45 | Self { 46 | x: self.x * rhs, 47 | y: self.y * rhs, 48 | } 49 | } 50 | } 51 | 52 | impl From for Offset { 53 | fn from(size: Size) -> Self { 54 | Self { 55 | x: size.width, 56 | y: size.height, 57 | } 58 | } 59 | } 60 | 61 | impl From for Point { 62 | fn from(offset: Offset) -> Self { 63 | Point { 64 | x: offset.x, 65 | y: offset.y, 66 | } 67 | } 68 | } 69 | 70 | impl From<&Offset> for Point { 71 | fn from(offset: &Offset) -> Self { 72 | Point { 73 | x: offset.x, 74 | y: offset.y, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/contexts/render/types/size.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 2 | 3 | use druid_shell::kurbo::Point; 4 | 5 | #[derive(Debug, Clone, Copy, Default)] 6 | pub struct Size { 7 | pub width: f64, 8 | pub height: f64, 9 | } 10 | 11 | impl Size { 12 | pub const ZERO: Size = Size { 13 | width: 0.0, 14 | height: 0.0, 15 | }; 16 | 17 | pub fn new(width: f64, height: f64) -> Self { 18 | Self { width, height } 19 | } 20 | 21 | pub fn aspect_ratio(&self) -> f64 { 22 | self.width / self.height 23 | } 24 | 25 | pub fn contains(&self, point: Point) -> bool { 26 | point.x >= 0. && point.y >= 0. && point.x <= self.width && point.y <= self.height 27 | } 28 | } 29 | 30 | impl std::fmt::Display for Size { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "({}x{})", self.width, self.height) 33 | } 34 | } 35 | 36 | impl Add for Size { 37 | type Output = Self; 38 | 39 | fn add(self, rhs: Self) -> Self::Output { 40 | Self { 41 | width: self.width + rhs.width, 42 | height: self.height + rhs.height, 43 | } 44 | } 45 | } 46 | 47 | impl Sub for Size { 48 | type Output = Self; 49 | 50 | fn sub(self, rhs: Self) -> Self::Output { 51 | Self { 52 | width: self.width - rhs.width, 53 | height: self.height - rhs.height, 54 | } 55 | } 56 | } 57 | 58 | impl AddAssign for Size { 59 | fn add_assign(&mut self, rhs: Self) { 60 | self.width += rhs.width; 61 | self.height += rhs.height; 62 | } 63 | } 64 | 65 | impl SubAssign for Size { 66 | fn sub_assign(&mut self, rhs: Self) { 67 | self.width -= rhs.width; 68 | self.height -= rhs.height; 69 | } 70 | } 71 | 72 | impl PartialEq for Size { 73 | fn eq(&self, other: &Self) -> bool { 74 | self.width == other.width && self.height == other.height 75 | } 76 | } 77 | 78 | impl PartialOrd for Size { 79 | fn partial_cmp(&self, _: &Self) -> Option { 80 | None 81 | } 82 | 83 | fn lt(&self, other: &Self) -> bool { 84 | self.width < other.width || self.height < other.height 85 | } 86 | 87 | fn le(&self, other: &Self) -> bool { 88 | self.width <= other.width || self.height <= other.height 89 | } 90 | 91 | fn gt(&self, other: &Self) -> bool { 92 | self.width > other.width || self.height > other.height 93 | } 94 | 95 | fn ge(&self, other: &Self) -> bool { 96 | self.width >= other.width || self.height >= other.height 97 | } 98 | } 99 | 100 | impl From for druid_shell::kurbo::Size { 101 | fn from(size: Size) -> Self { 102 | Self { 103 | width: size.width, 104 | height: size.height, 105 | } 106 | } 107 | } 108 | 109 | impl From for Size { 110 | fn from(size: druid_shell::kurbo::Size) -> Self { 111 | Self { 112 | width: size.width, 113 | height: size.height, 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/implementers/inherited.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use crate::{ 4 | api::{IntoWidgetPtr, WidgetPtr}, 5 | render::*, 6 | }; 7 | 8 | use super::{InheritedWidgetOS, WidgetDerive}; 9 | 10 | pub trait InheritedWidget: WidgetDerive + Sized { 11 | fn build<'w>(&'w self) -> Self::Widget<'w>; 12 | } 13 | 14 | impl InheritedWidgetOS for T { 15 | fn build<'w>(&'w self, _: &'w crate::api::contexts::RawBuildCx) -> Vec> { 16 | vec![T::build(self).into_widget_ptr()] 17 | } 18 | 19 | fn layout<'w>(&'w self, cx: LayoutCxOS, constraints: Constraints) -> Size { 20 | cx.child(0).layout(constraints) 21 | } 22 | 23 | fn paint<'w>(&'w self, mut cx: PaintCxOS, canvas: &mut Canvas, offset: &Offset) { 24 | cx.child(0).paint(canvas, offset) 25 | } 26 | 27 | fn inherited_key(&self) -> Option { 28 | Some(TypeId::of::()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/implementers/mod.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use frui_macros::copy_trait_as; 4 | 5 | use crate::render::*; 6 | 7 | use super::{ 8 | any_ext::AnyExt, 9 | contexts::{ 10 | build_cx::widget_state::WidgetStateOS, 11 | render::{ParentDataOS, RenderStateOS}, 12 | RawBuildCx, 13 | }, 14 | local_key::WidgetLocalKey, 15 | pointer_events::HitTestOS, 16 | structural_eq::StructuralEqOS, 17 | WidgetDebug, WidgetPtr, WidgetUniqueType, 18 | }; 19 | 20 | pub(crate) mod inherited; 21 | pub(crate) mod render; 22 | pub(crate) mod view; 23 | 24 | /// This trait can be implemented using `#[derive(WidgetKind)]`. 25 | pub trait WidgetDerive { 26 | /// Specifies the exact type of child/children of a given widget. It is 27 | /// automatically being inferred from the context using the TAIT feature. 28 | type Widget<'a>: super::Widget 29 | where 30 | Self: 'a; 31 | 32 | /// Implementation should make sure [`TypeId`] of this type is unique for 33 | /// given structure definition, even if that structure contains generic 34 | /// parameters. This is used to preserve state between generic widgets. 35 | #[doc(hidden)] 36 | type UniqueTypeId: 'static; 37 | } 38 | 39 | /// Object safe implementation for each widget kind. For example, `ViewWidget` 40 | /// has its matching `ViewWidgetOS`. 41 | /// 42 | /// Those implemetations are routed through `RawWidget` using the derive macro 43 | /// and accessed by framework through `&dyn RawWidget`. 44 | /// 45 | /// ## `RawWidget` 46 | /// 47 | /// `RawWidget` is the base trait containing all methods the framework needs. 48 | /// All `OS` widget implementations are routed through this trait (by the derive 49 | /// macro) and are accessed by the framework through `&dyn RawWidget`. 50 | #[doc(hidden)] 51 | #[copy_trait_as(RawWidget, ViewWidgetOS, InheritedWidgetOS, RenderWidgetOS)] 52 | pub trait OS: 53 | WidgetStateOS 54 | + RenderStateOS 55 | + ParentDataOS 56 | + WidgetLocalKey 57 | + WidgetUniqueType 58 | + WidgetDebug 59 | + HitTestOS 60 | + StructuralEqOS 61 | + AnyExt 62 | { 63 | fn build<'w>(&'w self, cx: &'w RawBuildCx) -> Vec>; 64 | 65 | fn layout(&self, cx: LayoutCxOS, constraints: Constraints) -> Size; 66 | 67 | fn paint(&self, cx: PaintCxOS, canvas: &mut Canvas, offset: &Offset); 68 | 69 | fn inherited_key(&self) -> Option { 70 | None 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/implementers/render.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{contexts::build_cx::_BuildCx, IntoWidgetPtr, WidgetPtr}, 3 | macro_exports::RawBuildCx, 4 | prelude::BuildCx, 5 | render::*, 6 | }; 7 | 8 | use super::{RenderWidgetOS, WidgetDerive}; 9 | 10 | pub trait RenderWidget: WidgetDerive + Sized { 11 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Vec>; 12 | 13 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size; 14 | 15 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset); 16 | } 17 | 18 | impl RenderWidgetOS for T { 19 | fn build<'w>(&'w self, cx: &'w RawBuildCx) -> Vec> { 20 | let cx = unsafe { std::mem::transmute::<&RawBuildCx, &_BuildCx>(cx) }; 21 | 22 | T::build(&self, cx) 23 | .into_iter() 24 | .map(|w| w.into_widget_ptr()) 25 | .collect() 26 | } 27 | 28 | fn layout(&self, cx: LayoutCxOS, constraints: Constraints) -> Size { 29 | let cx = &>::new(cx); 30 | 31 | T::layout(&self, cx, constraints) 32 | } 33 | 34 | fn paint(&self, cx: PaintCxOS, canvas: &mut Canvas, offset: &Offset) { 35 | let cx = &mut >::new(cx); 36 | 37 | T::paint(self, cx, canvas, offset); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/implementers/view.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | contexts::{ 4 | build_cx::{BuildCx, _BuildCx}, 5 | RawBuildCx, 6 | }, 7 | IntoWidgetPtr, WidgetPtr, 8 | }, 9 | render::*, 10 | }; 11 | 12 | use super::{ViewWidgetOS, WidgetDerive}; 13 | 14 | pub trait ViewWidget: WidgetDerive + Sized { 15 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w>; 16 | } 17 | 18 | impl ViewWidgetOS for T { 19 | fn build<'w>(&'w self, cx: &'w RawBuildCx) -> Vec> { 20 | let cx = unsafe { std::mem::transmute::<&RawBuildCx, &_BuildCx>(cx) }; 21 | 22 | vec![T::build(&self, cx).into_widget_ptr()] 23 | } 24 | 25 | fn layout<'w>(&self, cx: LayoutCxOS, constraints: Constraints) -> Size { 26 | cx.child(0).layout(constraints) 27 | } 28 | 29 | fn paint<'w>(&'w self, mut cx: PaintCxOS, canvas: &mut Canvas, offset: &Offset) { 30 | cx.child(0).paint(canvas, offset) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/impls.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use crate::render::*; 4 | use crate::{api::Widget, macro_exports::RenderWidgetOS, prelude::*}; 5 | 6 | use super::implementers::{RawWidget, WidgetDerive}; 7 | 8 | pub trait BoxedWidget: Widget + Sized { 9 | /// Convenience method used to type erase and box a widget. 10 | fn boxed<'a>(self) -> Box 11 | where 12 | Self: 'a; 13 | } 14 | 15 | impl BoxedWidget for T { 16 | default fn boxed<'a>(self) -> Box 17 | where 18 | Self: 'a, 19 | { 20 | Box::new(self) 21 | } 22 | } 23 | 24 | impl BoxedWidget for Box { 25 | fn boxed<'a>(self) -> Box 26 | where 27 | Self: 'a, 28 | { 29 | // Avoid double boxing. 30 | self 31 | } 32 | } 33 | 34 | // 35 | // Implementations 36 | // 37 | 38 | impl Widget for &T { 39 | fn as_raw(&self) -> &dyn RawWidget { 40 | T::as_raw(*self) 41 | } 42 | } 43 | 44 | impl Widget for &mut T { 45 | fn as_raw(&self) -> &dyn RawWidget { 46 | T::as_raw(*self) 47 | } 48 | } 49 | 50 | impl<'a> Widget for &'a dyn Widget { 51 | fn as_raw(&self) -> &dyn RawWidget { 52 | self.deref().as_raw() 53 | } 54 | } 55 | 56 | impl<'a> Widget for Box { 57 | fn as_raw(&self) -> &dyn RawWidget { 58 | self.deref().as_raw() 59 | } 60 | } 61 | 62 | impl Widget for Box { 63 | fn as_raw(&self) -> &dyn RawWidget { 64 | self.deref().as_raw() 65 | } 66 | } 67 | 68 | impl_widget_os_deref!(impl RawWidget for &T); 69 | impl_widget_os_deref!(impl RawWidget for &mut T); 70 | impl_widget_os_deref!(impl RawWidget for Box); 71 | impl_widget_os_deref!(impl<'a> RawWidget for &'a dyn Widget); 72 | impl_widget_os_deref!(impl<'a> RawWidget for Box); 73 | 74 | // 75 | // Unit type implementation 76 | // 77 | 78 | #[doc(hidden)] 79 | pub enum Unique {} 80 | 81 | impl Widget for () { 82 | fn as_raw(&self) -> &dyn RawWidget { 83 | self 84 | } 85 | } 86 | 87 | impl WidgetDerive for () { 88 | type Widget<'a> = (); 89 | 90 | type UniqueTypeId = Unique; 91 | } 92 | 93 | impl RenderWidget for () { 94 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 95 | vec![] 96 | } 97 | 98 | fn layout(&self, _: &LayoutCx, c: Constraints) -> Size { 99 | c.smallest() 100 | } 101 | 102 | fn paint(&self, _: &mut PaintCx, _: &mut Canvas, _: &Offset) {} 103 | } 104 | 105 | impl RawWidget for () { 106 | fn build<'w>(&'w self, cx: &'w super::contexts::RawBuildCx) -> Vec> { 107 | ::build(self, cx) 108 | } 109 | 110 | fn layout<'w>(&self, cx: LayoutCxOS, constraints: Constraints) -> Size { 111 | ::layout(self, cx, constraints) 112 | } 113 | 114 | fn paint<'w>(&'w self, cx: PaintCxOS, canvas: &mut Canvas, offset: &Offset) { 115 | ::paint(self, cx, canvas, offset) 116 | } 117 | 118 | fn inherited_key(&self) -> Option { 119 | ::inherited_key(self) 120 | } 121 | } 122 | 123 | macro_rules! impl_widget_os_deref_ { 124 | ($($impl:tt)*) => { 125 | $($impl)* { 126 | fn build<'w>(&'w self, cx: &'w super::contexts::RawBuildCx) -> Vec> { 127 | self.deref().build(cx) 128 | } 129 | 130 | fn layout<'w>( 131 | &self, 132 | cx: LayoutCxOS, 133 | constraints: Constraints, 134 | ) -> Size { 135 | self.deref().layout(cx, constraints) 136 | } 137 | 138 | fn paint<'w>( 139 | &'w self, 140 | cx: PaintCxOS, 141 | canvas: &mut Canvas, 142 | offset: &Offset, 143 | ) { 144 | self.deref().paint(cx, canvas, offset) 145 | } 146 | 147 | 148 | fn inherited_key(&self) -> Option { 149 | self.deref().inherited_key() 150 | } 151 | } 152 | 153 | }; 154 | } 155 | 156 | pub(self) use impl_widget_os_deref_ as impl_widget_os_deref; 157 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/local_key.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | use crate::prelude::{BuildCx, ViewWidget, Widget}; 4 | 5 | /// LocalKey is a widget that allows you to annotate the key for a `child` 6 | /// widget. 7 | #[derive(ViewWidget)] 8 | pub struct LocalKey { 9 | pub key: K, 10 | pub child: W, 11 | } 12 | 13 | impl LocalKey { 14 | pub fn new(key: K, child: W) -> Self { 15 | Self { key, child } 16 | } 17 | } 18 | 19 | impl ViewWidget for LocalKey { 20 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 21 | &self.child 22 | } 23 | } 24 | 25 | pub struct LocalKeyAny<'a> { 26 | key: &'a dyn PartialEqAny, 27 | } 28 | 29 | impl PartialEq for LocalKeyAny<'_> { 30 | fn eq(&self, other: &Self) -> bool { 31 | self.key.eq(other.key) 32 | } 33 | } 34 | 35 | trait PartialEqAny: Any { 36 | fn type_id(&self) -> TypeId; 37 | fn eq(&self, other: &dyn PartialEqAny) -> bool; 38 | } 39 | 40 | impl PartialEqAny for T { 41 | fn type_id(&self) -> TypeId { 42 | TypeId::of::() 43 | } 44 | 45 | fn eq(&self, other: &dyn PartialEqAny) -> bool { 46 | if TypeId::of::() == PartialEqAny::type_id(other) { 47 | // Safety: T is 'static and types match. 48 | unsafe { return self.eq(&*(other as *const _ as *const T)) } 49 | } else { 50 | false 51 | } 52 | } 53 | } 54 | 55 | // 56 | // 57 | 58 | pub trait WidgetLocalKey { 59 | fn local_key(&self) -> Option; 60 | } 61 | 62 | impl WidgetLocalKey for T { 63 | default fn local_key(&self) -> Option { 64 | None 65 | } 66 | } 67 | 68 | impl WidgetLocalKey for LocalKey { 69 | fn local_key(&self) -> Option { 70 | Some(LocalKeyAny { key: &self.key }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use self::implementers::{RawWidget, WidgetDerive}; 4 | 5 | pub(crate) mod any_ext; 6 | pub(crate) mod contexts; 7 | pub(crate) mod implementers; 8 | pub(crate) mod impls; 9 | pub(crate) mod local_key; 10 | pub(crate) mod pointer_events; 11 | pub(crate) mod structural_eq; 12 | pub(crate) mod widget_ptr; 13 | 14 | pub use widget_ptr::{IntoWidgetPtr, WidgetPtr}; 15 | 16 | pub trait Widget: RawWidget { 17 | fn as_raw(&self) -> &dyn RawWidget; 18 | } 19 | 20 | pub trait WidgetUniqueType { 21 | fn unique_type(&self) -> TypeId; 22 | } 23 | 24 | impl WidgetUniqueType for T { 25 | default fn unique_type(&self) -> TypeId { 26 | unreachable!() 27 | } 28 | } 29 | 30 | impl WidgetUniqueType for T { 31 | fn unique_type(&self) -> TypeId { 32 | std::any::TypeId::of::() 33 | } 34 | } 35 | 36 | pub trait WidgetDebug { 37 | fn debug_name(&self) -> &'static str; 38 | fn debug_name_short(&self) -> &'static str; 39 | } 40 | 41 | impl WidgetDebug for T { 42 | default fn debug_name(&self) -> &'static str { 43 | let full_name = std::any::type_name::(); 44 | full_name 45 | } 46 | 47 | fn debug_name_short(&self) -> &'static str { 48 | let full_name = std::any::type_name::(); 49 | 50 | let mut start = 0; 51 | let mut end = full_name.len(); 52 | 53 | for (n, char) in full_name.chars().enumerate() { 54 | if char == '<' { 55 | end = n; 56 | break; 57 | } else if char == ':' { 58 | start = n + 1; 59 | } 60 | } 61 | 62 | &full_name[start..end] 63 | } 64 | } -------------------------------------------------------------------------------- /crates/frui_core/src/api/pointer_events/context.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use druid_shell::kurbo::{Affine, Point}; 4 | 5 | use crate::app::tree::pointer_handler::HitTestEntries; 6 | use crate::app::tree::NodeRef; 7 | use crate::prelude::Widget; 8 | use crate::render::*; 9 | 10 | pub struct HitTestCx { 11 | pub(crate) inner: HitTestCxOS, 12 | _p: PhantomData, 13 | } 14 | 15 | impl RenderExt for HitTestCx { 16 | fn node(&self) -> &NodeRef { 17 | &self.inner.node 18 | } 19 | } 20 | 21 | impl HitTestCx { 22 | pub(crate) fn new(cx: HitTestCxOS) -> HitTestCx { 23 | Self { 24 | inner: cx, 25 | _p: PhantomData, 26 | } 27 | } 28 | } 29 | 30 | #[derive(Clone)] 31 | pub struct HitTestCxOS { 32 | pub(crate) node: NodeRef, 33 | /// All affine transformations applied to point at this depth. 34 | pub(crate) affine: Affine, 35 | /// All widgets that got hit and registered for pointer events. 36 | pub(crate) hit_entries: HitTestEntries, 37 | } 38 | 39 | impl HitTestCxOS { 40 | pub(crate) fn new(node: &NodeRef, hit_entries: HitTestEntries, affine: Affine) -> HitTestCxOS { 41 | Self { 42 | node: node.clone(), 43 | hit_entries, 44 | affine, 45 | } 46 | } 47 | 48 | pub fn child(&self, index: usize) -> Option { 49 | Some(HitTestCxOS { 50 | node: self.node.child(index)?, 51 | hit_entries: self.hit_entries.clone(), 52 | affine: self.affine, 53 | }) 54 | } 55 | 56 | pub fn children<'a>(&'a mut self) -> ChildrenIter<'a> { 57 | self.node.children().into_iter().map(|child| HitTestCxOS { 58 | node: child, 59 | ..self.clone() 60 | }) 61 | } 62 | 63 | /// Add comment. 64 | pub fn hit_test(&mut self, point: Point) -> bool { 65 | self.node.widget().hit_test_os(self.clone(), point) 66 | } 67 | 68 | /// Add comment. 69 | pub fn hit_test_with_paint_offset(&mut self, point: Point) -> bool { 70 | let offset = self.node.borrow().render_data.local_offset; 71 | let affine = Affine::translate((-offset.x, -offset.y)); 72 | self.hit_test_with_transform(point, affine) 73 | } 74 | 75 | // Todo: Add `hit_test_with_paint_transform` by defining our own 76 | // `canvas.save()` which will check `canvas.current_transform()` and store 77 | // it in the RenderState. 78 | 79 | /// Add comment. 80 | pub fn hit_test_with_transform(&mut self, point: Point, transform: Affine) -> bool { 81 | let mut cx = self.clone(); 82 | 83 | let point_after = transform * point; 84 | cx.affine = transform * self.affine; 85 | 86 | self.node.widget().hit_test_os(cx, point_after) 87 | } 88 | 89 | pub fn layout_box(&self) -> Size { 90 | self.node.borrow().render_data.size 91 | } 92 | } 93 | 94 | type ChildrenIter<'a> = 95 | impl Iterator + 'a + DoubleEndedIterator + ExactSizeIterator; 96 | 97 | impl std::ops::Deref for HitTestCx { 98 | type Target = HitTestCxOS; 99 | 100 | fn deref(&self) -> &Self::Target { 101 | &self.inner 102 | } 103 | } 104 | 105 | impl std::ops::DerefMut for HitTestCx { 106 | fn deref_mut(&mut self) -> &mut Self::Target { 107 | &mut self.inner 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/pointer_events/events.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::{ 2 | kurbo::{Affine, Point}, 3 | MouseEvent, 4 | }; 5 | 6 | #[derive(Debug, Clone)] 7 | pub enum PointerEvent { 8 | PointerUp(PointerUp), 9 | PointerDown(PointerDown), 10 | PointerMove(PointerMove), 11 | PointerExit(PointerExit), 12 | PointerScroll(PointerScroll), 13 | } 14 | 15 | impl PointerEvent { 16 | pub(crate) fn new(e: &MouseEvent, arg: &str) -> PointerEvent { 17 | match arg { 18 | "up" => Self::PointerUp(PointerUp(e.clone())), 19 | "move" => Self::PointerMove(PointerMove(e.clone())), 20 | "down" => Self::PointerDown(PointerDown(e.clone())), 21 | "wheel" => Self::PointerScroll(PointerScroll(e.clone())), 22 | _ => unreachable!(), 23 | } 24 | } 25 | 26 | pub fn pos(&self) -> Point { 27 | match self { 28 | PointerEvent::PointerDown(e) => e.0.pos, 29 | PointerEvent::PointerUp(e) => e.0.pos, 30 | PointerEvent::PointerScroll(e) => e.0.pos, 31 | PointerEvent::PointerMove(e) => e.0.pos, 32 | PointerEvent::PointerExit(e) => e.0.pos, 33 | } 34 | } 35 | 36 | pub fn transform(&self, affine: &Affine) -> Self { 37 | let pos = *affine * self.pos(); 38 | self.clone_at(pos) 39 | } 40 | 41 | pub fn clone_at(&self, pos: Point) -> Self { 42 | let mut r = self.clone(); 43 | 44 | match &mut r { 45 | PointerEvent::PointerUp(e) => { 46 | e.0.pos = pos; 47 | } 48 | PointerEvent::PointerDown(e) => { 49 | e.0.pos = pos; 50 | } 51 | PointerEvent::PointerScroll(e) => { 52 | e.0.pos = pos; 53 | } 54 | PointerEvent::PointerMove(e) => { 55 | e.0.pos = pos; 56 | } 57 | PointerEvent::PointerExit(e) => { 58 | e.0.pos = pos; 59 | } 60 | } 61 | 62 | r 63 | } 64 | 65 | pub(crate) fn raw(&self) -> MouseEvent { 66 | match self.clone() { 67 | PointerEvent::PointerUp(e) => e.0, 68 | PointerEvent::PointerDown(e) => e.0, 69 | PointerEvent::PointerMove(e) => e.0, 70 | PointerEvent::PointerExit(e) => e.0, 71 | PointerEvent::PointerScroll(e) => e.0, 72 | } 73 | } 74 | } 75 | 76 | // Todo: Refactor following ... 77 | 78 | #[derive(Debug, Clone)] 79 | pub struct PointerUp(pub MouseEvent); 80 | 81 | #[derive(Debug, Clone)] 82 | pub struct PointerDown(pub MouseEvent); 83 | 84 | #[derive(Debug, Clone)] 85 | pub struct PointerScroll(pub MouseEvent); 86 | 87 | #[derive(Debug, Clone)] 88 | pub struct PointerEnter(pub MouseEvent); 89 | 90 | #[derive(Debug, Clone)] 91 | pub struct PointerMove(pub MouseEvent); 92 | 93 | #[derive(Debug, Clone)] 94 | pub struct PointerExit(pub MouseEvent); 95 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/pointer_events/mod.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::kurbo::Point; 2 | use frui_macros::sealed; 3 | 4 | use self::context::HitTestCxOS; 5 | 6 | pub mod context; 7 | pub mod events; 8 | pub mod pointer_listener; 9 | pub mod pointer_region; 10 | 11 | pub use context::HitTestCx; 12 | pub use events::PointerEvent; 13 | pub use pointer_listener::PointerListener; 14 | pub use pointer_region::PointerRegion; 15 | 16 | pub trait HitTest: Sized { 17 | fn hit_test<'a>(&'a self, cx: &'a mut HitTestCx, point: Point) -> bool { 18 | if cx.layout_box().contains(point) { 19 | for mut child in cx.children() { 20 | if child.hit_test_with_paint_offset(point) { 21 | // Don't hit test other children if one already handled that 22 | // event. 23 | return true; 24 | } 25 | } 26 | 27 | return true; 28 | } 29 | 30 | false 31 | } 32 | 33 | #[allow(unused_variables)] 34 | fn handle_event(&self, cx: &mut HitTestCx, event: &PointerEvent) {} 35 | } 36 | 37 | #[sealed(crate)] 38 | pub trait HitTestOS { 39 | fn hit_test_os(&self, cx: HitTestCxOS, point: Point) -> bool; 40 | fn handle_event_os(&self, cx: HitTestCxOS, event: &PointerEvent); 41 | } 42 | 43 | impl HitTestOS for T { 44 | default fn hit_test_os(&self, mut cx: HitTestCxOS, point: Point) -> bool { 45 | if cx.layout_box().contains(point) { 46 | for mut child in cx.children() { 47 | if child.hit_test_with_paint_offset(point) { 48 | // Don't hit test other children if one already handled that 49 | // event. 50 | return true; 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | 57 | false 58 | } 59 | 60 | default fn handle_event_os(&self, _: HitTestCxOS, _: &PointerEvent) {} 61 | } 62 | 63 | impl HitTestOS for T { 64 | fn hit_test_os(&self, cx: HitTestCxOS, point: Point) -> bool { 65 | let cx = &mut >::new(cx); 66 | 67 | if T::hit_test(&self, cx, point) { 68 | cx.inner 69 | .hit_entries 70 | .borrow_mut() 71 | .insert(cx.inner.node.clone(), cx.inner.affine); 72 | 73 | true 74 | } else { 75 | false 76 | } 77 | } 78 | 79 | fn handle_event_os(&self, cx: HitTestCxOS, event: &PointerEvent) { 80 | let cx = &mut HitTestCx::new(cx); 81 | 82 | T::handle_event(&self, cx, event) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/pointer_events/pointer_listener.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | use super::{ 4 | events::{PointerDown, PointerEvent, PointerScroll, PointerUp}, 5 | HitTest, HitTestCx, 6 | }; 7 | 8 | #[derive(ViewWidget)] 9 | pub struct PointerListener 10 | where 11 | PU: FnPointerUp, 12 | PD: FnPointerDown, 13 | PS: FnPointerScroll, 14 | CHILD: Widget, 15 | { 16 | // Todo: Consider implementing `on_pointer_move` which would be triggered on 17 | // `PointerMove` events while the pointer is down. That event would be 18 | // dispatched outside of widget layout boundary, as opposed to `PointerRegion`. 19 | on_pointer_up: PU, 20 | on_pointer_down: PD, 21 | on_pointer_scroll: PS, 22 | child: CHILD, 23 | } 24 | 25 | impl ViewWidget for PointerListener 26 | where 27 | PU: FnPointerUp, 28 | PD: FnPointerDown, 29 | PS: FnPointerScroll, 30 | CHILD: Widget, 31 | { 32 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 33 | &self.child 34 | } 35 | } 36 | 37 | impl HitTest for PointerListener 38 | where 39 | PU: FnPointerUp, 40 | PD: FnPointerDown, 41 | PS: FnPointerScroll, 42 | CHILD: Widget, 43 | { 44 | fn hit_test<'a>( 45 | &'a self, 46 | cx: &'a mut HitTestCx, 47 | point: druid_shell::kurbo::Point, 48 | ) -> bool { 49 | if cx.layout_box().contains(point) { 50 | for mut child in cx.children() { 51 | child.hit_test(point); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | false 58 | } 59 | 60 | fn handle_event(&self, _: &mut HitTestCx, event: &PointerEvent) { 61 | match event { 62 | PointerEvent::PointerDown(e) => self.on_pointer_down.call(e), 63 | PointerEvent::PointerUp(e) => self.on_pointer_up.call(e), 64 | PointerEvent::PointerScroll(e) => self.on_pointer_scroll.call(e), 65 | _ => {} 66 | } 67 | } 68 | } 69 | 70 | // 71 | // Builder methods: 72 | 73 | /// No-op function 74 | #[doc(hidden)] 75 | pub struct NOP; 76 | 77 | macro_rules! impl_fn { 78 | (Fn($($arg:tt)*) for $target:ident with $temp_trait:ident) => { 79 | pub trait $temp_trait { 80 | fn call(&self, _: $($arg)*) {} 81 | } 82 | 83 | impl $temp_trait for $target { 84 | fn call(&self, _: $($arg)*) {} 85 | } 86 | 87 | impl $temp_trait for F { 88 | fn call(&self, v: $($arg)*) { 89 | self(v) 90 | } 91 | } 92 | }; 93 | } 94 | 95 | impl_fn!(Fn(&PointerUp) for NOP with FnPointerUp); 96 | impl_fn!(Fn(&PointerDown) for NOP with FnPointerDown); 97 | impl_fn!(Fn(&PointerScroll) for NOP with FnPointerScroll); 98 | 99 | impl PointerListener { 100 | pub fn builder() -> Self { 101 | Self { 102 | on_pointer_up: NOP, 103 | on_pointer_down: NOP, 104 | on_pointer_scroll: NOP, 105 | child: (), 106 | } 107 | } 108 | } 109 | 110 | impl PointerListener 111 | where 112 | PU: FnPointerUp, 113 | PD: FnPointerDown, 114 | PS: FnPointerScroll, 115 | CHILD: Widget, 116 | { 117 | pub fn on_pointer_up( 118 | self, 119 | f: impl Fn(&PointerUp), 120 | ) -> PointerListener { 121 | PointerListener { 122 | on_pointer_up: f, 123 | on_pointer_down: self.on_pointer_down, 124 | on_pointer_scroll: self.on_pointer_scroll, 125 | child: self.child, 126 | } 127 | } 128 | 129 | pub fn on_pointer_down( 130 | self, 131 | f: impl Fn(&PointerDown), 132 | ) -> PointerListener { 133 | PointerListener { 134 | on_pointer_up: self.on_pointer_up, 135 | on_pointer_down: f, 136 | on_pointer_scroll: self.on_pointer_scroll, 137 | child: self.child, 138 | } 139 | } 140 | 141 | pub fn on_pointer_scroll( 142 | self, 143 | f: impl Fn(&PointerScroll), 144 | ) -> PointerListener { 145 | PointerListener { 146 | on_pointer_up: self.on_pointer_up, 147 | on_pointer_down: self.on_pointer_down, 148 | on_pointer_scroll: f, 149 | child: self.child, 150 | } 151 | } 152 | 153 | pub fn child(self, child: impl Widget) -> PointerListener { 154 | PointerListener { 155 | on_pointer_up: self.on_pointer_up, 156 | on_pointer_down: self.on_pointer_down, 157 | on_pointer_scroll: self.on_pointer_scroll, 158 | child, 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/pointer_events/pointer_region.rs: -------------------------------------------------------------------------------- 1 | use crate::{prelude::*, render::*}; 2 | 3 | use super::{events::*, HitTest, HitTestCx}; 4 | 5 | #[derive(ViewWidget)] 6 | pub struct PointerRegion 7 | where 8 | PEN: FnPointerEnter, 9 | PMV: FnPointerMove, 10 | PEX: FnPointerExit, 11 | CHILD: Widget, 12 | { 13 | on_enter: PEN, 14 | on_move: PMV, 15 | on_exit: PEX, 16 | child: CHILD, 17 | } 18 | 19 | impl ViewWidget for PointerRegion 20 | where 21 | PEN: FnPointerEnter, 22 | PMV: FnPointerMove, 23 | PEX: FnPointerExit, 24 | CHILD: Widget, 25 | { 26 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 27 | &self.child 28 | } 29 | } 30 | 31 | impl RenderState for PointerRegion 32 | where 33 | PEN: FnPointerEnter, 34 | PMV: FnPointerMove, 35 | PEX: FnPointerExit, 36 | CHILD: Widget, 37 | { 38 | // Is hovered. 39 | type State = bool; 40 | 41 | fn create_state(&self) -> Self::State { 42 | false 43 | } 44 | } 45 | 46 | impl HitTest for PointerRegion 47 | where 48 | PEN: FnPointerEnter, 49 | PMV: FnPointerMove, 50 | PEX: FnPointerExit, 51 | CHILD: Widget, 52 | { 53 | fn handle_event(&self, cx: &mut HitTestCx, event: &PointerEvent) { 54 | match event { 55 | PointerEvent::PointerMove(e) => { 56 | if *cx.render_state() { 57 | self.on_move.call(&PointerMove(e.0.clone())); 58 | } else { 59 | // Pointer now hovers over this widget. 60 | *cx.render_state_mut() = true; 61 | 62 | self.on_enter.call(&PointerEnter(e.0.clone())); 63 | } 64 | } 65 | PointerEvent::PointerExit(e) => { 66 | // Pointer no longer hovers over this widget. 67 | *cx.render_state_mut() = false; 68 | 69 | self.on_exit.call(e); 70 | } 71 | _ => {} 72 | } 73 | } 74 | } 75 | 76 | // 77 | // Builder methods: 78 | 79 | /// No-op function 80 | #[doc(hidden)] 81 | pub struct NOP; 82 | 83 | macro_rules! impl_fn { 84 | (Fn($($arg:tt)*) for $target:ident with $temp_trait:ident) => { 85 | pub trait $temp_trait { 86 | fn call(&self, _: $($arg)*) {} 87 | } 88 | 89 | impl $temp_trait for $target { 90 | fn call(&self, _: $($arg)*) {} 91 | } 92 | 93 | impl $temp_trait for F { 94 | fn call(&self, v: $($arg)*) { 95 | self(v) 96 | } 97 | } 98 | }; 99 | } 100 | 101 | impl_fn!(Fn(&PointerEnter) for NOP with FnPointerEnter); 102 | impl_fn!(Fn(&PointerMove) for NOP with FnPointerMove); 103 | impl_fn!(Fn(&PointerExit) for NOP with FnPointerExit); 104 | 105 | impl PointerRegion { 106 | pub fn builder() -> Self { 107 | Self { 108 | on_enter: NOP, 109 | on_move: NOP, 110 | on_exit: NOP, 111 | child: (), 112 | } 113 | } 114 | } 115 | 116 | impl PointerRegion 117 | where 118 | PEN: FnPointerEnter, 119 | PMV: FnPointerMove, 120 | PEX: FnPointerExit, 121 | CHILD: Widget, 122 | { 123 | pub fn on_enter( 124 | self, 125 | f: impl Fn(&PointerEnter), 126 | ) -> PointerRegion { 127 | PointerRegion { 128 | on_enter: f, 129 | on_move: self.on_move, 130 | on_exit: self.on_exit, 131 | child: self.child, 132 | } 133 | } 134 | 135 | pub fn on_move( 136 | self, 137 | f: impl Fn(&PointerMove), 138 | ) -> PointerRegion { 139 | PointerRegion { 140 | on_enter: self.on_enter, 141 | on_move: f, 142 | on_exit: self.on_exit, 143 | child: self.child, 144 | } 145 | } 146 | 147 | pub fn on_exit( 148 | self, 149 | f: impl Fn(&PointerExit), 150 | ) -> PointerRegion { 151 | PointerRegion { 152 | on_enter: self.on_enter, 153 | on_move: self.on_move, 154 | on_exit: f, 155 | child: self.child, 156 | } 157 | } 158 | 159 | pub fn child(self, child: impl Widget) -> PointerRegion { 160 | PointerRegion { 161 | on_enter: self.on_enter, 162 | on_move: self.on_move, 163 | on_exit: self.on_exit, 164 | child, 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/structural_eq/mod.rs: -------------------------------------------------------------------------------- 1 | use frui_macros::sealed; 2 | 3 | use super::any_ext::AnyExt; 4 | 5 | mod structural_eq; 6 | 7 | pub use structural_eq::{StructuralEq, StructuralEqImpl}; 8 | 9 | #[sealed(crate)] 10 | pub trait StructuralEqOS { 11 | fn eq(&self, other: &dyn AnyExt) -> bool; 12 | } 13 | 14 | impl StructuralEqOS for T { 15 | fn eq(&self, other: &dyn AnyExt) -> bool { 16 | // Safety: 17 | // 18 | // `StructuralEq` is implemented by `#[derive(WidgetKind)]` macro, which doesn't 19 | // mutate any data of a widget through interior mutability thus it can't cause 20 | // dangling pointers. 21 | // 22 | // Additionally, the procedural macro correctly compares every field of a widget 23 | // (and that includes comparing fields containing references) which is important 24 | // because, if a structure contains any references, incorrectly assuming that 25 | // two widgets are equal could result in dangling references (after preserving 26 | // old widget configuration). 27 | unsafe { 28 | match other.downcast_ref::() { 29 | Some(other) => ::eq(self, other), 30 | None => { 31 | eprintln!( 32 | "WidgetEqOS: can't compare widgets of different types. This is a bug." 33 | ); 34 | false 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/frui_core/src/api/widget_ptr.rs: -------------------------------------------------------------------------------- 1 | use std::any::TypeId; 2 | 3 | use crate::macro_exports::{RawBuildCx, RawWidget}; 4 | 5 | use super::{ 6 | any_ext::AnyExt, contexts::build_cx::STATE_UPDATE_SUPRESSED, structural_eq::StructuralEqOS, 7 | Widget, 8 | }; 9 | 10 | #[derive(Clone)] 11 | pub struct WidgetPtr<'a> { 12 | /// Reference to the exact type of this widget. Used to properly dispatch methods. 13 | pub(crate) kind: &'a (dyn RawWidget + 'a), 14 | 15 | /// Whether this pointer references or owns a widget. Used to properly drop it. 16 | pub(crate) owned: Option<*mut (dyn Widget + 'a)>, 17 | } 18 | 19 | impl<'a> WidgetPtr<'a> { 20 | pub fn from_ref(widget: &'a dyn RawWidget) -> Self { 21 | Self { 22 | kind: widget, 23 | owned: None, 24 | } 25 | } 26 | 27 | /// # Note 28 | /// 29 | /// `widget` will not be dropped until you manually call [`WidgetPtr::drop`]. 30 | pub fn from_owned(widget: Box) -> Self { 31 | // Safety: 32 | // 33 | // `widget.kind()` returns `WidgetKind` which holds a reference to the `widget`. 34 | // Since that reference points to a heap, we can safely extend lifetime of it to 35 | // the lifetime of `WidgetPtr` until `drop` is called. 36 | unsafe { 37 | Self { 38 | kind: std::mem::transmute::<&dyn RawWidget, &dyn RawWidget>(widget.as_raw()), 39 | owned: Some(Box::into_raw(widget)), 40 | } 41 | } 42 | } 43 | 44 | /// ## Safety 45 | /// 46 | /// Data referenced by this [`WidgetPtr`] didn't move. 47 | /// 48 | /// Make sure there is no other [`WidgetPtr`] that may reference this 49 | /// [`WidgetPtr`] after calling this method. That includes calling `drop` 50 | /// a second time. 51 | pub unsafe fn drop(&self) { 52 | if let Some(widget) = self.owned { 53 | drop(Box::from_raw(widget)); 54 | } 55 | } 56 | 57 | /// # Note 58 | /// 59 | /// Returned [`WidgetPtr`] has erased lifetime, but its invariants must be upheld. 60 | pub fn build(&self, cx: &RawBuildCx) -> Vec> { 61 | STATE_UPDATE_SUPRESSED.store(true, std::sync::atomic::Ordering::SeqCst); 62 | 63 | let ptrs = self.kind.build(cx); 64 | 65 | STATE_UPDATE_SUPRESSED.store(false, std::sync::atomic::Ordering::SeqCst); 66 | 67 | // Safety: Consumers of `WidgetPtr` must upheld correct invariants. 68 | unsafe { std::mem::transmute::, Vec>(ptrs) } 69 | } 70 | 71 | pub fn can_update(&self, other: &WidgetPtr) -> bool { 72 | self.raw().unique_type() == other.raw().unique_type() 73 | && self.raw().state_type_id() == other.raw().state_type_id() 74 | } 75 | 76 | pub fn inherited_key(&self) -> TypeId { 77 | match self.kind.inherited_key() { 78 | Some(k) => k, 79 | None => unreachable!("inherited_key() called on non-inherited widget"), 80 | } 81 | } 82 | 83 | pub fn eq(&self, other: &WidgetPtr) -> bool { 84 | // If widget configurations are not owned, their pointer addresses 85 | // must be equal before we can compare them using `CheapEq`. 86 | if self.is_borrowed() { 87 | if self.widget_ptr() != other.widget_ptr() { 88 | return false; 89 | } 90 | } 91 | 92 | StructuralEqOS::eq(self.kind, other.as_any_ext()) 93 | } 94 | 95 | pub fn mount(&self, build_cx: &RawBuildCx) { 96 | STATE_UPDATE_SUPRESSED.store(true, std::sync::atomic::Ordering::SeqCst); 97 | 98 | self.kind.mount(build_cx); 99 | 100 | STATE_UPDATE_SUPRESSED.store(false, std::sync::atomic::Ordering::SeqCst); 101 | } 102 | 103 | pub fn unmount(&self, build_cx: &RawBuildCx) { 104 | STATE_UPDATE_SUPRESSED.store(true, std::sync::atomic::Ordering::SeqCst); 105 | 106 | self.kind.unmount(build_cx); 107 | 108 | STATE_UPDATE_SUPRESSED.store(false, std::sync::atomic::Ordering::SeqCst); 109 | } 110 | 111 | // 112 | // 113 | 114 | pub fn raw<'b>(&'b self) -> &'b (dyn RawWidget + 'b) { 115 | self.kind 116 | } 117 | 118 | // 119 | // 120 | 121 | pub fn as_any_ext(&self) -> &dyn AnyExt { 122 | self.kind.as_any_ext() 123 | } 124 | 125 | pub fn has_key(&self) -> bool { 126 | self.raw().local_key().is_some() 127 | } 128 | 129 | pub fn is_inherited_widget(&self) -> bool { 130 | self.kind.inherited_key().is_some() 131 | } 132 | 133 | fn is_borrowed(&self) -> bool { 134 | self.owned.is_none() 135 | } 136 | 137 | fn widget_ptr(&self) -> *const () { 138 | self.kind as *const _ as *const () 139 | } 140 | } 141 | 142 | impl Default for WidgetPtr<'_> { 143 | fn default() -> Self { 144 | WidgetPtr::from_owned(Box::new(())) 145 | } 146 | } 147 | 148 | pub trait IntoWidgetPtr { 149 | fn into_widget_ptr<'a>(self) -> WidgetPtr<'a> 150 | where 151 | Self: 'a; 152 | } 153 | 154 | impl IntoWidgetPtr for T { 155 | default fn into_widget_ptr<'a>(self) -> WidgetPtr<'a> 156 | where 157 | Self: 'a, 158 | { 159 | WidgetPtr::from_owned(Box::new(self)) 160 | } 161 | } 162 | 163 | impl IntoWidgetPtr for &T { 164 | fn into_widget_ptr<'a>(self) -> WidgetPtr<'a> 165 | where 166 | Self: 'a, 167 | { 168 | WidgetPtr::from_ref(self.as_raw()) 169 | } 170 | } 171 | 172 | impl IntoWidgetPtr for &dyn Widget { 173 | fn into_widget_ptr<'a>(self) -> WidgetPtr<'a> 174 | where 175 | Self: 'a, 176 | { 177 | WidgetPtr::from_ref(self.as_raw()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/listeners/keyboard.rs: -------------------------------------------------------------------------------- 1 | //! This implementation of KeyboardEventListeners 2 | 3 | use std::cell::RefCell; 4 | 5 | use druid_shell::KeyEvent; 6 | use slotmap::SlotMap; 7 | 8 | slotmap::new_key_type! { pub struct CallbackKey; } 9 | 10 | #[derive(Default)] 11 | pub struct KeyboardEventListeners { 12 | callbacks: SlotMap, 13 | } 14 | 15 | impl KeyboardEventListeners { 16 | /// Registers a callback which will be called when a keyboard event is received. 17 | /// 18 | /// ## Safety: 19 | /// 20 | /// Value `callback` points to must live until [`unregister`] called. 21 | /// 22 | /// In other words, you need to remove this callback, by calling [`remove`] 23 | /// with [`CallbackKey`] returned from this function, before that `callback` is 24 | /// going to be dropped. 25 | pub unsafe fn register<'a>(&mut self, callback: *const (dyn Fn(KeyEvent) + 'a)) -> CallbackKey { 26 | self.callbacks.insert(std::mem::transmute(callback)) 27 | } 28 | 29 | pub fn unregister(&mut self, key: CallbackKey) { 30 | self.callbacks.remove(key); 31 | } 32 | 33 | pub fn len(&self) -> usize { 34 | self.callbacks.len() 35 | } 36 | 37 | pub(crate) fn iter<'a>(&'a self) -> impl Iterator { 38 | self.callbacks.iter().map(|(_, f)| { 39 | // Safety: `callback` is valid as ensured by registrars to `KeyboardEventListeners`. 40 | unsafe { &**f } 41 | }) 42 | } 43 | } 44 | 45 | thread_local! { 46 | /// Todo: Optimize this with something like SlotMap<(&f, PreviousNode, NextNode)> ? 47 | /// Basically linked tree but with advantages of slot map...? 48 | pub static KEYBOARD_EVENT_LISTENERS: RefCell = Default::default(); 49 | } 50 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/listeners/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod keyboard; 2 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{RefCell, RefMut}; 2 | 3 | use druid_shell::piet::PietText; 4 | 5 | pub mod listeners; 6 | pub mod runner; 7 | pub mod tree; 8 | 9 | pub struct TextFactory(RefCell>); 10 | 11 | impl TextFactory { 12 | pub(crate) fn new() -> Self { 13 | TextFactory(RefCell::new(None)) 14 | } 15 | 16 | pub(crate) fn set(&self, f: PietText) { 17 | *self.0.borrow_mut() = Some(f); 18 | } 19 | 20 | pub fn get(&self) -> RefMut { 21 | RefMut::map(self.0.borrow_mut(), |b| { 22 | b.as_mut().expect("TextFactory was not set") 23 | }) 24 | } 25 | } 26 | 27 | thread_local! { 28 | pub static TEXT_FACTORY: TextFactory = TextFactory::new(); 29 | } 30 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/runner/miri/mod.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::{ 2 | kurbo::{Rect, Size}, 3 | KeyEvent, MouseEvent, Region, 4 | }; 5 | use log::LevelFilter; 6 | use simplelog::{ColorChoice, Config, TermLogger, TerminalMode}; 7 | 8 | use super::{window_handler::WindowHandler, FruiWindowHandler}; 9 | use crate::prelude::Widget; 10 | 11 | mod substitutes; 12 | pub use substitutes::*; 13 | 14 | pub struct MiriRunner { 15 | last_size: Size, 16 | handler: WindowHandler, 17 | } 18 | 19 | impl MiriRunner { 20 | pub fn new(widget: W) -> Self { 21 | // Enable debug logging: 22 | let _ = TermLogger::init( 23 | LevelFilter::Info, 24 | Config::default(), 25 | TerminalMode::Mixed, 26 | ColorChoice::AlwaysAnsi, 27 | ); 28 | 29 | let mut window_handler = WindowHandler::new(widget); 30 | 31 | // Execute application startup sequence: 32 | 33 | window_handler.connect(&WindowHandle {}); 34 | window_handler.size(Size::new(500., 400.)); 35 | window_handler.prepare_paint(); 36 | window_handler.paint( 37 | &mut Canvas::default(), 38 | &default_region(Size::new(500., 400.)), 39 | ); 40 | 41 | let mut this = MiriRunner { 42 | last_size: Size::new(500., 400.), 43 | handler: window_handler, 44 | }; 45 | 46 | // First update. 47 | this.update(true); 48 | 49 | this 50 | } 51 | 52 | pub fn update(&mut self, force_repaint: bool) { 53 | for token in SCHEDULE_IDLE.lock().unwrap().drain(..) { 54 | self.handler.idle(token); 55 | } 56 | 57 | if *REQUEST_ANIM_FRAME.lock().unwrap() || force_repaint { 58 | self.handler.prepare_paint(); 59 | self.handler 60 | .paint(&mut Canvas::default(), &default_region(self.last_size)); 61 | 62 | *REQUEST_ANIM_FRAME.lock().unwrap() = false; 63 | } 64 | } 65 | 66 | // 67 | // Pass window events: 68 | 69 | pub fn mouse_down(&mut self, event: &MouseEvent) { 70 | self.handler.mouse_down(&event); 71 | } 72 | 73 | pub fn mouse_move(&mut self, event: &MouseEvent) { 74 | self.handler.mouse_move(&event); 75 | } 76 | 77 | pub fn mouse_up(&mut self, event: &MouseEvent) { 78 | self.handler.mouse_up(&event); 79 | } 80 | 81 | pub fn key_down(&mut self, event: KeyEvent) { 82 | self.handler.key_down(event); 83 | } 84 | 85 | pub fn size(&mut self, size: druid_shell::kurbo::Size) { 86 | self.handler.size(size); 87 | self.update(true); 88 | } 89 | } 90 | 91 | fn default_region(window_size: Size) -> Region { 92 | let mut region = Region::EMPTY; 93 | region.add_rect(Rect::new(0., 0., window_size.width, window_size.height)); 94 | region 95 | } 96 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/runner/miri/substitutes.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, marker::PhantomData, sync::Mutex}; 2 | 3 | use druid_shell::{ 4 | piet::{CoreGraphicsImage, CoreGraphicsText, CoreGraphicsTextLayout, IntoBrush, PietText}, 5 | Cursor, IdleToken, 6 | }; 7 | 8 | pub static REQUEST_ANIM_FRAME: Mutex = Mutex::new(false); 9 | 10 | pub static SCHEDULE_IDLE: Mutex> = Mutex::new(Vec::new()); 11 | 12 | /// Placeholder for [`IdleHandle`](druid_shell::IdleHandle) that allows us to test Frui in Miri. 13 | pub struct IdleHandle {} 14 | 15 | impl IdleHandle { 16 | pub fn schedule_idle(&self, token: IdleToken) { 17 | SCHEDULE_IDLE.lock().unwrap().push(token) 18 | } 19 | } 20 | 21 | /// Placeholder for [`Application`](druid_shell::Application) that allows us to test Frui in Miri. 22 | pub struct Application {} 23 | 24 | impl Application { 25 | pub fn global() -> Self { 26 | Self {} 27 | } 28 | 29 | pub fn quit(&self) {} 30 | } 31 | 32 | /// Placeholder for [`WindowHandle`](druid_shell::WindowHandle) that allows us to test Frui in Miri. 33 | #[derive(Default, Clone)] 34 | pub struct WindowHandle {} 35 | 36 | impl WindowHandle { 37 | pub fn request_anim_frame(&mut self) { 38 | *REQUEST_ANIM_FRAME.lock().unwrap() = true; 39 | } 40 | 41 | pub fn get_idle_handle(&self) -> Option { 42 | Some(IdleHandle {}) 43 | } 44 | 45 | pub fn set_cursor(&self, _: &Cursor) {} 46 | 47 | pub fn invalidate(&self) {} 48 | 49 | pub fn close(&self) {} 50 | 51 | #[track_caller] 52 | pub fn text(&self) -> PietText { 53 | todo!() 54 | } 55 | } 56 | 57 | /// Placeholder for [`Piet`](druid_shell::piet::Piet) that allows us to test Frui in Miri. 58 | #[derive(Default)] 59 | pub struct Canvas<'a> { 60 | _p: PhantomData<&'a ()>, 61 | } 62 | 63 | #[allow(unused)] 64 | impl druid_shell::piet::RenderContext for Canvas<'_> { 65 | type Brush = Brush; 66 | 67 | type Text = CoreGraphicsText; 68 | 69 | type TextLayout = CoreGraphicsTextLayout; 70 | 71 | type Image = CoreGraphicsImage; 72 | 73 | fn status(&mut self) -> Result<(), druid_shell::piet::Error> { 74 | todo!() 75 | } 76 | 77 | fn solid_brush(&mut self, color: druid_shell::piet::Color) -> Self::Brush { 78 | Brush 79 | } 80 | 81 | fn gradient( 82 | &mut self, 83 | gradient: impl Into, 84 | ) -> Result { 85 | todo!() 86 | } 87 | 88 | fn clear( 89 | &mut self, 90 | region: impl Into>, 91 | color: druid_shell::piet::Color, 92 | ) { 93 | todo!() 94 | } 95 | 96 | fn stroke( 97 | &mut self, 98 | shape: impl druid_shell::kurbo::Shape, 99 | brush: &impl druid_shell::piet::IntoBrush, 100 | width: f64, 101 | ) { 102 | todo!() 103 | } 104 | 105 | fn stroke_styled( 106 | &mut self, 107 | shape: impl druid_shell::kurbo::Shape, 108 | brush: &impl druid_shell::piet::IntoBrush, 109 | width: f64, 110 | style: &druid_shell::piet::StrokeStyle, 111 | ) { 112 | } 113 | 114 | fn fill( 115 | &mut self, 116 | shape: impl druid_shell::kurbo::Shape, 117 | brush: &impl druid_shell::piet::IntoBrush, 118 | ) { 119 | } 120 | 121 | fn fill_even_odd( 122 | &mut self, 123 | shape: impl druid_shell::kurbo::Shape, 124 | brush: &impl druid_shell::piet::IntoBrush, 125 | ) { 126 | todo!() 127 | } 128 | 129 | fn clip(&mut self, shape: impl druid_shell::kurbo::Shape) {} 130 | 131 | fn text(&mut self) -> &mut Self::Text { 132 | todo!() 133 | } 134 | 135 | fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into) { 136 | todo!() 137 | } 138 | 139 | fn save(&mut self) -> Result<(), druid_shell::piet::Error> { 140 | // Todo: This is incorrect, we should create a stack for this. 141 | Ok(()) 142 | } 143 | 144 | fn restore(&mut self) -> Result<(), druid_shell::piet::Error> { 145 | // Todo: This is incorrect, we should create a stack for this. 146 | Ok(()) 147 | } 148 | 149 | fn finish(&mut self) -> Result<(), druid_shell::piet::Error> { 150 | todo!() 151 | } 152 | 153 | fn transform(&mut self, transform: druid_shell::kurbo::Affine) {} 154 | 155 | fn make_image( 156 | &mut self, 157 | width: usize, 158 | height: usize, 159 | buf: &[u8], 160 | format: druid_shell::piet::ImageFormat, 161 | ) -> Result { 162 | todo!() 163 | } 164 | 165 | fn draw_image( 166 | &mut self, 167 | image: &Self::Image, 168 | dst_rect: impl Into, 169 | interp: druid_shell::piet::InterpolationMode, 170 | ) { 171 | todo!() 172 | } 173 | 174 | fn draw_image_area( 175 | &mut self, 176 | image: &Self::Image, 177 | src_rect: impl Into, 178 | dst_rect: impl Into, 179 | interp: druid_shell::piet::InterpolationMode, 180 | ) { 181 | todo!() 182 | } 183 | 184 | fn capture_image_area( 185 | &mut self, 186 | src_rect: impl Into, 187 | ) -> Result { 188 | todo!() 189 | } 190 | 191 | fn blurred_rect( 192 | &mut self, 193 | rect: druid_shell::kurbo::Rect, 194 | blur_radius: f64, 195 | brush: &impl druid_shell::piet::IntoBrush, 196 | ) { 197 | todo!() 198 | } 199 | 200 | fn current_transform(&self) -> druid_shell::kurbo::Affine { 201 | todo!() 202 | } 203 | } 204 | 205 | #[derive(Debug, Clone, Copy)] 206 | pub struct Brush; 207 | 208 | impl IntoBrush> for Brush { 209 | fn make_brush<'a>( 210 | &'a self, 211 | _piet: &mut Canvas<'_>, 212 | _bbox: impl FnOnce() -> druid_shell::kurbo::Rect, 213 | ) -> Cow<'a, as druid_shell::piet::RenderContext>::Brush> { 214 | Cow::Owned(Brush) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/runner/mod.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::{IdleToken, KeyEvent, MouseEvent}; 2 | 3 | pub mod window_handler; 4 | 5 | #[cfg(feature = "miri")] 6 | pub mod miri; 7 | pub mod native; 8 | 9 | #[cfg(feature = "miri")] 10 | pub type IdleHandle = miri::IdleHandle; 11 | #[cfg(not(feature = "miri"))] 12 | pub type IdleHandle = druid_shell::IdleHandle; 13 | 14 | #[cfg(feature = "miri")] 15 | pub type Application = miri::Application; 16 | #[cfg(not(feature = "miri"))] 17 | pub type Application = druid_shell::Application; 18 | 19 | #[cfg(feature = "miri")] 20 | pub type WindowHandle = miri::WindowHandle; 21 | #[cfg(not(feature = "miri"))] 22 | pub type WindowHandle = druid_shell::WindowHandle; 23 | 24 | #[cfg(feature = "miri")] 25 | pub type Canvas<'a> = miri::Canvas<'a>; 26 | #[cfg(not(feature = "miri"))] 27 | pub type Canvas<'a> = druid_shell::piet::Piet<'a>; 28 | 29 | /// Wrapper around [`druid_shell::WinHandler`] that allows us to run tests in Miri. 30 | /// This implementation can be called by both [`MiriRunner`] and [`druid_shell::WinHandler`]. 31 | pub trait FruiWindowHandler { 32 | fn connect(&mut self, handle: &WindowHandle); 33 | 34 | fn prepare_paint(&mut self); 35 | 36 | fn paint(&mut self, piet: &mut Canvas, invalid: &druid_shell::Region); 37 | 38 | fn size(&mut self, size: druid_shell::kurbo::Size); 39 | 40 | fn idle(&mut self, token: IdleToken); 41 | 42 | fn destroy(&mut self); 43 | 44 | fn as_any(&mut self) -> &mut dyn std::any::Any; 45 | 46 | // Events: 47 | 48 | fn mouse_down(&mut self, event: &MouseEvent); 49 | 50 | fn mouse_up(&mut self, event: &MouseEvent); 51 | 52 | fn mouse_move(&mut self, event: &MouseEvent); 53 | 54 | fn wheel(&mut self, event: &MouseEvent); 55 | 56 | fn key_down(&mut self, event: KeyEvent) -> bool; 57 | 58 | fn request_close(&mut self); 59 | } 60 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/runner/native/mod.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::{ 2 | kurbo::Size, piet::Piet, Application, IdleToken, KeyEvent, MouseEvent, Region, WinHandler, 3 | WindowBuilder, WindowHandle, 4 | }; 5 | use log::LevelFilter; 6 | use simplelog::{ColorChoice, Config, TermLogger, TerminalMode}; 7 | 8 | use crate::prelude::Widget; 9 | 10 | use super::{window_handler::WindowHandler, FruiWindowHandler}; 11 | 12 | // Currently there is `'static` lifetime requirement for the root widget 13 | // because of the requirements of `WinHandle` from the druid_shell. 14 | // 15 | // In the future this requirement may be lifted. 16 | pub fn run_app<'a>(widget: impl Widget + 'static) { 17 | if cfg!(feature = "miri") { 18 | panic!(concat!( 19 | "feature `miri` is enabled which is not supported for `run_app`. ", 20 | "Disable feature `miri` to use `run_app`. To test application using ", 21 | "Miri use `MiriRunner` instead." 22 | )); 23 | } 24 | 25 | // Enable debug logging: 26 | if let Err(_) = TermLogger::init( 27 | LevelFilter::Info, 28 | Config::default(), 29 | TerminalMode::Mixed, 30 | ColorChoice::AlwaysAnsi, 31 | ) { 32 | // If logger was already set, ignore... 33 | } 34 | 35 | // 36 | // Run app: 37 | 38 | let app = Application::new().unwrap(); 39 | 40 | let mut window = WindowBuilder::new(app.clone()); 41 | window.set_handler(Box::new(WindowHandler::new(widget))); 42 | window.set_title("Frui App"); 43 | 44 | let window = window.build().unwrap(); 45 | 46 | window.show(); 47 | app.run(None); 48 | 49 | drop(window); 50 | } 51 | 52 | impl druid_shell::AppHandler for WindowHandler { 53 | fn command(&mut self, id: u32) { 54 | println!("handle system command of id {id}") 55 | } 56 | } 57 | 58 | #[allow(unused)] 59 | impl WinHandler for WindowHandler { 60 | fn connect(&mut self, handle: &WindowHandle) { 61 | #[cfg(feature = "miri")] 62 | unreachable!(); 63 | #[cfg(not(feature = "miri"))] 64 | FruiWindowHandler::connect(self, handle); 65 | } 66 | 67 | fn prepare_paint(&mut self) { 68 | FruiWindowHandler::prepare_paint(self); 69 | } 70 | 71 | #[allow(unused)] 72 | fn paint(&mut self, piet: &mut Piet, invalid: &Region) { 73 | #[cfg(feature = "miri")] 74 | unreachable!(); 75 | #[cfg(not(feature = "miri"))] 76 | FruiWindowHandler::paint(self, piet, invalid) 77 | } 78 | 79 | fn as_any(&mut self) -> &mut dyn std::any::Any { 80 | FruiWindowHandler::as_any(self) 81 | } 82 | 83 | fn size(&mut self, size: Size) { 84 | FruiWindowHandler::size(self, size) 85 | } 86 | 87 | fn idle(&mut self, token: IdleToken) { 88 | FruiWindowHandler::idle(self, token) 89 | } 90 | 91 | fn destroy(&mut self) { 92 | FruiWindowHandler::destroy(self) 93 | } 94 | 95 | fn mouse_down(&mut self, event: &MouseEvent) { 96 | FruiWindowHandler::mouse_down(self, event) 97 | } 98 | 99 | fn mouse_up(&mut self, event: &MouseEvent) { 100 | FruiWindowHandler::mouse_up(self, event) 101 | } 102 | 103 | fn mouse_move(&mut self, event: &MouseEvent) { 104 | FruiWindowHandler::mouse_move(self, event) 105 | } 106 | 107 | fn wheel(&mut self, event: &MouseEvent) { 108 | FruiWindowHandler::wheel(self, event) 109 | } 110 | 111 | fn key_down(&mut self, event: KeyEvent) -> bool { 112 | FruiWindowHandler::key_down(self, event) 113 | } 114 | 115 | fn request_close(&mut self) { 116 | FruiWindowHandler::request_close(self) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /crates/frui_core/src/app/tree/pointer_handler.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc}; 2 | 3 | use druid_shell::kurbo::Affine; 4 | 5 | use crate::prelude::{context::HitTestCxOS, events::PointerExit, PointerEvent}; 6 | 7 | use super::NodeRef; 8 | 9 | pub type HitTestEntries = Rc>>; 10 | 11 | #[derive(Default)] 12 | pub struct PointerHandler { 13 | /// Hit test results for last pointer down event. 14 | pointer_down_results: HitTestEntries, 15 | /// Hit test results for the last hover event. 16 | pointer_hover_results_last: HitTestEntries, 17 | } 18 | 19 | impl PointerHandler { 20 | pub fn handle_pointer_event(&mut self, root: NodeRef, event: PointerEvent) { 21 | match event { 22 | PointerEvent::PointerDown(_) => { 23 | self.hit_test(root, &self.pointer_down_results, &event); 24 | 25 | for (node, affine) in self.pointer_down_results.borrow_mut().iter() { 26 | self.handle_event(&node, event.transform(affine)); 27 | } 28 | } 29 | PointerEvent::PointerUp(_) => { 30 | // Call all nodes that were hit during PointerDown. 31 | for (node, affine) in self.pointer_down_results.borrow_mut().drain() { 32 | self.handle_event(&node, event.transform(&affine)); 33 | } 34 | } 35 | PointerEvent::PointerScroll(_) => { 36 | let results = HitTestEntries::default(); 37 | 38 | self.hit_test(root, &results, &event); 39 | 40 | for (node, affine) in results.borrow_mut().iter() { 41 | self.handle_event(&node, event.transform(affine)); 42 | } 43 | } 44 | PointerEvent::PointerMove(_) => { 45 | let new_results = HitTestEntries::default(); 46 | 47 | self.hit_test(root, &new_results, &event); 48 | 49 | // Dispatch to all widgets that got hit. 50 | for (node, affine) in new_results.borrow_mut().iter() { 51 | self.handle_event(&node, event.transform(affine)); 52 | } 53 | 54 | // Dispatch to widgets that lost "hover status" by this event. 55 | // Used to correctly dispatch PointerExit event. 56 | for (node, affine) in self 57 | .pointer_hover_results_last 58 | .borrow_mut() 59 | .iter() 60 | .filter(|(last, _)| !new_results.borrow_mut().contains_key(last)) 61 | { 62 | let event = event.transform(affine).raw(); 63 | let event = PointerEvent::PointerExit(PointerExit(event)); 64 | self.handle_event(&node, event); 65 | } 66 | 67 | self.pointer_hover_results_last = new_results; 68 | } 69 | _ => unreachable!(), 70 | } 71 | } 72 | 73 | fn hit_test(&self, node: NodeRef, new_hit_entries: &HitTestEntries, event: &PointerEvent) { 74 | let cx = HitTestCxOS::new(&node, new_hit_entries.clone(), Affine::default()); 75 | node.widget().hit_test_os(cx.clone(), event.pos()); 76 | } 77 | 78 | fn handle_event(&self, node: &NodeRef, event: PointerEvent) { 79 | let cx = HitTestCxOS::new(node, Rc::new(RefCell::default()), Affine::default()); 80 | node.widget().handle_event_os(cx.clone(), &event); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/frui_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(auto_traits)] 2 | #![feature(negative_impls)] 3 | #![feature(type_alias_impl_trait)] 4 | // 5 | #![allow(incomplete_features)] 6 | #![feature(specialization)] 7 | 8 | pub mod api; 9 | pub mod app; 10 | 11 | pub mod prelude { 12 | pub use super::{ 13 | api::{ 14 | contexts::build_cx::{ 15 | BuildCx, InheritedState, InheritedStateRef, InheritedStateRefMut, WidgetState, 16 | }, 17 | implementers::{inherited::InheritedWidget, view::ViewWidget}, 18 | impls::BoxedWidget, 19 | pointer_events::*, 20 | Widget, 21 | }, 22 | app::runner::native::run_app, 23 | }; 24 | 25 | pub use crate::render::{Offset, Size}; 26 | 27 | pub use druid_shell::piet::{Color, FontWeight}; 28 | 29 | // Macros exports. 30 | pub use frui_macros::{Builder, InheritedWidget, RenderWidget, ViewWidget}; 31 | 32 | // Core widgets exports. 33 | pub use super::api::local_key::LocalKey; 34 | } 35 | 36 | pub mod render { 37 | pub use crate::api::implementers::render::RenderWidget; 38 | 39 | pub use crate::api::contexts::render::*; 40 | pub use crate::app::runner::Canvas; 41 | pub use crate::app::TEXT_FACTORY; 42 | 43 | pub use druid_shell::{kurbo, piet}; 44 | pub use druid_shell::{ 45 | kurbo::{Affine, Point, Rect as DruidRect, Vec2}, 46 | piet::{Color, RenderContext}, 47 | }; 48 | } 49 | 50 | #[doc(hidden)] 51 | pub mod macro_exports { 52 | 53 | pub use crate::{ 54 | api::{ 55 | contexts::{ 56 | render::{LayoutCxOS, PaintCxOS}, 57 | RawBuildCx, 58 | }, 59 | implementers::{ 60 | InheritedWidgetOS, RawWidget, RenderWidgetOS, ViewWidgetOS, WidgetDerive, 61 | }, 62 | structural_eq::{StructuralEq, StructuralEqImpl}, 63 | WidgetPtr, 64 | }, 65 | prelude::Widget, 66 | render::{Canvas, Constraints, Offset, Size}, 67 | }; 68 | } 69 | 70 | #[doc(hidden)] 71 | pub use druid_shell; 72 | -------------------------------------------------------------------------------- /crates/frui_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frui_macros" 3 | version = "0.0.1" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["iglak "] 6 | description = "Macros for Frui UI framework" 7 | repository = "https://github.com/fruiframework/frui" 8 | edition = "2021" 9 | 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | quote = "1.0.10" 16 | proc-macro2 = "1.0.33" 17 | syn = { version = "1.0.82", features = ["full", "visit"] } 18 | proc-macro-crate = "1.2.1" 19 | -------------------------------------------------------------------------------- /crates/frui_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_diagnostic)] 2 | 3 | use proc_macro::TokenStream; 4 | 5 | mod macros; 6 | 7 | /// Implements `WidgetList` for tuples of size up to specified length. 8 | #[proc_macro] 9 | pub fn impl_widget_list(tokens: TokenStream) -> TokenStream { 10 | macros::impl_tuple_slice::impl_tuple_slice(tokens) 11 | } 12 | 13 | /// Seales trait and exports it with the visibility specified in the argument. 14 | #[proc_macro_attribute] 15 | pub fn sealed(args: TokenStream, body: TokenStream) -> TokenStream { 16 | macros::sealed::sealed(args.into(), body.into()) 17 | } 18 | 19 | /// Duplicates trait definition for each identifier specified. 20 | #[proc_macro_attribute] 21 | pub fn copy_trait_as(args: TokenStream, body: TokenStream) -> TokenStream { 22 | macros::copy_trait_as::copy_trait_as(body, args) 23 | } 24 | 25 | /// Derives builder method for every field. 26 | #[proc_macro_derive(Builder, attributes(alias_generic))] 27 | pub fn derive_builder(tokens: TokenStream) -> TokenStream { 28 | macros::builder::derive_builder(tokens) 29 | } 30 | 31 | // 32 | // Widget Implementations 33 | 34 | #[proc_macro_derive(ViewWidget)] 35 | pub fn view_widget(tokens: TokenStream) -> TokenStream { 36 | macros::widget_impl::View(&syn::parse_macro_input!(tokens as syn::ItemStruct)).into() 37 | } 38 | 39 | #[proc_macro_derive(InheritedWidget)] 40 | pub fn inherited_widget(tokens: TokenStream) -> TokenStream { 41 | macros::widget_impl::Inherited(&syn::parse_macro_input!(tokens as syn::ItemStruct)).into() 42 | } 43 | 44 | #[proc_macro_derive(RenderWidget)] 45 | pub fn render_widget(tokens: TokenStream) -> TokenStream { 46 | macros::widget_impl::Render(&syn::parse_macro_input!(tokens as syn::ItemStruct)).into() 47 | } 48 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; 2 | pub mod copy_trait_as; 3 | pub mod impl_tuple_slice; 4 | pub mod sealed; 5 | pub mod widget_impl; 6 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/copy_trait_as.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::{Ident, TokenStream as TokenStream2}; 3 | use quote::ToTokens; 4 | use syn::{parse::Parse, punctuated::Punctuated, ItemTrait, Token}; 5 | 6 | pub fn copy_trait_as(body: TokenStream, args: TokenStream) -> TokenStream { 7 | let mut item: ItemTrait = syn::parse_macro_input!(body); 8 | let idents: Identifiers = syn::parse_macro_input!(args); 9 | 10 | let mut tokens = TokenStream2::new(); 11 | 12 | for ident in idents.0 { 13 | item.ident = ident; 14 | item.to_tokens(&mut tokens); 15 | } 16 | 17 | tokens.into() 18 | } 19 | 20 | struct Identifiers(Punctuated); 21 | 22 | impl Parse for Identifiers { 23 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 24 | let v = input.parse_terminated(Ident::parse)?; 25 | Ok(Self(v)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/impl_tuple_slice.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{parse::Parse, Expr, ExprRange}; 4 | 5 | pub fn impl_tuple_slice(tokens: TokenStream) -> TokenStream { 6 | let input = syn::parse_macro_input!(tokens as Range); 7 | 8 | let o = (input.start..input.end).map(|args_count| { 9 | let bounds_1 = (0..args_count).map(|i| format_ident!("_{i}")); 10 | let bounds_2 = (0..args_count).map(|i| format_ident!("_{i}")); 11 | let bounds_3 = (0..args_count).map(syn::Index::from); 12 | 13 | quote! { 14 | #[doc(hidden)] 15 | impl< #(#bounds_1 : Widget),* > WidgetList for ( #(#bounds_2,)* ) { 16 | fn get(&self) -> Vec<&dyn Widget> { 17 | vec![ #( &self.#bounds_3 ),* ] 18 | } 19 | } 20 | } 21 | }); 22 | 23 | (quote! { #(#o)* }).into() 24 | } 25 | 26 | #[derive(Debug)] 27 | struct Range { 28 | start: usize, 29 | end: usize, 30 | } 31 | 32 | impl Parse for Range { 33 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 34 | let range = input.parse::()?; 35 | Ok(Self { 36 | start: extract_usize(&range.from), 37 | end: extract_usize(&range.to), 38 | }) 39 | } 40 | } 41 | 42 | fn extract_usize(range: &Option>) -> usize { 43 | match range.as_ref().unwrap().as_ref() { 44 | Expr::Lit(lit) => match &lit.lit { 45 | syn::Lit::Int(v) => v.base10_parse().unwrap(), 46 | _ => panic!(), 47 | }, 48 | _ => panic!(), 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/sealed.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::quote; 4 | use syn::Visibility; 5 | 6 | pub fn sealed(args: TokenStream, body: TokenStream) -> TokenStream { 7 | let vis: Visibility = syn::parse_macro_input!(args); 8 | let body: TokenStream2 = body.into(); 9 | 10 | quote! { 11 | pub(#vis) use sealed::*; 12 | 13 | mod sealed { 14 | use super::*; 15 | 16 | #body 17 | } 18 | } 19 | .into() 20 | } 21 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/widget_impl/RawWidget.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::ItemStruct; 4 | 5 | use super::{exports_path, WidgetKind}; 6 | 7 | pub fn impl_raw_widget(item: &ItemStruct, widget_kind: WidgetKind) -> TokenStream { 8 | let WidgetKindOS = kind_to_os(widget_kind); 9 | 10 | #[rustfmt::skip] 11 | let Imports { 12 | Vec, TypeId, 13 | RawWidget, WidgetPtr, 14 | RawBuildCx, LayoutCxOS, PaintCxOS, Canvas, 15 | Size, Offset, Constraints, 16 | } = imports_impl_widget_os(); 17 | 18 | let Target = &item.ident; 19 | let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl(); 20 | 21 | quote! { 22 | impl #impl_generics #RawWidget for #Target #ty_generics #where_clause { 23 | fn build<'w>(&'w self, cx: &'w #RawBuildCx) -> #Vec<#WidgetPtr<'w>> { 24 | ::build(self, cx) 25 | } 26 | 27 | fn layout(&self, cx: #LayoutCxOS, constraints: #Constraints) -> #Size { 28 | ::layout(self, cx, constraints) 29 | } 30 | 31 | fn paint(&self, cx: #PaintCxOS, canvas: &mut #Canvas, offset: &#Offset) { 32 | ::paint(self, cx, canvas, offset) 33 | } 34 | 35 | fn inherited_key(&self) -> Option<#TypeId> { 36 | ::inherited_key(self) 37 | } 38 | } 39 | } 40 | } 41 | 42 | fn kind_to_os(widget_kind: WidgetKind) -> TokenStream { 43 | let exports = exports_path(); 44 | 45 | match widget_kind { 46 | WidgetKind::View => quote!(#exports::ViewWidgetOS), 47 | WidgetKind::Inherited => quote!(#exports::InheritedWidgetOS), 48 | WidgetKind::Render => quote!(#exports::RenderWidgetOS), 49 | } 50 | } 51 | 52 | struct Imports { 53 | // Standard 54 | Vec: TokenStream, 55 | TypeId: TokenStream, 56 | // Traits 57 | RawWidget: TokenStream, 58 | WidgetPtr: TokenStream, 59 | // Contextes 60 | RawBuildCx: TokenStream, 61 | LayoutCxOS: TokenStream, 62 | Canvas: TokenStream, 63 | PaintCxOS: TokenStream, 64 | // Types 65 | Size: TokenStream, 66 | Offset: TokenStream, 67 | Constraints: TokenStream, 68 | } 69 | 70 | fn imports_impl_widget_os() -> Imports { 71 | let exports = exports_path(); 72 | 73 | Imports { 74 | Vec: quote!(::std::vec::Vec), 75 | TypeId: quote!(::std::any::TypeId), 76 | RawWidget: quote!(#exports::RawWidget), 77 | WidgetPtr: quote!(#exports::WidgetPtr), 78 | RawBuildCx: quote!(#exports::RawBuildCx), 79 | LayoutCxOS: quote!(#exports::LayoutCxOS), 80 | Canvas: quote!(#exports::Canvas), 81 | PaintCxOS: quote!(#exports::PaintCxOS), 82 | Size: quote!(#exports::Size), 83 | Offset: quote!(#exports::Offset), 84 | Constraints: quote!(#exports::Constraints), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/widget_impl/StructuralEq.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Literal, TokenStream}; 2 | use quote::{quote, ToTokens}; 3 | use syn::ItemStruct; 4 | 5 | use super::exports_path; 6 | 7 | pub fn impl_structural_eq(item: &ItemStruct) -> TokenStream { 8 | let Imports { 9 | StructuralEqImpl, .. 10 | } = imports(); 11 | 12 | let (eq_enabled, eq_impl) = eq_impl(item.clone()); 13 | 14 | let Target = &item.ident; 15 | let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl(); 16 | 17 | quote! { 18 | unsafe impl #impl_generics #StructuralEqImpl for #Target #ty_generics #where_clause { 19 | const EQ_ENABLED: bool = #eq_enabled; 20 | 21 | fn eq(&self, other: &Self) -> bool { 22 | #eq_impl 23 | } 24 | } 25 | } 26 | } 27 | 28 | fn eq_impl(input: ItemStruct) -> (bool, TokenStream) { 29 | let Imports { StructuralEq, .. } = imports(); 30 | 31 | // Constant-evaluated (optimized) expression indicating if all fields are cheap to compare. 32 | // That means that e.g. no field contains another widget, which would cause recursive 33 | // equality tests of the widget subtree to be performed. 34 | let fields_cheap_to_eq = input.fields.iter().map(|t| { 35 | let ty = &t.ty; 36 | quote!(<#ty as #StructuralEq>::EQ_ENABLED &&) 37 | }); 38 | 39 | let fields_eq = input.fields.iter().enumerate().map(|(n, t)| { 40 | let field_ident = t.ident.clone().map_or( 41 | // Unnamed struct field. 42 | Literal::usize_unsuffixed(n).to_token_stream(), 43 | // Named struct field. 44 | |v| v.to_token_stream(), 45 | ); 46 | quote!(#StructuralEq::eq(&self.#field_ident, &other.#field_ident) &&) 47 | }); 48 | 49 | let eq_impl = quote! (#(#fields_cheap_to_eq)* #(#fields_eq)* true); 50 | 51 | let cheap_to_cmp = input.fields.len() == 0; 52 | 53 | (cheap_to_cmp, eq_impl) 54 | } 55 | 56 | struct Imports { 57 | StructuralEq: TokenStream, 58 | StructuralEqImpl: TokenStream, 59 | } 60 | 61 | fn imports() -> Imports { 62 | let exports = exports_path(); 63 | 64 | Imports { 65 | StructuralEq: quote!(#exports::StructuralEq), 66 | StructuralEqImpl: quote! { #exports::StructuralEqImpl }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/widget_impl/WidgetDerive.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream as TokenStream2}; 2 | use quote::{format_ident, quote, ToTokens}; 3 | use syn::{GenericParam, Generics, Ident, ItemStruct, Lifetime, LifetimeDef, TypeParamBound}; 4 | 5 | use super::unique_type_id; 6 | 7 | pub fn impl_widget_derive(input: &ItemStruct, target: &Ident) -> TokenStream2 { 8 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 9 | 10 | let super::Imports { 11 | LT, 12 | Widget, 13 | WidgetDerive, 14 | .. 15 | } = super::imports(); 16 | 17 | let Generated { 18 | Alias, 19 | AliasWhere, 20 | AliasParams, 21 | AssocParams, 22 | } = generate(target, &input.generics); 23 | 24 | let UniqueTypeId = unique_type_id(target); 25 | 26 | quote! { 27 | // At the moment of implementing this, TAIT (Type Alias Implement Trait feature) 28 | // doesn't work inside of the definition of `WidgetDerive` implementation. 29 | // 30 | // As a workaround we use a secondary type alias that is outside of `WidgetDerive` 31 | // definition, which helps the compiler to correctly infer type from `build` method. 32 | // 33 | #[doc(hidden)] 34 | type #Alias #AliasParams #AliasWhere = impl #Widget + #LT; 35 | 36 | impl #impl_generics #WidgetDerive for #target #ty_generics #where_clause { 37 | type Widget<#LT> = #Alias #AssocParams where Self: #LT; 38 | 39 | type UniqueTypeId = #UniqueTypeId; 40 | } 41 | } 42 | } 43 | 44 | struct Generated { 45 | Alias: Ident, 46 | AliasWhere: TokenStream2, 47 | AliasParams: Generics, 48 | AssocParams: TokenStream2, 49 | } 50 | 51 | fn generate(name: &Ident, generics: &Generics) -> Generated { 52 | let mut AliasParams = generics.clone(); 53 | 54 | for param in AliasParams.type_params_mut() { 55 | // Add 'frui for each generic bound. 56 | // 57 | // E.g.: 58 | param.bounds.push(frui_lt_t()); 59 | } 60 | 61 | AliasParams.params.insert(0, frui_lt_g()); 62 | 63 | let AssocParams = { 64 | let mut generics = generics.clone(); 65 | generics.params.insert(0, frui_lt_g()); 66 | generics.split_for_impl().1.to_token_stream() 67 | }; 68 | 69 | let Alias = format_ident!("FruiInferWidgetFor{}", name); 70 | 71 | Generated { 72 | Alias, 73 | AliasWhere: AliasParams.split_for_impl().2.to_token_stream(), 74 | AliasParams, 75 | AssocParams, 76 | } 77 | } 78 | 79 | fn frui_lt_t() -> TypeParamBound { 80 | TypeParamBound::Lifetime(Lifetime::new("'frui", Span::call_site())) 81 | } 82 | 83 | fn frui_lt_g() -> GenericParam { 84 | GenericParam::Lifetime(LifetimeDef::new(Lifetime::new("'frui", Span::call_site()))) 85 | } 86 | -------------------------------------------------------------------------------- /crates/frui_macros/src/macros/widget_impl/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(bad_style)] 2 | 3 | mod RawWidget; 4 | mod StructuralEq; 5 | mod WidgetDerive; 6 | 7 | use proc_macro2::{Ident, Span, TokenStream}; 8 | use proc_macro_crate::FoundCrate; 9 | use quote::{format_ident, quote}; 10 | use syn::ItemStruct; 11 | 12 | use self::{ 13 | RawWidget::impl_raw_widget, StructuralEq::impl_structural_eq, WidgetDerive::impl_widget_derive, 14 | }; 15 | 16 | // 17 | // Exports 18 | 19 | pub fn View(structure: &ItemStruct) -> TokenStream { 20 | impl_widget(structure, WidgetKind::View) 21 | } 22 | 23 | pub fn Inherited(structure: &ItemStruct) -> TokenStream { 24 | impl_widget(structure, WidgetKind::Inherited) 25 | } 26 | 27 | pub fn Render(structure: &ItemStruct) -> TokenStream { 28 | impl_widget(structure, WidgetKind::Render) 29 | } 30 | 31 | // 32 | // Impl 33 | 34 | #[derive(Debug, Clone, Copy)] 35 | pub enum WidgetKind { 36 | View, 37 | Inherited, 38 | Render, 39 | } 40 | 41 | fn impl_widget(input: &ItemStruct, kind: WidgetKind) -> TokenStream { 42 | let Target = &input.ident; 43 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 44 | 45 | let Imports { 46 | LT, 47 | Widget, 48 | RawWidget, 49 | .. 50 | } = imports(); 51 | 52 | let UniqueTypeId = unique_type_id(Target); 53 | 54 | let RawWidgetImplementation = impl_raw_widget(input, kind); 55 | let StructuralEqImplementation = impl_structural_eq(input); 56 | let WidgetDeriveImplementation = impl_widget_derive(input, Target); 57 | 58 | quote! { 59 | // Combine different WidgetKind implementations into one `RawWidget`. 60 | #RawWidgetImplementation 61 | 62 | // Implement StructuralEq 63 | #StructuralEqImplementation 64 | 65 | // Automatically derive `type Self::Widget<'a> = impl Widget`. 66 | #WidgetDeriveImplementation 67 | 68 | #[doc(hidden)] 69 | pub enum #UniqueTypeId {} 70 | 71 | impl #impl_generics #Widget for #Target #ty_generics #where_clause { 72 | fn as_raw<#LT>(&#LT self) -> &#LT dyn #RawWidget { 73 | self 74 | } 75 | } 76 | } 77 | } 78 | 79 | pub fn exports_path() -> TokenStream { 80 | let frui = proc_macro_crate::crate_name("frui"); 81 | let frui_core = proc_macro_crate::crate_name("frui_core"); 82 | let frui_widgets = proc_macro_crate::crate_name("frui_widgets"); 83 | 84 | let frui = match (frui, frui_core, frui_widgets) { 85 | (Ok(f), _, _) => into_ident(f), 86 | (_, Ok(f), _) => into_ident(f), 87 | (_, _, Ok(f)) => into_ident(f), 88 | (Err(_), Err(_), Err(_)) => panic!("couldn't locate frui crate path"), 89 | }; 90 | 91 | fn into_ident(crate_: FoundCrate) -> TokenStream { 92 | match crate_ { 93 | FoundCrate::Itself => quote!(crate), 94 | FoundCrate::Name(name) => { 95 | let ident = Ident::new(&name, Span::call_site()); 96 | quote!( ::#ident ) 97 | } 98 | } 99 | } 100 | 101 | quote!(#frui::macro_exports) 102 | } 103 | 104 | fn unique_type_id(name: &Ident) -> Ident { 105 | format_ident!("FruiUniqueTypeIdFor{}", name) 106 | } 107 | 108 | struct Imports { 109 | LT: TokenStream, 110 | Widget: TokenStream, 111 | RawWidget: TokenStream, 112 | WidgetDerive: TokenStream, 113 | } 114 | 115 | fn imports() -> Imports { 116 | let exports = exports_path(); 117 | 118 | Imports { 119 | LT: quote! { 'frui }, 120 | Widget: quote! { #exports::Widget }, 121 | RawWidget: quote! { #exports::RawWidget }, 122 | WidgetDerive: quote! { #exports::WidgetDerive }, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/frui_widgets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frui_widgets" 3 | version = "0.0.1" 4 | license = "MIT OR Apache-2.0" 5 | authors = ["iglak "] 6 | description = "Essential widgets for Frui UI framework" 7 | repository = "https://github.com/fruiframework/frui" 8 | edition = "2021" 9 | 10 | 11 | [dependencies] 12 | frui = { path = "../frui_core", package = "frui_core", version = "0.0.1" } 13 | frui_macros = { path = "../frui_macros", package = "frui_macros", version = "0.0.1" } 14 | 15 | log = "0.4.17" 16 | druid-shell = { git = "https://github.com/linebender/druid.git", rev = "ac3815114c65d46fd388431d3013a9412501916b" } 17 | 18 | [features] 19 | miri = [] 20 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/basic.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use frui::prelude::*; 4 | use frui::render::*; 5 | 6 | use crate::{Alignment, BoxLayoutData, Directional, EdgeInsets, TextDirection}; 7 | 8 | pub trait ChildParentDataProvider { 9 | fn ensure_parent_data(&self, cx: &LayoutCx, default: F) 10 | where 11 | F: Fn() -> P, 12 | P: 'static; 13 | } 14 | 15 | impl ChildParentDataProvider for T { 16 | fn ensure_parent_data(&self, cx: &LayoutCx, default: F) 17 | where 18 | F: Fn() -> P, 19 | P: 'static, 20 | { 21 | for child in cx.children() { 22 | if child.try_parent_data::

().is_none() { 23 | let data = default(); 24 | child.set_parent_data(data); 25 | } 26 | } 27 | } 28 | } 29 | 30 | #[derive(InheritedWidget, Builder)] 31 | pub struct Directionality { 32 | pub direction: TextDirection, 33 | pub child: T, 34 | } 35 | 36 | impl WidgetState for Directionality { 37 | type State = TextDirection; 38 | 39 | fn create_state(&self) -> Self::State { 40 | self.direction 41 | } 42 | } 43 | 44 | impl InheritedWidget for Directionality { 45 | fn build<'w>(&'w self) -> Self::Widget<'w> { 46 | &self.child 47 | } 48 | } 49 | 50 | impl Directionality<()> { 51 | pub fn of(cx: &LayoutCx) -> Option { 52 | let state = cx.depend_on_inherited_widget::(); 53 | state.map(|s| *s.as_ref().deref()) 54 | } 55 | 56 | pub fn of_or_default(cx: &LayoutCx) -> TextDirection { 57 | Self::of(cx).unwrap_or_default() 58 | } 59 | 60 | pub fn unwrap_or_default( 61 | text_direction: Option, 62 | cx: &LayoutCx, 63 | ) -> TextDirection { 64 | text_direction.unwrap_or_else(|| Self::of_or_default(cx)) 65 | } 66 | } 67 | 68 | #[derive(RenderWidget, Builder)] 69 | pub struct Align> { 70 | pub child: T, 71 | pub alignment: A, 72 | pub widgh_factor: Option, 73 | pub height_factor: Option, 74 | pub text_direction: Option, 75 | } 76 | 77 | impl Align<(), Alignment> { 78 | pub fn builder() -> Self { 79 | Self { 80 | child: (), 81 | alignment: Alignment::default(), 82 | widgh_factor: None, 83 | height_factor: None, 84 | text_direction: None, 85 | } 86 | } 87 | } 88 | 89 | impl RenderWidget for Align 90 | where 91 | T: Widget, 92 | A: Directional, 93 | { 94 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 95 | vec![&self.child] 96 | } 97 | 98 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 99 | self.ensure_parent_data(cx, || BoxLayoutData::default()); 100 | let text_direction = self 101 | .text_direction 102 | .unwrap_or_else(|| Directionality::of_or_default(cx)); 103 | let alignment = self.alignment.resolve(&text_direction); 104 | let shrink_wrap_width = 105 | self.widgh_factor.is_some() || constraints.max_width == f64::INFINITY; 106 | let shrink_wrap_height = 107 | self.height_factor.is_some() || constraints.max_height == f64::INFINITY; 108 | 109 | let child = cx.child(0); 110 | let child_size = child.layout(constraints.loosen()); 111 | let size = constraints.constrain(Size::new( 112 | if shrink_wrap_width { 113 | child_size.width * self.widgh_factor.unwrap_or(1.0) 114 | } else { 115 | f64::INFINITY 116 | }, 117 | if shrink_wrap_height { 118 | child_size.height * self.height_factor.unwrap_or(1.0) 119 | } else { 120 | f64::INFINITY 121 | }, 122 | )); 123 | let mut child_parent_data = child.try_parent_data_mut::().unwrap(); 124 | child_parent_data.offset = alignment.along(size - child_size); 125 | size 126 | } 127 | 128 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 129 | let child_offset = cx 130 | .child(0) 131 | .try_parent_data::() 132 | .unwrap() 133 | .offset; 134 | cx.child(0).paint(canvas, &(child_offset + *offset)) 135 | } 136 | } 137 | 138 | #[derive(RenderWidget, Builder)] 139 | pub struct Padding> { 140 | pub child: T, 141 | pub padding: P, 142 | } 143 | 144 | impl Padding<(), EdgeInsets> { 145 | pub fn builder() -> Self { 146 | Self { 147 | child: (), 148 | padding: EdgeInsets::ZERO, 149 | } 150 | } 151 | } 152 | 153 | impl RenderWidget for Padding 154 | where 155 | T: Widget, 156 | P: Directional, 157 | { 158 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 159 | vec![&self.child] 160 | } 161 | 162 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 163 | self.ensure_parent_data(cx, BoxLayoutData::default); 164 | let text_direction = Directionality::of_or_default(cx); 165 | let padding = self.padding.resolve(&text_direction); 166 | let child_constraints = padding.deflate_constraints(&constraints); 167 | let child_size = cx.child(0).layout(child_constraints); 168 | let child = cx.child(0); 169 | let mut child_parent_data = child.try_parent_data_mut::().unwrap(); 170 | child_parent_data.offset = padding.top_left(); 171 | constraints.constrain(child_size + padding.collapsed_size()) 172 | } 173 | 174 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 175 | let child_offset = cx 176 | .child(0) 177 | .try_parent_data::() 178 | .unwrap() 179 | .offset; 180 | cx.child(0).paint(canvas, &(*offset + child_offset)) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/boxes.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | use crate::BoxLayoutData; 5 | 6 | #[derive(RenderWidget, Default, Builder)] 7 | pub struct ConstrainedBox { 8 | pub child: T, 9 | pub constraints: Constraints, 10 | } 11 | 12 | impl ParentData for ConstrainedBox { 13 | type Data = BoxLayoutData; 14 | 15 | fn create_data(&self) -> Self::Data { 16 | BoxLayoutData::default() 17 | } 18 | } 19 | 20 | impl RenderWidget for ConstrainedBox { 21 | fn build<'w>(&'w self, _cx: BuildCx<'w, Self>) -> Vec> { 22 | vec![&self.child] 23 | } 24 | 25 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 26 | let constraints = self.constraints.enforce(constraints); 27 | let child_size = cx.child(0).layout(constraints); 28 | 29 | if child_size != Size::ZERO { 30 | child_size 31 | } else { 32 | self.constraints.enforce(constraints).constrain(Size::ZERO) 33 | } 34 | } 35 | 36 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 37 | cx.child(0).paint(canvas, offset) 38 | } 39 | } 40 | 41 | #[derive(RenderWidget, Builder)] 42 | pub struct UnconstrainedBox { 43 | pub child: T, 44 | } 45 | 46 | impl ParentData for UnconstrainedBox { 47 | type Data = BoxLayoutData; 48 | 49 | fn create_data(&self) -> Self::Data { 50 | BoxLayoutData::default() 51 | } 52 | } 53 | 54 | impl RenderWidget for UnconstrainedBox { 55 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 56 | vec![&self.child] 57 | } 58 | 59 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 60 | let child_size = cx.child(0).layout(constraints.loosen()); 61 | if child_size != Size::ZERO { 62 | child_size 63 | } else { 64 | constraints.biggest() 65 | } 66 | } 67 | 68 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 69 | cx.child(0).paint(canvas, offset) 70 | } 71 | } 72 | pub struct SizedBox; 73 | 74 | impl SizedBox { 75 | pub fn from_size(child: T, size: Size) -> impl Widget { 76 | ConstrainedBox { 77 | child, 78 | constraints: Constraints::new_tight_for(Some(size.width), Some(size.height)), 79 | } 80 | } 81 | 82 | pub fn new(child: T, width: Option, height: Option) -> impl Widget { 83 | ConstrainedBox { 84 | child, 85 | constraints: Constraints::new_tight_for(width, height), 86 | } 87 | } 88 | 89 | pub fn square(child: T, size: f64) -> impl Widget { 90 | Self::from_size(child, Size::new(size, size)) 91 | } 92 | 93 | pub fn shrink(child: T) -> impl Widget { 94 | ConstrainedBox { 95 | child, 96 | constraints: Constraints::new_tight_for(None, None), 97 | } 98 | } 99 | } 100 | 101 | #[derive(RenderWidget, Builder)] 102 | pub struct ColoredBox { 103 | pub child: T, 104 | pub color: Color, 105 | } 106 | 107 | impl RenderWidget for ColoredBox { 108 | fn build<'w>(&'w self, _cx: BuildCx<'w, Self>) -> Vec> { 109 | vec![&self.child] 110 | } 111 | 112 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 113 | let child_size = cx.child(0).layout(constraints); 114 | if child_size != Size::ZERO { 115 | child_size 116 | } else { 117 | constraints.smallest() 118 | } 119 | } 120 | 121 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 122 | let rect = Rect::from_origin_size(*offset, cx.size()); 123 | let brush = &canvas.solid_brush(self.color.clone()); 124 | canvas.fill(druid_shell::piet::kurbo::Rect::from(rect), brush); 125 | cx.child(0).paint(canvas, offset) 126 | } 127 | } 128 | 129 | #[derive(RenderWidget, Builder)] 130 | pub struct LimitedBox { 131 | pub child: T, 132 | pub max_width: f64, 133 | pub max_height: f64, 134 | } 135 | 136 | impl LimitedBox<()> { 137 | pub fn builder() -> Self { 138 | Self { 139 | child: (), 140 | max_width: f64::INFINITY, 141 | max_height: f64::INFINITY, 142 | } 143 | } 144 | } 145 | 146 | impl LimitedBox { 147 | fn limit_constraints(&self, constraints: &Constraints) -> Constraints { 148 | Constraints { 149 | min_width: constraints.min_width, 150 | max_width: if constraints.has_bounded_width() { 151 | constraints.max_width 152 | } else { 153 | constraints.constrain_width(self.max_width) 154 | }, 155 | min_height: constraints.min_height, 156 | max_height: if constraints.has_bounded_height() { 157 | constraints.max_height 158 | } else { 159 | constraints.constrain_height(self.max_height) 160 | }, 161 | } 162 | } 163 | } 164 | 165 | impl RenderWidget for LimitedBox { 166 | fn build<'w>(&'w self, _cx: BuildCx<'w, Self>) -> Vec> { 167 | vec![&self.child] 168 | } 169 | 170 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 171 | let limited_constraints = self.limit_constraints(&constraints); 172 | constraints.constrain(cx.child(0).layout(limited_constraints)) 173 | } 174 | 175 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 176 | cx.child(0).paint(canvas, offset) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/container.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | use crate::{BoxDecoration, Decoration, DecorationPosition, DefaultBoxDecoration, TextDirection}; 5 | 6 | #[derive(RenderWidget)] 7 | pub struct Container { 8 | child: W, 9 | width: Option, 10 | height: Option, 11 | color: Option, 12 | } 13 | 14 | impl Container<()> { 15 | pub fn builder() -> Container<()> { 16 | Container { 17 | child: (), 18 | width: None, 19 | height: None, 20 | color: None, 21 | } 22 | } 23 | } 24 | 25 | impl Container { 26 | pub fn child(self, child: C) -> Container { 27 | Container { 28 | child, 29 | width: self.width, 30 | height: self.height, 31 | color: self.color, 32 | } 33 | } 34 | 35 | #[track_caller] 36 | pub fn width(mut self, width: f64) -> Self { 37 | assert!(width >= 0.0, "width must be >= 0.0"); 38 | self.width = Some(width); 39 | self 40 | } 41 | 42 | #[track_caller] 43 | pub fn height(mut self, height: f64) -> Self { 44 | assert!(height >= 0.0, "height must be >= 0.0"); 45 | self.height = Some(height); 46 | self 47 | } 48 | 49 | pub fn color(mut self, color: Color) -> Self { 50 | self.color = Some(color); 51 | self 52 | } 53 | } 54 | 55 | impl RenderWidget for Container { 56 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 57 | vec![&self.child] 58 | } 59 | 60 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 61 | let constraints = Constraints::new_tight_for(self.width, self.height).enforce(constraints); 62 | 63 | cx.child(0).layout(constraints) 64 | } 65 | 66 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 67 | if let Some(color) = &self.color { 68 | let brush = &canvas.solid_brush(color.clone()); 69 | canvas.fill(DruidRect::from_origin_size(offset, cx.size()), brush); 70 | } 71 | 72 | cx.child(0).paint(canvas, offset) 73 | } 74 | } 75 | 76 | #[derive(RenderWidget, Builder)] 77 | pub struct DecoratedBox { 78 | pub child: W, 79 | pub decoration: D, 80 | pub position: DecorationPosition, 81 | } 82 | 83 | impl DecoratedBox<(), DefaultBoxDecoration> { 84 | pub fn builder() -> Self { 85 | Self { 86 | child: (), 87 | decoration: BoxDecoration::builder(), 88 | position: DecorationPosition::Background, 89 | } 90 | } 91 | } 92 | 93 | impl RenderWidget for DecoratedBox { 94 | fn build<'w>(&'w self, _cx: BuildCx<'w, Self>) -> Vec> { 95 | vec![&self.child] 96 | } 97 | 98 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 99 | constraints.constrain(cx.child(0).layout(constraints)) 100 | } 101 | 102 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 103 | let rect = Rect::from_origin_size(offset, cx.size()); 104 | let path = self 105 | .decoration 106 | .get_clip_path(rect.into(), &TextDirection::Ltr); 107 | if self.position == DecorationPosition::Background { 108 | self.decoration.paint(canvas, rect.into(), offset); 109 | } 110 | canvas 111 | .with_save(|c| { 112 | c.clip(path); 113 | cx.child(0).paint(c, offset); 114 | Ok(()) 115 | }) 116 | .unwrap(); 117 | if self.position == DecorationPosition::Foreground { 118 | self.decoration.paint(canvas, rect.into(), offset); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/event_detectors/keyboard.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::KeyEvent; 2 | use frui::{ 3 | app::listeners::keyboard::{CallbackKey, KEYBOARD_EVENT_LISTENERS}, 4 | prelude::*, 5 | }; 6 | 7 | #[derive(ViewWidget)] 8 | pub struct KeyboardEventDetector { 9 | pub on_event: F, 10 | pub child: W, 11 | } 12 | 13 | impl WidgetState for KeyboardEventDetector { 14 | type State = Option; 15 | 16 | fn create_state<'a>(&'a self) -> Self::State { 17 | None 18 | } 19 | 20 | fn mount(&self, cx: BuildCx) { 21 | *cx.state_mut() = Some( 22 | KEYBOARD_EVENT_LISTENERS 23 | .with(|listeners| unsafe { listeners.borrow_mut().register(&self.on_event) }), 24 | ); 25 | } 26 | 27 | fn unmount(&self, cx: BuildCx) { 28 | let mut key = cx.state_mut(); 29 | KEYBOARD_EVENT_LISTENERS.with(|listeners| listeners.borrow_mut().unregister(key.unwrap())); 30 | *key = None; 31 | } 32 | } 33 | 34 | impl ViewWidget for KeyboardEventDetector { 35 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 36 | &self.child 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/event_detectors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod keyboard; 2 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/flex/center.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | #[derive(RenderWidget)] 5 | pub struct Center { 6 | pub child: W, 7 | } 8 | 9 | impl Center { 10 | pub fn child(child: W) -> Self { 11 | Center { child } 12 | } 13 | } 14 | 15 | impl RenderWidget for Center { 16 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 17 | vec![&self.child] 18 | } 19 | 20 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 21 | let child_size = cx.child(0).layout(constraints.loosen()); 22 | 23 | let mut size = constraints.biggest(); 24 | 25 | if constraints.max_height == f64::INFINITY { 26 | size.height = child_size.height; 27 | } else if constraints.max_width == f64::INFINITY { 28 | size.width = child_size.width; 29 | } 30 | 31 | size 32 | } 33 | 34 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 35 | let self_size = cx.size(); 36 | let child_size = cx.child(0).size(); 37 | 38 | let child_offset = Offset { 39 | x: offset.x + (self_size.width - child_size.width) / 2., 40 | y: offset.y + (self_size.height - child_size.height) / 2., 41 | }; 42 | 43 | cx.child(0).paint(canvas, &child_offset); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/flex/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use frui::prelude::*; 4 | use frui::render::*; 5 | 6 | pub use alignment::*; 7 | pub use center::*; 8 | pub use flex::*; 9 | pub use stack::*; 10 | 11 | pub mod alignment; 12 | pub mod center; 13 | pub mod flex; 14 | pub mod stack; 15 | 16 | #[derive(Debug, Clone, Copy, Default)] 17 | pub struct BoxLayoutData { 18 | pub offset: Offset, 19 | } 20 | 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 22 | pub enum MainAxisSize { 23 | Min, 24 | Max, 25 | } 26 | 27 | impl Default for MainAxisSize { 28 | fn default() -> Self { 29 | MainAxisSize::Min 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Copy)] 34 | pub enum MainAxisAlignment { 35 | Start, 36 | Center, 37 | End, 38 | SpaceBetween, 39 | /// Leading space will not work with `MainAxisAlignment::SpaceAround` and `MainAxisAlignment::SpaceEvenly` 40 | SpaceAround, 41 | SpaceEvenly, 42 | } 43 | 44 | impl Default for MainAxisAlignment { 45 | fn default() -> Self { 46 | MainAxisAlignment::Start 47 | } 48 | } 49 | 50 | #[derive(Debug, Clone, Copy)] 51 | pub enum CrossAxisSize { 52 | Min, 53 | Max, 54 | } 55 | 56 | impl Default for CrossAxisSize { 57 | fn default() -> Self { 58 | CrossAxisSize::Min 59 | } 60 | } 61 | 62 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 63 | pub enum CrossAxisAlignment { 64 | Start, 65 | Center, 66 | End, 67 | Stretch, 68 | Baseline, 69 | } 70 | 71 | impl Default for CrossAxisAlignment { 72 | fn default() -> Self { 73 | CrossAxisAlignment::Start 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 78 | pub enum FlexFit { 79 | Loose, 80 | Tight, 81 | } 82 | 83 | /// Used by flexible widgets to determine the flex factor of a child. 84 | #[derive(Debug, Clone, Copy)] 85 | pub struct FlexData { 86 | fit: FlexFit, 87 | flex_factor: usize, 88 | box_data: BoxLayoutData, 89 | } 90 | 91 | impl Default for FlexData { 92 | fn default() -> Self { 93 | FlexData { 94 | flex_factor: 0, 95 | fit: FlexFit::Loose, 96 | box_data: BoxLayoutData::default(), 97 | } 98 | } 99 | } 100 | 101 | impl Deref for FlexData { 102 | type Target = BoxLayoutData; 103 | 104 | fn deref(&self) -> &Self::Target { 105 | &self.box_data 106 | } 107 | } 108 | 109 | impl DerefMut for FlexData { 110 | fn deref_mut(&mut self) -> &mut Self::Target { 111 | &mut self.box_data 112 | } 113 | } 114 | 115 | #[derive(RenderWidget, Builder)] 116 | pub struct Flexible { 117 | pub fit: FlexFit, 118 | pub flex: usize, 119 | pub child: W, 120 | } 121 | 122 | impl Flexible<()> { 123 | pub fn new(child: impl Widget) -> Flexible { 124 | Flexible::builder().child(child) 125 | } 126 | 127 | pub fn builder() -> Self { 128 | Self { 129 | fit: FlexFit::Loose, 130 | flex: 1, 131 | child: (), 132 | } 133 | } 134 | } 135 | 136 | impl ParentData for Flexible { 137 | type Data = FlexData; 138 | 139 | fn create_data(&self) -> Self::Data { 140 | FlexData { 141 | flex_factor: self.flex, 142 | fit: self.fit, 143 | box_data: BoxLayoutData::default(), 144 | } 145 | } 146 | } 147 | 148 | impl RenderWidget for Flexible { 149 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 150 | vec![&self.child] 151 | } 152 | 153 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 154 | constraints.constrain(cx.child(0).layout(constraints)) 155 | } 156 | 157 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 158 | cx.child(0).paint(canvas, offset) 159 | } 160 | } 161 | 162 | pub struct Expanded; 163 | 164 | impl Expanded { 165 | pub fn new(child: T) -> Flexible { 166 | Flexible { 167 | fit: FlexFit::Tight, 168 | flex: 1, 169 | child, 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | mod basic; 4 | mod boxes; 5 | mod container; 6 | mod event_detectors; 7 | mod flex; 8 | mod painting; 9 | mod scroll; 10 | mod testing; 11 | mod text; 12 | mod transform; 13 | mod widget_list; 14 | 15 | pub use self::basic::*; 16 | pub use self::boxes::*; 17 | pub use self::container::*; 18 | pub use self::event_detectors::keyboard::*; 19 | pub use self::flex::*; 20 | pub use self::painting::*; 21 | pub use self::scroll::*; 22 | pub use self::testing::*; 23 | pub use self::text::*; 24 | pub use self::transform::*; 25 | pub use self::widget_list::*; 26 | 27 | #[doc(hidden)] 28 | pub use frui::macro_exports; 29 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/painting/border_radius.rs: -------------------------------------------------------------------------------- 1 | use frui::render::{RRect, Radius, Rect}; 2 | 3 | use crate::{Directional, TextDirection}; 4 | 5 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 6 | pub struct BorderRadius { 7 | pub top_left: Radius, 8 | pub top_right: Radius, 9 | pub bottom_left: Radius, 10 | pub bottom_right: Radius, 11 | } 12 | 13 | impl BorderRadius { 14 | pub const ZERO: BorderRadius = BorderRadius::all(Radius::ZERO); 15 | 16 | pub const fn all(radius: Radius) -> BorderRadius { 17 | BorderRadius { 18 | top_left: radius, 19 | top_right: radius, 20 | bottom_left: radius, 21 | bottom_right: radius, 22 | } 23 | } 24 | 25 | pub fn circular(radius: f64) -> BorderRadius { 26 | BorderRadius::all(Radius::circular(radius)) 27 | } 28 | 29 | pub fn only( 30 | top_left: Radius, 31 | top_right: Radius, 32 | bottom_left: Radius, 33 | bottom_right: Radius, 34 | ) -> BorderRadius { 35 | BorderRadius { 36 | top_left, 37 | top_right, 38 | bottom_left, 39 | bottom_right, 40 | } 41 | } 42 | 43 | pub fn vertical(top: Radius, bottom: Radius) -> BorderRadius { 44 | BorderRadius { 45 | top_left: top, 46 | top_right: top, 47 | bottom_left: bottom, 48 | bottom_right: bottom, 49 | } 50 | } 51 | 52 | pub fn horizontal(left: Radius, right: Radius) -> BorderRadius { 53 | BorderRadius { 54 | top_left: left, 55 | top_right: right, 56 | bottom_left: left, 57 | bottom_right: right, 58 | } 59 | } 60 | 61 | pub fn is_uniform(&self) -> bool { 62 | self.top_left == self.top_right 63 | && self.top_left == self.bottom_left 64 | && self.top_left == self.bottom_right 65 | } 66 | 67 | pub fn to_rrect(&self, rect: &Rect) -> RRect { 68 | RRect::from_rect_and_corners( 69 | rect.clone(), 70 | self.top_left, 71 | self.top_right, 72 | self.bottom_right, 73 | self.bottom_left, 74 | ) 75 | } 76 | } 77 | 78 | impl Directional for BorderRadius { 79 | type Output = BorderRadius; 80 | 81 | fn resolve(&self, _text_direction: &TextDirection) -> BorderRadius { 82 | *self 83 | } 84 | } 85 | 86 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 87 | pub struct BorderRadiusDirectional { 88 | pub top_start: Radius, 89 | pub top_end: Radius, 90 | pub bottom_start: Radius, 91 | pub bottom_end: Radius, 92 | } 93 | 94 | impl Directional for BorderRadiusDirectional { 95 | type Output = BorderRadius; 96 | 97 | fn resolve(&self, text_direction: &TextDirection) -> BorderRadius { 98 | match text_direction { 99 | TextDirection::Ltr => BorderRadius { 100 | top_left: self.top_start, 101 | top_right: self.top_end, 102 | bottom_left: self.bottom_start, 103 | bottom_right: self.bottom_end, 104 | }, 105 | TextDirection::Rtl => BorderRadius { 106 | top_left: self.top_end, 107 | top_right: self.top_start, 108 | bottom_left: self.bottom_end, 109 | bottom_right: self.bottom_start, 110 | }, 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/painting/borders.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Mul}; 2 | 3 | use druid_shell::{kurbo::BezPath, piet::StrokeStyle}; 4 | use frui::prelude::*; 5 | use frui::render::Rect; 6 | 7 | use crate::EdgeInsets; 8 | 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub enum BorderStyle { 11 | None, 12 | Solid, 13 | /// Dashed line with a gap at the start and end of the line. 14 | /// Follow the standard of PostScript 15 | /// 16 | /// Example: 17 | /// A dash line with 5px solid and 5px gap, and 2px solid and 5px gap, start offset 0px 18 | /// ```rust 19 | /// BorderStyle::Dashed(vec![5.0, 5.0, 2.0, 5.0], 0.0) 20 | /// ``` 21 | /// You can use offset to change the start position of the line and make an animation. 22 | Dash(Vec, f64), 23 | } 24 | 25 | /// A side of a border of a box. 26 | #[derive(Debug, Clone, PartialEq)] 27 | pub struct BorderSide { 28 | pub color: Color, 29 | pub width: f64, 30 | pub style: BorderStyle, 31 | } 32 | 33 | impl BorderSide { 34 | pub const NONE: BorderSide = BorderSide { 35 | color: Color::BLACK, 36 | width: 0.0, 37 | style: BorderStyle::None, 38 | }; 39 | 40 | pub fn merge(a: &BorderSide, b: &BorderSide) -> Self { 41 | assert!(BorderSide::can_merge(a, b)); 42 | let a_is_none = a.style == BorderStyle::None && a.width == 0.0; 43 | let b_is_none = b.style == BorderStyle::None && b.width == 0.0; 44 | if a_is_none && b_is_none { 45 | BorderSide::NONE 46 | } else if a_is_none { 47 | b.clone() 48 | } else if b_is_none { 49 | a.clone() 50 | } else { 51 | BorderSide { 52 | color: a.color.clone(), 53 | width: a.width + b.width, 54 | style: a.style.clone(), 55 | } 56 | } 57 | } 58 | 59 | pub fn can_merge(a: &BorderSide, b: &BorderSide) -> bool { 60 | if (a.style == BorderStyle::None && a.width == 0.0) 61 | || (b.style == BorderStyle::None && b.width == 0.0) 62 | { 63 | true 64 | } else { 65 | a.style == b.style && a.color == b.color 66 | } 67 | } 68 | 69 | pub fn copy_with( 70 | &self, 71 | color: Option, 72 | width: Option, 73 | style: Option, 74 | ) -> Self { 75 | BorderSide { 76 | color: color.unwrap_or(self.color.clone()), 77 | width: width.unwrap_or(self.width), 78 | style: style.unwrap_or(self.style.clone()), 79 | } 80 | } 81 | 82 | pub fn to_stroke_style(&self) -> Option { 83 | if let BorderStyle::Dash(ref dash, offset) = self.style { 84 | let mut stroke = StrokeStyle::new(); 85 | stroke.set_dash_pattern(dash.clone()); 86 | stroke.set_dash_offset(offset); 87 | Some(stroke) 88 | } else { 89 | None 90 | } 91 | } 92 | } 93 | 94 | impl Mul for BorderSide { 95 | type Output = Self; 96 | 97 | fn mul(self, rhs: f64) -> Self::Output { 98 | BorderSide { 99 | color: self.color, 100 | width: (self.width * rhs).max(0.0), 101 | style: if rhs <= 0.0 { 102 | BorderStyle::None 103 | } else { 104 | self.style 105 | }, 106 | } 107 | } 108 | } 109 | 110 | pub trait ShapeBorder: Add + Sized { 111 | fn dimensions(&self) -> EdgeInsets; 112 | 113 | fn stroke_path(&self, rect: Rect) -> BezPath; 114 | 115 | fn shape_path(&self, rect: Rect) -> BezPath; 116 | } 117 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/painting/decoration.rs: -------------------------------------------------------------------------------- 1 | use druid_shell::{ 2 | kurbo::{BezPath, Circle, RoundedRect}, 3 | piet::{kurbo::Shape, RenderContext}, 4 | }; 5 | use frui::{ 6 | prelude::*, 7 | render::{Canvas, Offset, Rect}, 8 | }; 9 | 10 | use crate::{ 11 | border_radius::BorderRadius, box_border::BoxShape, BoxBorder, BoxShadow, Directional, 12 | EdgeInsets, ShapeBorder, TextDirection, EPSILON, 13 | }; 14 | 15 | pub trait BoxPainter { 16 | fn paint(&self, canvas: &mut Canvas, offset: Offset); 17 | } 18 | 19 | pub trait Decoration { 20 | fn padding(&self) -> EdgeInsets; 21 | 22 | fn get_clip_path(&self, rect: Rect, text_direction: &TextDirection) -> BezPath; 23 | 24 | fn paint(&self, canvas: &mut Canvas, rect: Rect, offset: &Offset); 25 | } 26 | 27 | #[derive(Copy, Clone, Debug, PartialEq)] 28 | pub enum DecorationPosition { 29 | Background, 30 | Foreground, 31 | } 32 | 33 | pub struct BoxDecoration 34 | where 35 | B: Directional, 36 | BR: Directional, 37 | { 38 | pub color: Option, 39 | pub box_shadow: Vec, 40 | // TODO: image background 41 | // pub image: Option, 42 | pub border: Option, 43 | pub border_radius: Option
, 44 | // FIXME: implement gradient or merge it with color 45 | // pub gradient: Option, 46 | pub shape: BoxShape, 47 | pub text_direction: TextDirection, 48 | } 49 | 50 | pub type DefaultBoxDecoration = BoxDecoration; 51 | 52 | impl BoxDecoration { 53 | pub fn builder() -> DefaultBoxDecoration { 54 | Self { 55 | color: None, 56 | box_shadow: Vec::new(), 57 | border: None, 58 | border_radius: None, 59 | shape: BoxShape::Rectangle, 60 | text_direction: TextDirection::Ltr, 61 | } 62 | } 63 | } 64 | 65 | impl BoxDecoration 66 | where 67 | B: Directional, 68 | BR: Directional, 69 | { 70 | pub fn color(mut self, color: Color) -> Self { 71 | self.color = Some(color); 72 | self 73 | } 74 | 75 | pub fn box_shadow(mut self, box_shadow: Vec) -> Self { 76 | self.box_shadow.clear(); 77 | self.box_shadow.extend(box_shadow); 78 | self 79 | } 80 | 81 | pub fn border(self, border: BORDER) -> BoxDecoration 82 | where 83 | BORDER: Directional, 84 | { 85 | BoxDecoration:: { 86 | color: self.color, 87 | box_shadow: self.box_shadow, 88 | border: Some(border), 89 | border_radius: self.border_radius, 90 | shape: self.shape, 91 | text_direction: self.text_direction, 92 | } 93 | } 94 | 95 | pub fn border_radius(self, border_radius: RADIUS) -> BoxDecoration 96 | where 97 | RADIUS: Directional, 98 | { 99 | BoxDecoration:: { 100 | color: self.color, 101 | box_shadow: self.box_shadow, 102 | border: self.border, 103 | border_radius: Some(border_radius), 104 | shape: self.shape, 105 | text_direction: self.text_direction, 106 | } 107 | } 108 | 109 | pub fn shape(mut self, shape: BoxShape) -> Self { 110 | self.shape = shape; 111 | self 112 | } 113 | 114 | pub fn text_direction(mut self, text_direction: TextDirection) -> Self { 115 | self.text_direction = text_direction; 116 | self 117 | } 118 | } 119 | 120 | impl Decoration for BoxDecoration 121 | where 122 | B: Directional, 123 | BR: Directional, 124 | { 125 | fn padding(&self) -> EdgeInsets { 126 | self.border.as_ref().map_or(EdgeInsets::ZERO, |b| { 127 | b.resolve(&self.text_direction).dimensions() 128 | }) 129 | } 130 | 131 | fn get_clip_path(&self, rect: Rect, text_direction: &TextDirection) -> BezPath { 132 | match self.shape { 133 | BoxShape::Circle => { 134 | Circle::new(rect.center(), rect.width().min(rect.height()) / 2.0).to_path(EPSILON) 135 | } 136 | BoxShape::Rectangle => { 137 | if let Some(border_radius) = &self.border_radius { 138 | let border_radius = border_radius.resolve(text_direction); 139 | let rrect = border_radius.to_rrect(&rect); 140 | RoundedRect::try_from(rrect).unwrap().to_path(EPSILON) 141 | } else { 142 | druid_shell::piet::kurbo::Rect::from(rect).to_path(EPSILON) 143 | } 144 | } 145 | } 146 | } 147 | 148 | fn paint(&self, canvas: &mut Canvas, rect: Rect, offset: &Offset) { 149 | let path = self.get_clip_path(rect, &self.text_direction); 150 | 151 | // draw shadows 152 | if (self.shape != BoxShape::Rectangle || self.border_radius.is_some()) 153 | && !self.box_shadow.is_empty() 154 | { 155 | log::warn!("Box shadows are not supported for non-rectangular shapes (yet)"); 156 | } 157 | for shadow in &self.box_shadow { 158 | shadow.paint(canvas, rect, offset); 159 | } 160 | 161 | // draw background color 162 | if let Some(color) = &self.color { 163 | canvas.fill(path.clone(), color); 164 | } 165 | // or draw gradient 166 | // if let Some(gradient) = &self.gradient { 167 | // canvas.fill(rect, gradient); 168 | // } 169 | // FIXME: draw background image 170 | 171 | // draw border 172 | if let Some(border) = &self.border { 173 | let border = border.resolve(&self.text_direction); 174 | let radius = self 175 | .border_radius 176 | .as_ref() 177 | .map(|r| r.resolve(&self.text_direction)) 178 | .unwrap_or(BorderRadius::ZERO); 179 | border.paint(canvas, rect, Some(self.shape), radius) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/painting/mod.rs: -------------------------------------------------------------------------------- 1 | pub use border_radius::*; 2 | pub use borders::*; 3 | pub use box_border::*; 4 | pub use decoration::*; 5 | pub use edge_insets::*; 6 | pub use shadow::*; 7 | 8 | pub mod border_radius; 9 | pub mod borders; 10 | pub mod box_border; 11 | pub mod decoration; 12 | pub mod edge_insets; 13 | pub mod shadow; 14 | 15 | pub const EPSILON: f64 = 1E-9; 16 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/painting/shadow.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Mul; 2 | 3 | use druid_shell::{kurbo::Affine, piet::RenderContext}; 4 | use frui::{ 5 | prelude::*, 6 | render::{Canvas, Offset, Rect}, 7 | }; 8 | 9 | #[derive(Copy, Clone, Debug, PartialEq)] 10 | pub enum BlurStyle { 11 | Normal, 12 | Solid, 13 | Outer, 14 | Inner, 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq)] 18 | pub struct BoxShadow { 19 | pub color: Color, 20 | pub offset: Offset, 21 | pub blur_radius: f64, 22 | pub spread_radius: f64, 23 | pub blur_style: BlurStyle, 24 | } 25 | 26 | impl BoxShadow { 27 | pub fn paint(&self, canvas: &mut Canvas, rect: Rect, _offset: &Offset) { 28 | assert!( 29 | self.blur_style == BlurStyle::Normal, 30 | "Shadow now only supports BlurStyle::Normal blur style" 31 | ); 32 | let brush = canvas.solid_brush(self.color.clone()); 33 | if self.spread_radius < rect.shortest_side() { 34 | canvas 35 | .with_save(|c| { 36 | c.transform(Affine::translate((self.offset.x, self.offset.y))); 37 | let rect = rect.inflate(self.spread_radius); 38 | c.blurred_rect(rect.into(), self.blur_radius, &brush); 39 | Ok(()) 40 | }) 41 | .unwrap(); 42 | } else { 43 | log::warn!("Spread radius is larger than the shortest side of the rect, the shadow will not be painted"); 44 | } 45 | } 46 | } 47 | 48 | impl Mul for BoxShadow { 49 | type Output = Self; 50 | 51 | fn mul(self, rhs: f64) -> Self::Output { 52 | BoxShadow { 53 | color: self.color, 54 | offset: self.offset * rhs, 55 | blur_radius: self.blur_radius * rhs, 56 | spread_radius: self.spread_radius * rhs, 57 | blur_style: self.blur_style, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/scroll.rs: -------------------------------------------------------------------------------- 1 | //! This is a bad prototype. 2 | 3 | use frui::prelude::*; 4 | use frui::render::*; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub enum ScrollDirection { 8 | Horizontal, 9 | Vertical, 10 | // Todo: All, 11 | } 12 | 13 | /// Todo: Finish implementation. 14 | #[derive(RenderWidget, Builder)] 15 | pub struct Scroll { 16 | pub child: W, 17 | pub scroll_direction: ScrollDirection, 18 | } 19 | 20 | impl Scroll<()> { 21 | pub fn builder() -> Scroll<()> { 22 | Scroll { 23 | child: (), 24 | scroll_direction: ScrollDirection::Vertical, 25 | } 26 | } 27 | } 28 | 29 | #[doc(hidden)] 30 | pub struct ScrollState { 31 | scroll_offset: Vec2, 32 | } 33 | 34 | impl WidgetState for Scroll { 35 | type State = ScrollState; 36 | 37 | fn create_state(&self) -> Self::State { 38 | ScrollState { 39 | scroll_offset: Vec2::new(0., 0.), 40 | } 41 | } 42 | } 43 | 44 | impl RenderWidget for Scroll { 45 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 46 | vec![&self.child] 47 | } 48 | 49 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 50 | let child_constraints = match self.scroll_direction { 51 | ScrollDirection::Horizontal => Constraints { 52 | min_width: 0., 53 | max_width: f64::INFINITY, 54 | ..constraints 55 | }, 56 | ScrollDirection::Vertical => Constraints { 57 | min_height: 0., 58 | max_height: f64::INFINITY, 59 | ..constraints 60 | }, 61 | }; 62 | 63 | cx.child(0).layout(child_constraints); 64 | 65 | constraints.biggest() 66 | } 67 | 68 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 69 | if let Err(e) = canvas.save() { 70 | log::error!("saving render context failed: {:?}", e); 71 | return; 72 | } 73 | 74 | let viewport = Rect::from_origin_size(*offset, cx.size()); 75 | canvas.clip(druid_shell::piet::kurbo::Rect::from(viewport)); 76 | canvas.transform(Affine::translate(-cx.widget_state().scroll_offset)); 77 | 78 | cx.child(0).paint(canvas, offset); 79 | 80 | if let Err(e) = canvas.restore() { 81 | log::error!("restoring render context failed: {:?}", e); 82 | } 83 | 84 | // Todo: Draw scroll bar. 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/testing.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | use druid_shell::piet::{kurbo::Rect, Color, LineCap, RenderContext, StrokeStyle}; 5 | 6 | pub trait DebugContainerExt: Widget + Sized { 7 | fn debug_container(self) -> DebugContainer { 8 | DebugContainer { 9 | child: self, 10 | print_size: "", 11 | } 12 | } 13 | } 14 | 15 | impl DebugContainerExt for T {} 16 | 17 | #[derive(RenderWidget, Builder)] 18 | pub struct DebugContainer { 19 | pub child: W, 20 | /// Print to the console size of child widget computed during layout. It 21 | /// will not print to the console if str == "". 22 | pub print_size: &'static str, 23 | } 24 | 25 | impl DebugContainer { 26 | pub fn new(child: W) -> Self { 27 | Self { 28 | child, 29 | print_size: "", 30 | } 31 | } 32 | } 33 | 34 | impl RenderWidget for DebugContainer { 35 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 36 | vec![&self.child] 37 | } 38 | 39 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 40 | let size = cx.child(0).layout(constraints); 41 | 42 | if self.print_size != "" { 43 | println!("{} = {:?}", self.print_size, size); 44 | } 45 | 46 | size 47 | } 48 | 49 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 50 | cx.child(0).paint(canvas, offset); 51 | 52 | let rect = Rect::from_origin_size(*offset, cx.child(0).size()); 53 | let brush = &canvas.solid_brush(Color::GREEN); 54 | 55 | canvas.stroke_styled(rect, brush, 2., &StrokeStyle::new().line_cap(LineCap::Butt)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/text.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | use druid_shell::piet::{ 5 | kurbo::Point, Color, FontFamily, FontWeight, PietTextLayout, Text as TextExt, TextLayout, 6 | TextLayoutBuilder, 7 | }; 8 | 9 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 10 | pub enum TextDirection { 11 | Rtl, 12 | Ltr, 13 | } 14 | 15 | impl Default for TextDirection { 16 | fn default() -> Self { 17 | TextDirection::Ltr 18 | } 19 | } 20 | 21 | pub trait Directional { 22 | type Output; 23 | 24 | fn resolve(&self, text_direction: &TextDirection) -> Self::Output; 25 | } 26 | 27 | #[derive(RenderWidget, Builder)] 28 | pub struct Text> { 29 | text: S, 30 | size: f64, 31 | color: Color, 32 | weight: FontWeight, 33 | family: FontFamily, 34 | } 35 | 36 | impl> Text { 37 | pub fn new(string: S) -> Self { 38 | Self { 39 | text: string, 40 | size: 16., 41 | color: Color::WHITE, 42 | weight: FontWeight::default(), 43 | // Layout of `FontFamily::SYSTEM_UI` is incredibly slow. Other fonts 44 | // seem to render just fine. This issue is related to Piet. 45 | // 46 | // For now, the default will be `FontFamily::MONOSPACE`. 47 | family: FontFamily::MONOSPACE, 48 | } 49 | } 50 | } 51 | 52 | #[cfg(not(feature = "miri"))] 53 | impl> RenderState for Text { 54 | type State = PietTextLayout; 55 | 56 | fn create_state(&self) -> Self::State { 57 | TEXT_FACTORY.with(|f| f.get().new_text_layout("").build().unwrap()) 58 | } 59 | } 60 | 61 | #[cfg(not(feature = "miri"))] 62 | impl> RenderWidget for Text { 63 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 64 | vec![] as Vec<()> 65 | } 66 | 67 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 68 | let max_width = constraints.biggest().width; 69 | 70 | *cx.render_state_mut() = TEXT_FACTORY.with(|f| { 71 | f.get() 72 | .new_text_layout(self.text.as_ref().to_owned()) 73 | .font(self.family.clone(), self.size) 74 | .text_color(self.color.clone()) 75 | .range_attribute(.., self.weight) 76 | .max_width(max_width) 77 | .build() 78 | .unwrap() 79 | }); 80 | 81 | let text_size = cx.render_state().size().into(); 82 | 83 | constraints.constrain(text_size) 84 | } 85 | 86 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 87 | RenderContext::draw_text( 88 | canvas, 89 | &cx.render_state(), 90 | Point { 91 | x: offset.x, 92 | y: offset.y, 93 | }, 94 | ); 95 | } 96 | } 97 | 98 | #[cfg(feature = "miri")] 99 | pub struct TextRenderState([u8; 30]); 100 | 101 | #[cfg(feature = "miri")] 102 | impl> RenderState for Text { 103 | type State = TextRenderState; 104 | 105 | fn create_state(&self) -> Self::State { 106 | TextRenderState([1; 30]) 107 | } 108 | } 109 | 110 | #[cfg(feature = "miri")] 111 | impl> RenderWidget for Text { 112 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 113 | vec![] as Vec<()> 114 | } 115 | 116 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 117 | let _: &mut TextRenderState = &mut cx.render_state_mut(); 118 | 119 | Size { 120 | width: constraints.smallest().width, 121 | height: constraints.smallest().height, 122 | } 123 | } 124 | 125 | fn paint(&self, _: &mut PaintCx, _: &mut Canvas, _: &Offset) {} 126 | } 127 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/transform.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | use frui::render::*; 3 | 4 | #[derive(RenderWidget)] 5 | pub struct Transform(pub Affine, pub W); 6 | 7 | impl RenderWidget for Transform { 8 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Vec> { 9 | vec![&self.1] 10 | } 11 | 12 | fn layout(&self, cx: &LayoutCx, constraints: Constraints) -> Size { 13 | cx.child(0).layout(constraints) 14 | } 15 | 16 | fn paint(&self, cx: &mut PaintCx, canvas: &mut Canvas, offset: &Offset) { 17 | let r = canvas.with_save(|cv| { 18 | cv.transform(self.0); 19 | cx.child(0).paint(cv, offset); 20 | 21 | Ok(()) 22 | }); 23 | 24 | r.unwrap(); 25 | } 26 | } 27 | 28 | impl HitTest for Transform { 29 | fn hit_test<'a>(&'a self, cx: &'a mut HitTestCx, point: Point) -> bool { 30 | if cx.layout_box().contains(point) { 31 | for mut child in cx.children() { 32 | if child.hit_test_with_transform(point, self.0.inverse()) { 33 | return true; 34 | } 35 | } 36 | } 37 | 38 | false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/frui_widgets/src/widget_list.rs: -------------------------------------------------------------------------------- 1 | pub use frui::prelude::Widget; 2 | 3 | /// Represents a list of widgets. 4 | /// 5 | /// # Widget tuple 6 | /// 7 | /// Most often you will use [`WidgetList`] in form of a tuple: 8 | /// 9 | /// ``` 10 | /// Column::builder() 11 | /// .children((WidgetA, WidgetB, WidgetC)); 12 | /// ``` 13 | /// 14 | /// This pattern is great if you have a statically known list of widgets, since 15 | /// each child widget can have different type. 16 | /// 17 | /// Do note that currently only tuples with less than 50 widgets work. To handle 18 | /// bigger lists you can use [`wlist`] macro or [`Vec`]/array of widgets. 19 | /// 20 | /// 21 | /// # Widget list macro 22 | /// 23 | /// If tuple pattern doesn't make it for you, you can use [`wlist`] macro. It 24 | /// automatically erases types of provided children, boxes them and puts inside 25 | /// of a [`Vec`]. 26 | /// 27 | /// 28 | /// ``` 29 | /// Column::builder() 30 | /// .children(wlist![WidgetA, WidgetB, WidgetC]); 31 | /// ``` 32 | /// 33 | /// This pattern is great if number of widgets you have is greater than 50 since 34 | /// widget tuple works only up to 50 widgets. 35 | /// 36 | /// Do note that using [`wlist`] boxes every widget which may cause performance 37 | /// issues if wildly overused. 38 | /// 39 | /// ## Todo 40 | /// 41 | /// It is possible to avoid boxing and allocating vec for that widget list, I've 42 | /// written the idea on the Discord. Implement it. 43 | /// 44 | /// 45 | /// # Widget vector / array / slice 46 | /// 47 | /// As an alternative to the previous patterns, you can also use vectors, arrays 48 | /// and slices as [`WidgetList`]. Those come with their respectful constraints, 49 | /// like the fact that types of elements stored in these kinds of collections 50 | /// must be the same. 51 | /// 52 | /// [`wlist`]: (crate::wlist) 53 | pub trait WidgetList { 54 | fn get(&self) -> Vec<&dyn Widget>; 55 | } 56 | 57 | /// See [`WidgetList`] documentation for usage. 58 | #[macro_export] 59 | macro_rules! wlist { 60 | ($($x:expr),* $(,)?) => { 61 | std::vec![$($crate::macro_exports::BoxedWidget::boxed($x)),*] as Vec> 62 | } 63 | } 64 | 65 | frui_macros::impl_widget_list!(0..50); 66 | 67 | impl WidgetList for [W] { 68 | fn get(&self) -> Vec<&dyn Widget> { 69 | self.iter().map(|e| e as &dyn Widget).collect() 70 | } 71 | } 72 | 73 | impl WidgetList for Vec { 74 | fn get(&self) -> Vec<&dyn Widget> { 75 | self.iter().map(|e| e as &dyn Widget).collect() 76 | } 77 | } 78 | 79 | impl WidgetList for [W; N] { 80 | fn get(&self) -> Vec<&dyn Widget> { 81 | self.iter().map(|e| e as &dyn Widget).collect() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/boxes.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | use frui::prelude::*; 4 | 5 | mod misc; 6 | 7 | #[derive(ViewWidget)] 8 | struct App; 9 | 10 | impl ViewWidget for App { 11 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 12 | Stack::builder().children(( 13 | Align::builder() 14 | .alignment(AlignmentDirectional::TOP_START) 15 | .child(SizedBox::from_size( 16 | ColoredBox { 17 | color: Color::OLIVE, 18 | child: Text::new("TOP_START"), 19 | }, 20 | Size::new(100.0, 100.0), 21 | )), 22 | Align::builder() 23 | .alignment(AlignmentDirectional::TOP_CENTER) 24 | .child(SizedBox::from_size( 25 | ColoredBox { 26 | color: Color::RED, 27 | child: Text::new("TOP_CENTER"), 28 | }, 29 | Size::new(100.0, 100.0), 30 | )), 31 | Align::builder() 32 | .alignment(AlignmentDirectional::TOP_END) 33 | .child(SizedBox::from_size( 34 | ColoredBox { 35 | color: Color::PURPLE, 36 | child: Text::new("TOP_END"), 37 | }, 38 | Size::new(100.0, 100.0), 39 | )), 40 | Align::builder() 41 | .alignment(AlignmentDirectional::CENTER_START) 42 | .child(SizedBox::from_size( 43 | ColoredBox { 44 | color: Color::BLUE, 45 | child: Text::new("CENTER_START"), 46 | }, 47 | Size::new(100.0, 100.0), 48 | )), 49 | Align::builder() 50 | .alignment(AlignmentDirectional::CENTER) 51 | .child(LimitedBox { 52 | max_width: 100.0, 53 | max_height: 100.0, 54 | child: ColoredBox { 55 | color: Color::YELLOW, 56 | child: Padding::builder() 57 | .padding(EdgeInsets::all(20.0)) 58 | .child(Text::new("CENTER").color(Color::BLACK)), 59 | }, 60 | }), 61 | Align::builder() 62 | .alignment(AlignmentDirectional::CENTER_END) 63 | .child(SizedBox::from_size( 64 | ColoredBox { 65 | color: Color::GREEN, 66 | child: Text::new("CENTER_END"), 67 | }, 68 | Size::new(100.0, 100.0), 69 | )), 70 | Align::builder() 71 | .alignment(AlignmentDirectional::BOTTOM_START) 72 | .child(SizedBox::from_size( 73 | ColoredBox { 74 | color: Color::FUCHSIA, 75 | child: Text::new("BOTTOM_START"), 76 | }, 77 | Size::new(100.0, 100.0), 78 | )), 79 | Align::builder() 80 | .alignment(AlignmentDirectional::BOTTOM_CENTER) 81 | .child(SizedBox::from_size( 82 | ColoredBox { 83 | color: Color::TEAL, 84 | child: Text::new("BOTTOM_CENTER"), 85 | }, 86 | Size::new(100.0, 100.0), 87 | )), 88 | Align::builder() 89 | .alignment(AlignmentDirectional::BOTTOM_END) 90 | .child(SizedBox::from_size( 91 | ColoredBox { 92 | color: Color::AQUA, 93 | child: Text::new("BOTTOM_END"), 94 | }, 95 | Size::new(100.0, 100.0), 96 | )), 97 | )) 98 | } 99 | } 100 | 101 | fn main() { 102 | run_app(Directionality { 103 | direction: TextDirection::Rtl, 104 | child: App, 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /examples/column.rs: -------------------------------------------------------------------------------- 1 | //! This example shows usage of the [`Column`] widget and its different options. 2 | //! 3 | //! [`DebugContainer`] is used to visualize layout bounds of the [`Column`] 4 | //! widget. 5 | //! 6 | //! Feel free to modify each of the properties of the [`Column`] to see how it 7 | //! affects the way its children are laid out! 8 | 9 | #![feature(type_alias_impl_trait)] 10 | 11 | use frui::prelude::*; 12 | 13 | mod misc; 14 | use misc::flex_children as list; 15 | 16 | #[derive(ViewWidget)] 17 | struct App; 18 | 19 | impl ViewWidget for App { 20 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 21 | DebugContainer::new( 22 | Column::builder() 23 | .space_between(20.0) 24 | .text_direction(TextDirection::Ltr) 25 | .vertical_direction(VerticalDirection::Up) 26 | .main_axis_size(MainAxisSize::Min) 27 | .cross_axis_size(CrossAxisSize::Min) 28 | .main_axis_alignment(MainAxisAlignment::Center) 29 | .cross_axis_alignment(CrossAxisAlignment::Center) 30 | .children(list::flexible()), 31 | ) 32 | } 33 | } 34 | 35 | fn main() { 36 | run_app(App); 37 | } 38 | 39 | #[cfg(all(test, feature = "miri"))] 40 | mod test { 41 | use super::*; 42 | use frui::{ 43 | app::runner::miri::MiriRunner, 44 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 45 | }; 46 | 47 | #[test] 48 | pub fn run_example_under_miri() { 49 | let mut runner = MiriRunner::new(App); 50 | 51 | for _ in 0..4 { 52 | runner.key_down(KeyEvent::for_test( 53 | Modifiers::default(), 54 | Key::Character(" ".into()), 55 | )); 56 | runner.update(true); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/counter.rs: -------------------------------------------------------------------------------- 1 | //! This is an obligatory example of a counter app. 2 | 3 | #![allow(unused_attributes)] 4 | #![feature(type_alias_impl_trait)] 5 | 6 | use frui::prelude::*; 7 | 8 | #[path = "button.rs"] 9 | mod button; 10 | 11 | use button::Button; 12 | 13 | #[derive(ViewWidget)] 14 | pub struct Counter; 15 | 16 | impl WidgetState for Counter { 17 | type State = isize; 18 | 19 | fn create_state(&self) -> Self::State { 20 | 0 21 | } 22 | } 23 | 24 | impl ViewWidget for Counter { 25 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 26 | Column::builder() 27 | .space_between(60.0) 28 | .main_axis_size(MainAxisSize::Max) 29 | .cross_axis_size(CrossAxisSize::Max) 30 | .main_axis_alignment(MainAxisAlignment::Center) 31 | .cross_axis_alignment(CrossAxisAlignment::Center) 32 | .children(( 33 | Text::new(cx.state().to_string()) 34 | .size(150.0) 35 | .weight(FontWeight::BOLD), 36 | Row::builder() 37 | .space_between(10.0) // 38 | .children(( 39 | Button { 40 | label: Text::new("+").size(30.), 41 | on_click: || *cx.state_mut() += 1, 42 | }, 43 | Button { 44 | label: Text::new("-").size(30.), 45 | on_click: || *cx.state_mut() -= 1, 46 | }, 47 | )), 48 | )) 49 | } 50 | } 51 | 52 | #[allow(unused)] 53 | fn main() { 54 | run_app(Counter); 55 | } 56 | -------------------------------------------------------------------------------- /examples/crab_counter.rs: -------------------------------------------------------------------------------- 1 | //! Crab Counter is an app that allows you to keep track of the number of crabs 2 | //! you have! 🦀 🦀 🦀 3 | 4 | #![feature(type_alias_impl_trait)] 5 | 6 | use frui::prelude::*; 7 | 8 | mod button; 9 | 10 | use button::Button; 11 | 12 | #[derive(ViewWidget)] 13 | struct CrabCounter; 14 | 15 | impl WidgetState for CrabCounter { 16 | type State = isize; 17 | 18 | fn create_state(&self) -> Self::State { 19 | 0 20 | } 21 | } 22 | 23 | impl ViewWidget for CrabCounter { 24 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 25 | Column::builder() 26 | .space_between(60.0) 27 | .main_axis_size(MainAxisSize::Max) 28 | .cross_axis_size(CrossAxisSize::Max) 29 | .main_axis_alignment(MainAxisAlignment::Center) 30 | .cross_axis_alignment(CrossAxisAlignment::Center) 31 | .children(( 32 | Text::new(format!("{} 🦀", *cx.state())) 33 | .size(100.0) 34 | .weight(FontWeight::BOLD), 35 | Row::builder() 36 | .space_between(10.0) // 37 | .children(( 38 | Button { 39 | label: Text::new("+").size(30.), 40 | on_click: || *cx.state_mut() += 1, 41 | }, 42 | Button { 43 | label: Text::new("-").size(30.), 44 | on_click: || *cx.state_mut() -= 1, 45 | }, 46 | )), 47 | )) 48 | } 49 | } 50 | 51 | fn main() { 52 | run_app(CrabCounter); 53 | } 54 | 55 | #[cfg(all(test, feature = "miri"))] 56 | mod test { 57 | use super::*; 58 | 59 | use frui::{ 60 | app::runner::miri::MiriRunner, 61 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 62 | }; 63 | 64 | #[test] 65 | pub fn run_example_under_miri() { 66 | let mut runner = MiriRunner::new(CrabCounter); 67 | 68 | for _ in 0..4 { 69 | runner.key_down(KeyEvent::for_test( 70 | Modifiers::default(), 71 | Key::Character(" ".into()), 72 | )); 73 | runner.update(true); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/decoration.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | use frui::prelude::*; 4 | 5 | fn main() { 6 | run_app(ColoredBox { 7 | color: Color::WHITE, 8 | child: Row::builder() 9 | .main_axis_size(MainAxisSize::Max) 10 | .cross_axis_size(CrossAxisSize::Max) 11 | .main_axis_alignment(MainAxisAlignment::SpaceEvenly) 12 | .cross_axis_alignment(CrossAxisAlignment::Center) 13 | .children(( 14 | SizedBox::from_size( 15 | DecoratedBox::builder() 16 | .position(DecorationPosition::Background) 17 | .decoration( 18 | BoxDecoration::builder() 19 | .color(Color::Rgba32(0x28C6A8FF)) 20 | .border_radius(BorderRadius::circular(10.0)) 21 | .border(BoxBorder::all( 22 | Color::Rgba32(0x000000FF), 23 | 2.0, 24 | BorderStyle::Dash(vec![10.0, 5.0], 0.0), 25 | )) 26 | .box_shadow(vec![BoxShadow { 27 | color: Color::BLACK.with_alpha(0.3), 28 | offset: Offset::new(5.0, 2.0), 29 | blur_radius: 10.0, 30 | spread_radius: 0.0, 31 | blur_style: BlurStyle::Normal, 32 | }]), 33 | ) 34 | .child(Center::child(Text::new("Hello, world!"))), 35 | Size::new(100.0, 100.0), 36 | ), 37 | SizedBox::from_size( 38 | DecoratedBox::builder() 39 | .position(DecorationPosition::Background) 40 | .decoration( 41 | BoxDecoration::builder() 42 | .color(Color::Rgba32(0xFC6900FF)) 43 | .shape(BoxShape::Circle), 44 | ) 45 | .child(Center::child(Text::new("+").size(60.0))), 46 | Size::new(100.0, 100.0), 47 | ), 48 | )), 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /examples/flex.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | use frui::prelude::*; 4 | 5 | mod misc; 6 | 7 | #[derive(ViewWidget)] 8 | struct App; 9 | 10 | impl ViewWidget for App { 11 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 12 | DebugContainer::new( 13 | Flex::builder() 14 | .space_between(10.0) 15 | .direction(Axis::Horizontal) 16 | .vertical_direction(VerticalDirection::Down) 17 | .main_axis_size(MainAxisSize::Max) 18 | .cross_axis_size(CrossAxisSize::Min) 19 | .main_axis_alignment(MainAxisAlignment::SpaceBetween) 20 | .cross_axis_alignment(CrossAxisAlignment::Center) 21 | .children(( 22 | SizedBox::from_size( 23 | ColoredBox { 24 | child: Text::new("Hello world!"), 25 | color: Color::RED, 26 | }, 27 | Size::new(100.0, 100.0), 28 | ), 29 | Flexible { 30 | flex: 1, 31 | fit: FlexFit::Tight, 32 | child: SizedBox::from_size( 33 | ColoredBox { 34 | child: Text::new("Hello world!"), 35 | color: Color::RED, 36 | }, 37 | Size::new(100.0, 100.0), 38 | ), 39 | }, 40 | SizedBox::from_size( 41 | ColoredBox { 42 | child: Text::new("Hello world!"), 43 | color: Color::RED, 44 | }, 45 | Size::new(100.0, 100.0), 46 | ), 47 | Flexible { 48 | flex: 2, 49 | fit: FlexFit::Tight, 50 | child: ColoredBox { 51 | child: Text::new("Hello world!"), 52 | color: Color::FUCHSIA, 53 | }, 54 | }, 55 | Expanded::new(ColoredBox { 56 | child: Text::new("Hello world!"), 57 | color: Color::GREEN, 58 | }), 59 | SizedBox::from_size( 60 | ColoredBox { 61 | child: Text::new("Hello world!"), 62 | color: Color::BLUE, 63 | }, 64 | Size::new(100.0, 100.0), 65 | ), 66 | )), 67 | ) 68 | } 69 | } 70 | 71 | fn main() { 72 | run_app(Directionality { 73 | direction: TextDirection::Rtl, 74 | child: App, 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /examples/inherited_widget.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to construct an [`InheritedWidget`] and use it in 2 | //! your code. 3 | //! 4 | //! [`InheritedWidget`] allows you to access its state from anywhere in its 5 | //! subtree. Additionally, whenever its state is updated, it will mark every 6 | //! widget that is dependent on it for a rebuild. That way, only necessary 7 | //! widgets are going to be rebuilt instead of the whole subtree. 8 | //! 9 | //! You can observe the above behavior in this example. [`ChangesOnRebuild`] 10 | //! inside of the [`App`] widget remains the same even though 11 | //! [`InheritedSwitchConsumer`] reacts to [`InheritedSwitch`] state changes 12 | //! triggered by the [`InheritedSwitchDispatcher`] and rebuilds. 13 | 14 | #![feature(type_alias_impl_trait)] 15 | 16 | use frui::prelude::*; 17 | 18 | mod misc; 19 | use misc::{ChangesOnRebuild, SwitchGuard}; 20 | 21 | // (1) 22 | // 23 | // First we will define an InheritedWidget which will be inserted at some point 24 | // into the widget tree. State of that widget is going to be boolean. That 25 | // boolean will be then accessed later from widgets which want to depend on 26 | // that InheritedSwitch widget. 27 | 28 | #[derive(InheritedWidget)] 29 | struct InheritedSwitch { 30 | child: W, 31 | } 32 | 33 | impl WidgetState for InheritedSwitch { 34 | type State = bool; 35 | 36 | fn create_state<'a>(&'a self) -> Self::State { 37 | false 38 | } 39 | } 40 | 41 | impl InheritedWidget for InheritedSwitch { 42 | fn build<'w>(&'w self) -> Self::Widget<'w> { 43 | &self.child 44 | } 45 | } 46 | 47 | // (2) 48 | // 49 | // Now we will define the InheritedSwitch::of method, which will simplify 50 | // access to the state of the InheritedWidget. 51 | 52 | impl InheritedSwitch<()> { 53 | pub fn of<'a, T>(cx: BuildCx<'a, T>) -> SwitchGuard<'a> { 54 | let state = cx.depend_on_inherited_widget::().unwrap(); 55 | 56 | SwitchGuard::new(state) 57 | } 58 | } 59 | 60 | // (3) 61 | // 62 | // Here we will define a widget which will trigger change in InheritedSwitch 63 | // state. It will react to a keyboard event and will flip the boolean value 64 | // by using InheritedSwitch::of method we defined earlier. 65 | 66 | #[derive(ViewWidget)] 67 | struct InheritedSwitchDispatcher; 68 | 69 | impl ViewWidget for InheritedSwitchDispatcher { 70 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 71 | KeyboardEventDetector { 72 | on_event: |_| InheritedSwitch::of(cx).switch(), 73 | child: (), 74 | } 75 | } 76 | } 77 | 78 | // (4) 79 | // 80 | // Here we will define a widget that reacts to changes in InheritedSwitch state. 81 | // Again we access the state through InheritedSwitch::of and display it. 82 | 83 | #[derive(ViewWidget)] 84 | struct InheritedSwitchConsumer; 85 | 86 | impl ViewWidget for InheritedSwitchConsumer { 87 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 88 | Text::new(InheritedSwitch::of(cx).to_string()) 89 | } 90 | } 91 | 92 | // (5) 93 | // 94 | // We will now insert previously defined InheritedSwitchDispatcher and 95 | // InheritedSwitchConsumer to the App. 96 | // 97 | // Additionally, we will add ChangesOnRebuild widget to prove that nothing 98 | // except for the dependent widgets gets rebuilt. 99 | 100 | #[derive(ViewWidget)] 101 | struct App; 102 | 103 | impl ViewWidget for App { 104 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 105 | Center::child(Column::builder().children(( 106 | Text::new(concat!( 107 | "Following widget doesn't depend on inherited widget, ", 108 | "so it should not be rebuilt (number should stay the same).", 109 | )), 110 | ChangesOnRebuild, 111 | Text::new("\n"), 112 | // 113 | Text::new(concat!( 114 | "Following widget depends on inherited widget, ", 115 | "so its value should update when you press a key.", 116 | )), 117 | InheritedSwitchConsumer, 118 | InheritedSwitchDispatcher, 119 | ))) 120 | } 121 | } 122 | 123 | // (6) 124 | // 125 | // Finally, we wrap our App in InheritedSwitch widget so that 126 | // InheritedSwitchDispatcher and InheritedSwitchConsumer can access its state. 127 | 128 | fn main() { 129 | run_app(InheritedSwitch { child: App }); 130 | } 131 | 132 | #[cfg(all(test, feature = "miri"))] 133 | mod test { 134 | use super::*; 135 | use frui::{ 136 | app::runner::miri::MiriRunner, 137 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 138 | }; 139 | 140 | #[test] 141 | pub fn inherited_widget() { 142 | let mut runner = MiriRunner::new(InheritedSwitch { child: App }); 143 | 144 | for _ in 0..4 { 145 | runner.key_down(KeyEvent::for_test( 146 | Modifiers::default(), 147 | Key::Character(" ".into()), 148 | )); 149 | runner.update(true); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /examples/local_key.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to annotate children of a [`RenderWidget`] in a way 2 | //! that allows Frui to preserve the state of those widgets between rebuilds. 3 | //! 4 | //! [`RandomState`] is a widget that will generate and display a new number 5 | //! every time its state has been reset. If you run this example you will be 6 | //! able to notice that the state of [`RandomState`] widgets in each of the 7 | //! [`Column`]s doesn't reset after you switch views (by clicking any key). 8 | //! 9 | //! This is because each of those widgets is annotated with [`LocalKey`] which 10 | //! informs Frui that if it is possible it should try preserving the state of 11 | //! that widget between rebuilds. 12 | 13 | #![feature(type_alias_impl_trait)] 14 | 15 | use frui::prelude::*; 16 | 17 | mod misc; 18 | use misc::{RandomState, Switch}; 19 | 20 | #[derive(ViewWidget)] 21 | struct App; 22 | 23 | impl WidgetState for App { 24 | type State = Switch; 25 | 26 | fn create_state(&self) -> Self::State { 27 | Switch::default() 28 | } 29 | } 30 | 31 | impl ViewWidget for App { 32 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 33 | KeyboardEventDetector { 34 | on_event: |_| cx.state_mut().switch(), 35 | child: if cx.state().value() { 36 | Center::child(Column::builder().children(( 37 | LocalKey::new(1usize, RandomState), 38 | Text::new("First Widget 🦀"), 39 | LocalKey::new(2i32, RandomState), 40 | ))) 41 | .boxed() 42 | } else { 43 | Center::child(Column::builder().children(( 44 | Text::new("First Widget 🦀"), 45 | Text::new("Second Widget 🦀"), 46 | LocalKey::new(1usize, RandomState), 47 | LocalKey::new(2i32, RandomState), 48 | ))) 49 | .boxed() 50 | }, 51 | } 52 | } 53 | } 54 | 55 | fn main() { 56 | run_app(App); 57 | } 58 | 59 | #[cfg(all(test, feature = "miri"))] 60 | mod test { 61 | use super::*; 62 | use frui::{ 63 | app::runner::miri::MiriRunner, 64 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 65 | }; 66 | 67 | #[test] 68 | pub fn run_example_under_miri() { 69 | let mut runner = MiriRunner::new(App); 70 | 71 | for _ in 0..4 { 72 | runner.key_down(KeyEvent::for_test( 73 | Modifiers::default(), 74 | Key::Character(" ".into()), 75 | )); 76 | runner.update(true); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/misc/flex_children.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use frui::prelude::*; 4 | 5 | const RED: Color = Color::rgb8(255, 0, 110); 6 | const BLUE: Color = Color::rgb8(0, 186, 255); 7 | const GREEN: Color = Color::rgb8(13, 245, 152); 8 | 9 | const SQUARE_RED: Big = Big::new(RED); 10 | const SQUARE_BLUE: Big = Big(50., 50., BLUE); 11 | const SQUARE_GREEN: Big = Big::new(GREEN); 12 | 13 | pub fn inflexible() -> impl WidgetList { 14 | (SQUARE_RED, SQUARE_BLUE, SQUARE_GREEN) 15 | } 16 | 17 | pub fn flexible() -> impl WidgetList { 18 | ( 19 | Expanded::new( 20 | Container::builder() 21 | .width(100.) 22 | .height(100.) 23 | .color(RED) 24 | .child(Text::new("Tight,flex=1")), 25 | ), 26 | Flexible::new( 27 | Container::builder() 28 | .width(50.) 29 | .height(50.) 30 | .color(BLUE) 31 | .child(Text::new("Loose,flex=1")), 32 | ), 33 | Expanded::new( 34 | Container::builder() 35 | .width(100.) 36 | .height(100.) 37 | .color(GREEN) 38 | .child(Text::new("Tight,flex=2")), 39 | ) 40 | .flex(2), 41 | ) 42 | } 43 | 44 | pub fn flexible_inflexible() -> impl WidgetList { 45 | ( 46 | Expanded::new( 47 | Container::builder() 48 | .width(100.) 49 | .height(100.) 50 | .color(RED) 51 | .child(Text::new("Tight,flex=1")), 52 | ), 53 | SQUARE_RED, 54 | Flexible::new( 55 | Container::builder() 56 | .width(50.) 57 | .height(50.) 58 | .color(BLUE) 59 | .child(Text::new("Loose,flex=1")), 60 | ), 61 | SQUARE_BLUE, 62 | Expanded::new( 63 | Container::builder() 64 | .width(100.) 65 | .height(100.) 66 | .color(GREEN) 67 | .child(Text::new("Tight,flex=2")), 68 | ) 69 | .flex(2), 70 | SQUARE_GREEN, 71 | ) 72 | } 73 | 74 | #[derive(ViewWidget)] 75 | pub struct Big(pub f64, pub f64, pub Color); 76 | 77 | impl Big { 78 | pub(crate) const fn new(color: Color) -> Self { 79 | Self(100., 100., color) 80 | } 81 | } 82 | 83 | impl ViewWidget for Big { 84 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 85 | Container::builder() 86 | .color(self.2.clone()) 87 | .width(self.0) 88 | .height(self.1) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/misc/mod.rs: -------------------------------------------------------------------------------- 1 | mod random_state; 2 | mod switch; 3 | 4 | pub mod flex_children; 5 | 6 | pub use random_state::*; 7 | pub use switch::*; 8 | -------------------------------------------------------------------------------- /examples/misc/random_state.rs: -------------------------------------------------------------------------------- 1 | use frui::prelude::*; 2 | 3 | use rand::Rng; 4 | 5 | /// This widget will display a different number every time its state is lost. 6 | #[derive(ViewWidget)] 7 | pub struct RandomState; 8 | 9 | impl WidgetState for RandomState { 10 | type State = usize; 11 | 12 | fn create_state<'a>(&'a self) -> Self::State { 13 | rand::thread_rng().gen() 14 | } 15 | } 16 | 17 | impl ViewWidget for RandomState { 18 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 19 | Text::new(cx.state().to_string()) 20 | } 21 | } 22 | 23 | /// This widget will display a different number every time it gets rebuilt. 24 | #[derive(ViewWidget)] 25 | pub struct ChangesOnRebuild; 26 | 27 | impl ViewWidget for ChangesOnRebuild { 28 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 29 | Text::new(rand::thread_rng().gen::().to_string()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/misc/switch.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::ops::Deref; 4 | 5 | use frui::prelude::InheritedState; 6 | 7 | #[derive(Default)] 8 | pub struct Switch(bool); 9 | 10 | impl Switch { 11 | pub fn switch(&mut self) { 12 | self.0 = !self.0; 13 | } 14 | 15 | pub fn value(&self) -> bool { 16 | self.0 17 | } 18 | } 19 | 20 | pub struct SwitchGuard<'a> { 21 | pub state: InheritedState<'a, bool>, 22 | pub value: bool, 23 | } 24 | 25 | impl<'a> SwitchGuard<'a> { 26 | pub fn new(state: InheritedState<'a, bool>) -> Self { 27 | let value = state.as_ref().clone(); 28 | Self { state, value } 29 | } 30 | 31 | pub fn switch(&'a mut self) { 32 | let v = &mut *self.state.as_mut(); 33 | *v = !*v; 34 | self.value = !self.value; 35 | } 36 | } 37 | 38 | impl<'a> Deref for SwitchGuard<'a> { 39 | type Target = bool; 40 | 41 | fn deref(&self) -> &Self::Target { 42 | &self.value 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/pointer_events.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to use [`PointerListener`] and [`PointerRegion`]. 2 | 3 | #![feature(type_alias_impl_trait)] 4 | 5 | use frui::prelude::*; 6 | use frui::render::*; 7 | 8 | #[derive(ViewWidget)] 9 | struct App; 10 | 11 | impl WidgetState for App { 12 | type State = Stats; 13 | 14 | fn create_state(&self) -> Self::State { 15 | Stats::default() 16 | } 17 | } 18 | 19 | impl ViewWidget for App { 20 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 21 | Stack::builder().children(( 22 | // Stats: 23 | Positioned::builder() 24 | .left(30.) 25 | .top(30.) 26 | .child(cx.state().clone()), 27 | Center::child( 28 | PointerListener::builder() 29 | .on_pointer_down(|_| cx.state_mut().down_count += 1) 30 | .on_pointer_up(|_| cx.state_mut().up_count += 1) 31 | .on_pointer_scroll(|_| cx.state_mut().scroll_count += 1) 32 | .child( 33 | PointerRegion::builder() 34 | .on_enter(|_| cx.state_mut().enter_count += 1) 35 | .on_exit(|_| cx.state_mut().exit_count += 1) 36 | .on_move(|e| cx.state_mut().pointer_pos = e.0.pos) 37 | .child( 38 | Container::builder() 39 | .width(100.) 40 | .height(100.) 41 | .color(Color::SILVER) 42 | .child(()), 43 | ), 44 | ), 45 | ), 46 | )) 47 | } 48 | } 49 | 50 | #[derive(ViewWidget, Debug, Default, Clone)] 51 | struct Stats { 52 | pointer_pos: Point, 53 | up_count: usize, 54 | down_count: usize, 55 | scroll_count: usize, 56 | enter_count: usize, 57 | exit_count: usize, 58 | } 59 | 60 | impl ViewWidget for Stats { 61 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 62 | Text::new(format!("{:#?}", self)) 63 | } 64 | } 65 | 66 | fn main() { 67 | run_app(App); 68 | } 69 | -------------------------------------------------------------------------------- /examples/preserve_state.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how the order of children of a [`RenderWidget`] 2 | //! affects whether the state of those children widgets will be preserved. 3 | //! 4 | //! [`RandomState`] is a widget that will generate and display a new number 5 | //! every time its state has been reset. If you run this example you will be 6 | //! able to notice that the state of [`RandomState`] widgets in each of the 7 | //! [`Column`]s doesn't reset after you switch views (by clicking any key). 8 | //! 9 | //! This is because that [`RandomState`] in both of the views is a 3rd child. 10 | //! Changing the order of that widget between children will ultimately cause 11 | //! [`RandomState`] to lose its state. 12 | //! 13 | //! To avoid this, see `local_key` example which shows how you can annotate 14 | //! stateful widgets in a way that will preserve their state even if their 15 | //! order in children list changes. 16 | 17 | #![feature(type_alias_impl_trait)] 18 | 19 | use frui::prelude::*; 20 | 21 | mod misc; 22 | use misc::{RandomState, Switch}; 23 | 24 | #[derive(ViewWidget)] 25 | struct App; 26 | 27 | impl WidgetState for App { 28 | type State = Switch; 29 | 30 | fn create_state(&self) -> Self::State { 31 | Switch::default() 32 | } 33 | } 34 | 35 | impl ViewWidget for App { 36 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 37 | KeyboardEventDetector { 38 | on_event: |_| cx.state_mut().switch(), 39 | child: if cx.state().value() { 40 | Center::child(Column::builder().children(( 41 | Text::new("First child 🦀"), 42 | Text::new("Second child 🦀"), 43 | RandomState, // <-- 3rd child 44 | ))) 45 | .boxed() 46 | } else { 47 | Center::child(Column::builder().children(( 48 | Text::new("First child 🦀"), 49 | // Following widget makes `RandomState` a third child of the Column. 50 | // If you delete it, when you run this example you will be able to 51 | // see that its state is not preserved (and number changes). 52 | (), 53 | RandomState, // <-- 3rd child 54 | ))) 55 | .boxed() 56 | }, 57 | } 58 | } 59 | } 60 | 61 | fn main() { 62 | run_app(App); 63 | } 64 | 65 | #[cfg(all(test, feature = "miri"))] 66 | mod test { 67 | use super::*; 68 | use frui::{ 69 | app::runner::miri::MiriRunner, 70 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 71 | }; 72 | 73 | #[test] 74 | pub fn run_app_under_miri() { 75 | let mut runner = MiriRunner::new(App); 76 | 77 | for _ in 0..4 { 78 | runner.key_down(KeyEvent::for_test( 79 | Modifiers::default(), 80 | Key::Character(" ".into()), 81 | )); 82 | runner.update(true); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/row.rs: -------------------------------------------------------------------------------- 1 | //! This example shows usage of the [`Row`] widget and its different options. 2 | //! 3 | //! [`DebugContainer`] is used to visualize layout bounds of the [`Row`] widget. 4 | //! 5 | //! Feel free to modify each of the properties of the [`Row`] to see how it 6 | //! affects the way its children are laid out! 7 | 8 | #![feature(type_alias_impl_trait)] 9 | 10 | use frui::prelude::*; 11 | 12 | mod misc; 13 | use misc::flex_children as list; 14 | 15 | #[derive(ViewWidget)] 16 | struct App; 17 | 18 | impl ViewWidget for App { 19 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 20 | DebugContainer::new( 21 | Row::builder() 22 | .space_between(20.0) 23 | .text_direction(TextDirection::Ltr) 24 | .vertical_direction(VerticalDirection::Up) 25 | .main_axis_size(MainAxisSize::Min) 26 | .cross_axis_size(CrossAxisSize::Min) 27 | .main_axis_alignment(MainAxisAlignment::Center) 28 | .cross_axis_alignment(CrossAxisAlignment::Center) 29 | .children(list::flexible_inflexible()), 30 | ) 31 | } 32 | } 33 | 34 | fn main() { 35 | run_app(App); 36 | } 37 | 38 | #[cfg(all(test, feature = "miri"))] 39 | mod test { 40 | use super::*; 41 | use frui::{ 42 | app::runner::miri::MiriRunner, 43 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 44 | }; 45 | 46 | #[test] 47 | pub fn run_example_under_miri() { 48 | let mut runner = MiriRunner::new(App); 49 | 50 | for _ in 0..4 { 51 | runner.key_down(KeyEvent::for_test( 52 | Modifiers::default(), 53 | Key::Character(" ".into()), 54 | )); 55 | runner.update(true); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/stack.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | use frui::prelude::*; 4 | 5 | mod misc; 6 | 7 | #[derive(ViewWidget)] 8 | struct App; 9 | 10 | impl ViewWidget for App { 11 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 12 | Stack::builder() 13 | .alignment(AlignmentDirectional::CENTER_END) 14 | .children(( 15 | Positioned::builder() 16 | .top(100.0) 17 | .left(100.0) 18 | .bottom(100.0) 19 | .right(100.0) 20 | .child(Container::builder().color(Color::AQUA)), 21 | Text::new("🦀") // 22 | .size(100.0) 23 | .weight(FontWeight::BOLD), 24 | Positioned::builder() // 25 | .right(10.0) 26 | .bottom(10.0) 27 | .child( 28 | Container::builder() 29 | .color(Color::GREEN) 30 | .width(50.0) 31 | .height(50.0), 32 | ), 33 | Center::child( 34 | Text::new("Centered") // 35 | .size(50.0) 36 | .weight(FontWeight::BOLD), 37 | ), 38 | )) 39 | } 40 | } 41 | 42 | fn main() { 43 | run_app(App); 44 | } 45 | -------------------------------------------------------------------------------- /examples/switch_widgets.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to construct a simple widget which will switch 2 | //! between two views. 3 | //! 4 | //! [`Switch`] is a simple structure holding [`bool`] which provides 5 | //! convenience method `switch()` which toggles between two states of a 6 | //! [`bool`]. 7 | //! 8 | //! Each of the views needs to be boxed, because of the way Rust treats return 9 | //! types. To achieve this `boxed()` helper method is used which wraps given 10 | //! widget with a Box and type erases it. 11 | //! 12 | //! [`KeyboardEventDetector`] is a widget which allows its consumers to react 13 | //! to keyboard events through a callback provided in `on_event`. 14 | 15 | #![feature(type_alias_impl_trait)] 16 | 17 | use frui::prelude::*; 18 | 19 | mod misc; 20 | use misc::Switch; 21 | 22 | #[derive(ViewWidget)] 23 | struct App; 24 | 25 | impl WidgetState for App { 26 | type State = Switch; 27 | 28 | fn create_state(&self) -> Self::State { 29 | Switch::default() 30 | } 31 | } 32 | 33 | impl ViewWidget for App { 34 | fn build<'w>(&'w self, cx: BuildCx<'w, Self>) -> Self::Widget<'w> { 35 | KeyboardEventDetector { 36 | on_event: |_| cx.state_mut().switch(), 37 | child: match cx.state().value() { 38 | true => Text::new("Top Left").boxed(), 39 | false => Center::child(Text::new("Centered")).boxed(), 40 | }, 41 | } 42 | } 43 | } 44 | 45 | fn main() { 46 | run_app(App); 47 | } 48 | 49 | #[cfg(all(test, feature = "miri"))] 50 | mod test { 51 | use super::*; 52 | use frui::{ 53 | app::runner::miri::MiriRunner, 54 | druid_shell::{keyboard_types::Key, KeyEvent, Modifiers}, 55 | }; 56 | 57 | #[test] 58 | pub fn run_example_under_miri() { 59 | let mut runner = MiriRunner::new(App); 60 | 61 | for _ in 0..4 { 62 | runner.key_down(KeyEvent::for_test( 63 | Modifiers::default(), 64 | Key::Character(" ".into()), 65 | )); 66 | runner.update(true); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/transform.rs: -------------------------------------------------------------------------------- 1 | //! This example shows how to use [`Transform`] widget. 2 | //! 3 | //! Do note that this widget is not yet implemented properly and for now it 4 | //! exists just to showcase that widgets are being hit tested correctly. 5 | 6 | #![feature(type_alias_impl_trait)] 7 | 8 | use frui::prelude::*; 9 | use frui::render::*; 10 | 11 | mod counter; 12 | 13 | use counter::Counter; 14 | 15 | #[derive(ViewWidget)] 16 | struct CounterRotated; 17 | 18 | impl ViewWidget for CounterRotated { 19 | fn build<'w>(&'w self, _: BuildCx<'w, Self>) -> Self::Widget<'w> { 20 | let screen_width = 500.; 21 | let screen_height = 400.; 22 | 23 | // Transform widget is not yet fully implemented, but I decided to 24 | // inlcude it to showcase that widget hit testing is correctly 25 | // implemented! 26 | Transform( 27 | Affine::translate((screen_width / 2., screen_height / 2.)) 28 | * Affine::rotate(std::f64::consts::FRAC_PI_8) 29 | * Affine::translate((-screen_width / 2., -screen_height / 2.)), 30 | Counter, 31 | ) 32 | } 33 | } 34 | 35 | fn main() { 36 | run_app(CounterRotated) 37 | } 38 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use frui_core::*; 2 | 3 | pub mod prelude { 4 | pub use frui_core::prelude::*; 5 | pub use frui_widgets::*; 6 | } 7 | --------------------------------------------------------------------------------