├── .gitignore ├── .media └── showcase.png ├── Cargo.toml ├── LICENSE ├── README.md ├── Widgets.md ├── rustfmt.toml ├── thunderclap-macros ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── rooftop.rs │ └── widget.rs └── thunderclap ├── Cargo.toml ├── examples └── counter │ └── main.rs └── src ├── app.rs ├── base.rs ├── draw ├── mod.rs └── state.rs ├── error.rs ├── geom.rs ├── lib.rs ├── themes ├── assets │ ├── Inter-Italic.ttf │ ├── Inter-Regular.ttf │ ├── Inter-SemiBold.ttf │ └── Inter-SemiBoldItalic.ttf ├── dynamic.rs ├── mod.rs └── primer.rs └── ui ├── button.rs ├── checkbox.rs ├── container.rs ├── core.rs ├── hstack.rs ├── label.rs ├── margins.rs ├── max_fill.rs ├── mod.rs ├── scroll_bar.rs ├── text_area.rs └── vstack.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | **/flamegraph.svg 5 | **/perf.data 6 | -------------------------------------------------------------------------------- /.media/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzfool/thunderclap/7e11b25db19b58907e82223dd76b47b3ffa7f694/.media/showcase.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "thunderclap-macros", 4 | "thunderclap", 5 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under either "Apache 2.0" (http://www.apache.org/licenses/LICENSE-2.0) 2 | or "MIT" (http://opensource.org/licenses/MIT), at your option. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thunderclap 2 | 3 | A Rust toolkit to write decomposable and fast user interfaces. It is: 4 | 5 | - **Event-driven:** Thunderclap builds efficient abstractions over the Reclutch event system to avoid unnecessary computations. 6 | - **Simple:** Thunderclap provides a suite of widgets alongside various infrastructures to simplify writing your own widgets. 7 | - **Customizable:** There isn't a single line of hard-coded widget rendering; trivial widgets are fully parameterized and non-trivial widgets delegate to a provided theme. 8 | 9 | 10 | 11 | ## Overview 12 | 13 | Thunderclap aims to take the traditional widget hierarchy model from bulletproof libraries (e.g. Qt) and combine it with the cutting-edge simplicity of modern toolkits (e.g. Flutter). 14 | To accomplish this it provides three primary high-level components: 15 | 16 | - A widget library that fills the need for boilerplate UI components. 17 | - A theme API with a verbose typography and color scheme protocol. 18 | - A macro to emulate a declarative UI syntax for widget creation. 19 | 20 | The high-level UI code semantics are identical to that of XAML's. 21 | This implies several features; 22 | 23 | - Widgets that are explicitly given a name map to fields of the widget type (`x:Name="name"` becomes `as name`, then `self.name`). 24 | - Widget properties can be optionally set, falling back to a default value (`property="value"` becomes `property=value`). 25 | - Binding to events is done directly in the high-level syntax (`Event="handler"`becomes `@event { handler }`). 26 | 27 | The biggest difference is that XAML is stored in an external file, however Thunderclap parses input from a macro directly in code. 28 | 29 | ## Example 30 | 31 | There's also [an in-depth overview of the code below](https://github.com/jazzfool/thunderclap/wiki/Making-a-counter). 32 | 33 | ```rust 34 | use thunderclap::{ 35 | app, base, 36 | themes::Primer, 37 | ui::{Button, Label, VStack}, 38 | }; 39 | 40 | rooftop! { 41 | struct Counter: () { 42 | fn build( 43 | count: i32 = 0, 44 | ) { 45 | VStack() { 46 | Label( 47 | text=bind(format!("Count: {}", bind.count).into()), 48 | wrap=false, 49 | ), 50 | Button(text="Count Up") 51 | @press { 52 | widget.data.count += 1; 53 | }, 54 | Button(text="Count Down") 55 | @press { 56 | widget.data.count -= 1; 57 | }, 58 | } 59 | } 60 | } 61 | } 62 | 63 | fn main() { 64 | let app = app::create( 65 | |_, display| Primer::new(display).unwrap(), // theme 66 | |u_aux, g_aux, theme| { 67 | Counter { 68 | // Perhaps we want to start counting from 5 instead of 0 69 | count: 5, 70 | ..Counter::from_theme(theme) 71 | }.construct(theme, u_aux, g_aux) 72 | }, 73 | app::AppOptions { 74 | name: "Counter App".into(), 75 | ..Default::default() 76 | }, 77 | ).unwrap(); 78 | app.start(|_| None); 79 | } 80 | ``` 81 | 82 | --- 83 | 84 | ### You can see a rundown of all the widgets [here](Widgets.md). 85 | 86 | ## Theme List (so far) 87 | 88 | - GitHub Primer 89 | 90 | ## Widget List (so far) 91 | 92 | - Button 93 | - Vertical Stack 94 | - Container 95 | - Label 96 | - Checkbox 97 | - Horizontal Stack 98 | - Text area 99 | - Margins 100 | - Max Fill 101 | 102 | ## Project State 103 | 104 | Still very early in development, however the core code is now at a usable point where you can branch off and develop your own widgets. Provided widget suite is in active development. 105 | 106 | ## License 107 | 108 | Thunderclap is licensed under either 109 | 110 | - [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) 111 | - [MIT](http://opensource.org/licenses/MIT) 112 | 113 | at your choosing. 114 | -------------------------------------------------------------------------------- /Widgets.md: -------------------------------------------------------------------------------- 1 | # Widget Rundown 2 | 3 | This is a rundown of all the widgets, giving a brief overview of each widget. 4 | 5 | **Note:** This is *not* an alternative to the API documentation. 6 | 7 | ## Component Widgets 8 | 9 | ### Button - `thunderclap::ui::Button` 10 | 11 | *A button which can be pressed and focused by the user. Suitable for simple user actions.* 12 | 13 | - **`Themed.....`** ✔️ 14 | - **`Focusable..`** ✔️ 15 | - **`Layable....`** ✔️ 16 | - **Properties:** 17 | - `text`: Text shown in the button. 18 | - `typeface`: Typeface used in for the text. 19 | - `color`: Color of the text. 20 | - `background`: Background color of the text. 21 | - `focus`: Color used to indicate focus (usually in the form of a border). 22 | - `contrast`: Contrast mode of `background` and `color`. 23 | - `disabled`: Whether the button can be interacted with. 24 | - **Outgoing Event Queues:** 25 | - `event_queue`: `ButtonEvent` 26 | - `press`: The button has been pressed. 27 | - `release`: The button has been released. 28 | - `begin_hover`: The cursor has entered the button boundaries. 29 | - `end_hover`: The cursor has left the button boundaries. 30 | - `focus`: The button has gained focus. 31 | - `blur`: The button has lost focus. 32 | 33 | ### Label - `thunderclap::ui::Label` 34 | 35 | *Aligned text wrapped in a rectangle.* 36 | 37 | - **`Themed.....`** ❌ 38 | - **`Focusable..`** ❌ 39 | - **`Layable....`** ✔️ 40 | - **Properties:** 41 | - `text`: Text shown by the label. 42 | - `typeface`: Typeface of the text. 43 | - `color`: Color of the text. 44 | - `align`: Horizontal alignment of the text. 45 | - `wrap`: Whether text should be wrapped to fit in the rectangle. 46 | - **Outgoing Event Queues:** 47 | - *None* 48 | 49 | ### Checkbox - `thunderclap::ui::Checkbox` 50 | 51 | *Toggled checkbox. Suitable for boolean inputs.* 52 | 53 | - **`Themed.....`** ✔️ 54 | - **`Focusable..`** ✔️ 55 | - **`Layable....`** ✔️ 56 | - **Properties:** 57 | - `foreground`: Color of the check mark. 58 | - `background`: Color of the checkbox. 59 | - `focus`: Color used to indicate focus (usually in the form of a border). 60 | - `contrast`: Contrast mode of `background` and `foreground`. 61 | - `checked`: Whether the checkbox is checked. 62 | - `disabled`: Whether the checkbox can be interacted with. 63 | - **Outgoing Event Queues:** 64 | - `event_queue`: `CheckboxEvent` 65 | - `press`: The checkbox has been pressed. 66 | - `release`: The checkbox has been released. 67 | - `check`: The checkbox has been checked. 68 | - `uncheck`: The checkbox has been unchecked. 69 | - `begin_hover`: The cursor has entered the checkbox boundaries. 70 | - `end_hover`: The cursor has left the checkbox boundaries. 71 | - `focus`: The checkbox has gained focus. 72 | - `blur`: The checkbox has lost focus. 73 | 74 | ### Text Area - `thunderclap::ui::TextArea` 75 | 76 | *Accepts single line text input. Deliberately a visually bare-bones widget so that text input can be placed outside a textbox context. Suitable for string input.* 77 | 78 | - **`Themed.....`** ✔️ 79 | - **`Focusable..`** ✔️ 80 | - **`Layable....`** ✔️ 81 | - **Properties:** 82 | - `text`: Text within the text area. 83 | - `placeholder`: Placeholder text to appear when text is empty. 84 | - `typeface`: Typeface used for text. 85 | - `color`: Color of the text. 86 | - `placeholder_color`: Color of the placeholder text. 87 | - `cursor_color`: Color of text cursor/caret. 88 | - `disabled`: Whether the text area can be interacted with. 89 | - `cursor`: Text cursor/caret position. 90 | - **Outgoing Event Queues:** 91 | - `event_queue`: `TextAreaEvent` 92 | - `focus`: The text area has gained focus. 93 | - `blur`: The text area has lost focus. 94 | - `user_modify`: The text area has been modified by the user. 95 | 96 | ## Abstract Widgets 97 | 98 | ### Vertical Stack - `thunderclap::ui::VStack` 99 | 100 | *Layout widget which arranges widgets vertically.* 101 | 102 | - **`Themed.....`** ❌ 103 | - **`Focusable..`** ❌ 104 | - **`Layable....`** ✔️ 105 | - **Outgoing Event Queues:** 106 | - *None* 107 | 108 | ### Horizontal Stack - `thunderclap::ui::HStack` 109 | 110 | *Layout widget which arranges widget horizontally.* 111 | 112 | - **`Themed.....`** ❌ 113 | - **`Focusable..`** ❌ 114 | - **`Layable....`** ✔️ 115 | - **Outgoing Event Queues:** 116 | - *None* 117 | 118 | ### Container - `thunderclap::ui::Container` 119 | 120 | *Dynamically stores a list of widgets. This is useful if you don't need to access a child past initialization-time; essentially grouping it into a single child to minimize unused fields.* 121 | *The children will still be rendered and receive updates.* 122 | 123 | - **`Themed.....`** ❌ 124 | - **`Focusable..`** ❌ 125 | - **`Layable....`** ❌ 126 | - **Outgoing Event Queues:** 127 | - *None* 128 | 129 | ### Margins - `thunderclap::ui::Margins` 130 | 131 | *Adds margins around the boundaries of it's children as a whole.* 132 | 133 | - **`Themed.....`** ❌ 134 | - **`Focusable..`** ❌ 135 | - **`Layable....`** ✔️ 136 | - **Outgoing Event Queues:** 137 | - *None* 138 | 139 | ### Max Fill - `thunderclap::ui::MaxFill` 140 | 141 | *Computes the rectangle fitting all it's children, then resizes all it's children to said rectangle.* 142 | 143 | - **`Themed.....`** ❌ 144 | - **`Focusable..`** ❌ 145 | - **`Layable....`** ✔️ 146 | - **Outgoing Event Queues:** 147 | - *None* 148 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" -------------------------------------------------------------------------------- /thunderclap-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thunderclap-macros" 3 | version = "0.0.0" 4 | authors = ["jazzfool "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | description = "Proceudral macros for Thunderclap" 8 | homepage = "https://github.com/jazzfool/thunderclap/tree/master/thunderclap-macros" 9 | repository = "https://github.com/jazzfool/thunderclap" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | syn = { version = "1.0", features = ["full"] } 16 | quote = "1.0" 17 | proc-macro2 = "1.0" 18 | -------------------------------------------------------------------------------- /thunderclap-macros/README.md: -------------------------------------------------------------------------------- 1 | # `derive` 2 | 3 | All of these derives accept a `thunderclap_crate` attribute to specify the name of the Thunderclap crate; 4 | 5 | ```rust 6 | use thunderclap as alternative_thunderclap; 7 | 8 | #[derive(SomeThunderclapDerive)] 9 | #[thunderclap_crate(alternative_thunderclap)] // <-- 10 | struct Foo // ... 11 | ``` 12 | 13 | Realistically, there's no need to use this. This mainly used within Thunderclap so that internal types can `derive` where the only handle to the crate root is `crate::`. 14 | 15 | ## `PipelineEvent` 16 | 17 | ```rust 18 | #[derive(PipelineEvent, Clone, Copy, PartialEq)] 19 | enum MyEvent { 20 | #[event_key(stop)] 21 | Stop, 22 | #[event_key(play)] 23 | Play(f32), 24 | #[event_key(rewind)] 25 | Rewind { 26 | seconds: u32, 27 | play: bool, 28 | }, 29 | } 30 | ``` 31 | 32 | Which resolves down to: 33 | ```rust 34 | impl thunderclap::pipe::Event for MyEvent { 35 | fn get_key(&self) -> &'static str { 36 | match self { 37 | MyEvent::Stop => "stop", 38 | MyEvent::Play(..) => "play", 39 | MyEvent::Rewind{..} => "rewind", 40 | } 41 | } 42 | } 43 | 44 | impl MyEvent { // These are automatically called by `pipeline!` to "cast" the event. 45 | pub fn unwrap_as_stop(self) -> Option<()> { 46 | if let MyEvent::Stop = self { Some(()) } else { None } 47 | } 48 | 49 | pub fn unwrap_as_play(self) -> Option<(f32)> { 50 | if let MyEvent::Play(x0) = self { Some(x0) } else { None } 51 | } 52 | 53 | pub fn unwrap_as_rewind(self) -> Option<(u32, bool)> { 54 | if let MyEvent::Rewind{seconds, play} = self { Some((seconds, play)) } else { None } 55 | } 56 | } 57 | ``` 58 | 59 | ## `LayableWidget` 60 | 61 | ```rust 62 | #[derive(LayableWidget)] 63 | struct MyWidget { 64 | #[widget_layout] 65 | layout: WidgetLayoutEvents, 66 | } 67 | ``` 68 | 69 | Expands to... 70 | 71 | ```rust 72 | impl thunderclap::base::LayableWidget for MyWidget { 73 | #[inline] 74 | fn listen_to_layout(&mut self, layout: impl Into>) { 75 | self.layout.update(layout); 76 | } 77 | 78 | #[inline] 79 | fn layout_id(&self) -> Option { 80 | self.layout.id() 81 | } 82 | } 83 | ``` 84 | 85 | ## `DropNotifier` 86 | 87 | ```rust 88 | #[derive(DropNotifier)] 89 | struct MyWidget { 90 | #[widget_drop_event] 91 | drop_event: RcEventQueue, 92 | } 93 | ``` 94 | 95 | Expands to... 96 | 97 | ```rust 98 | impl thunderclap::base::DropNotifier for MyWidget { 99 | #[inline(always)] 100 | fn drop_event(&self) -> &thunderclap::reclutch::event::RcEventQueue { 101 | &self.drop_event 102 | } 103 | } 104 | ``` 105 | 106 | Note that you'll still have to appropriately implement `Drop` to emit into `drop_event`; 107 | 108 | ```rust 109 | // Manually implemented 110 | impl Drop for MyWidget { 111 | fn drop(&mut self) { 112 | self.drop_event.emit_owned(DropEvent); 113 | } 114 | } 115 | ``` 116 | 117 | ## `HasVisibility` 118 | 119 | ```rust 120 | #[derive(HasVisibility)] 121 | struct MyWidget { 122 | #[widget_visibility] 123 | visibility: Visibility, 124 | } 125 | ``` 126 | 127 | Expands to... 128 | 129 | ```rust 130 | impl thunderclap::base::HasVisibility { 131 | #[inline] 132 | fn set_visibility(&mut self, visibility: thunderclap::base::Visibility) { 133 | self.visibility = visibility; 134 | } 135 | 136 | #[inline] 137 | fn visibility(&self) -> thunderclap::base::Visibility { 138 | self.visibility 139 | } 140 | } 141 | ``` 142 | 143 | TL;DR: setter and getter. 144 | 145 | ## `Repaintable` 146 | 147 | ```rust 148 | #[derive(Repaintable)] 149 | struct MyWidget { 150 | #[repaint_target] 151 | a: CommandGroup, 152 | 153 | #[repaint_target] 154 | b: CommandGroup, 155 | 156 | #[widget_child] 157 | #[repaint_target] 158 | c: AnotherWidget, // <-- assuming this has a method called `repaint`. 159 | } 160 | ``` 161 | 162 | Expands to... 163 | 164 | ```rust 165 | impl thunderclap::base::Repaintable for MyWidget { 166 | #[inline] 167 | fn repaint(&mut self) { 168 | self.a.repaint(); 169 | self.b.repaint(); 170 | self.c.repaint(); 171 | 172 | for child in thunderclap::base::WidgetChildren::children_mut(self) { 173 | child.repaint(); 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | ## `Movable` and `Resizable` 180 | 181 | Both these derives accept an attribute `widget_transform_callback`. 182 | 183 | In the case of deriving both `Movable` and `Resizable`, note that "overlapping" derive attributes are valid, so in many scenarios you can write the attribute once for it to be applied to both derives. 184 | 185 | Assume `` means "interchangeable", since these two derives are almost identical. 186 | 187 | ```rust 188 | #[derive()] 189 | #[widget_transform_callback(on_transform)] 190 | struct MyWidget { 191 | #[widget_rect] 192 | rect: RelativeRect 193 | // -- OR -- 194 | #[widget_] 195 | x: , 196 | } 197 | ``` 198 | 199 | Expands to... 200 | 201 | ```rust 202 | impl thunderclap::base:: for MyWidget { 203 | fn set_(&mut self, : thunderclap::reclutch::display::) { 204 | self.rect. = ; 205 | // -- OR -- 206 | self.x = ; 207 | 208 | thunderclap::base::Repaintable::repaint(self); 209 | self.on_transform(); 210 | } 211 | 212 | #[inline] 213 | fn (&self) -> thunderclap::reclutch::display:: { 214 | self.rect. 215 | // -- OR -- 216 | self.x 217 | } 218 | } 219 | ``` 220 | 221 | Here the `// -- OR --` denotes that the derive can operate on either a point/size field or a rectangle field. 222 | -------------------------------------------------------------------------------- /thunderclap-macros/src/rooftop.rs: -------------------------------------------------------------------------------- 1 | use {proc_macro::TokenStream, quote::quote}; 2 | 3 | #[derive(Debug)] 4 | struct DataField { 5 | name: syn::Ident, 6 | default: syn::Expr, 7 | field_type: syn::Type, 8 | } 9 | 10 | impl syn::parse::Parse for DataField { 11 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 12 | let name = input.parse::()?; 13 | input.parse::()?; 14 | let field_type = input.parse::()?; 15 | input.parse::()?; 16 | let default = input.parse::()?; 17 | 18 | Ok(DataField { name, default, field_type }) 19 | } 20 | } 21 | 22 | #[derive(Debug)] 23 | struct DataFieldList { 24 | list: Vec, 25 | } 26 | 27 | impl syn::parse::Parse for DataFieldList { 28 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 29 | if input.is_empty() { 30 | Ok(DataFieldList { list: Vec::new() }) 31 | } else { 32 | let fields: syn::punctuated::Punctuated<_, syn::Token![,]> = 33 | input.parse_terminated(DataField::parse)?; 34 | let data_fields = fields.into_iter().collect(); 35 | Ok(DataFieldList { list: data_fields }) 36 | } 37 | } 38 | } 39 | 40 | enum FunctionBody<'a> { 41 | View(syn::parse::ParseBuffer<'a>), 42 | Other(syn::Block), 43 | } 44 | 45 | fn parse_function( 46 | input: syn::parse::ParseStream, 47 | ) -> syn::Result<(syn::Ident, Option, FunctionBody, bool)> { 48 | input.parse::()?; 49 | 50 | let fn_name = input.parse::()?; 51 | let parameters; 52 | syn::parenthesized!(parameters in input); 53 | 54 | let dfl = 55 | if parameters.is_empty() { None } else { parameters.parse::()?.into() }; 56 | 57 | let fn_body = if fn_name == "build" { 58 | let body; 59 | syn::braced!(body in input); 60 | FunctionBody::View(body) 61 | } else { 62 | let body = input.parse::()?; 63 | FunctionBody::Other(body) 64 | }; 65 | 66 | let next_fn = input.peek(syn::Token![fn]); 67 | 68 | Ok((fn_name, dfl, fn_body, next_fn)) 69 | } 70 | 71 | #[derive(Debug, Clone)] 72 | struct WidgetNode { 73 | type_name: syn::Ident, 74 | var_name: syn::Ident, 75 | data_assignments: Vec, 76 | children: Vec, 77 | } 78 | 79 | impl WidgetNode { 80 | fn compile_layout(&self) -> proc_macro2::TokenStream { 81 | let name = &self.var_name; 82 | if self.children.is_empty() { 83 | quote! { 84 | &mut #name 85 | } 86 | } else { 87 | let children: Vec<_> = self 88 | .children 89 | .iter() 90 | .map(|child| { 91 | let layout = child.compile_layout(); 92 | quote! { 93 | None => #layout, 94 | } 95 | }) 96 | .collect(); 97 | quote! { 98 | define_layout! { 99 | for #name => { 100 | #(#children)* 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | fn parse_view( 109 | input: syn::parse::ParseStream, 110 | bindings: &mut Vec, 111 | terminals: &mut Vec, 112 | count: &mut u64, 113 | ) -> syn::Result<(WidgetNode, bool)> { 114 | let type_name = input.parse::()?; 115 | let assignments; 116 | syn::parenthesized!(assignments in input); 117 | let data_assignments: syn::punctuated::Punctuated<_, syn::Token![,]> = 118 | assignments.parse_terminated(DataAssignment::parse)?; 119 | let mut data_assignments: Vec<_> = data_assignments.into_iter().collect(); 120 | let var_name = if input.parse::().is_ok() { 121 | input.parse::()? 122 | } else { 123 | *count += 1; 124 | quote::format_ident!("unnamed_widget_{}", count) 125 | }; 126 | 127 | for assignment in &data_assignments { 128 | if assignment.binding { 129 | let value = assignment.value.clone(); 130 | let var = assignment.var.clone(); 131 | bindings.push(quote! { 132 | { 133 | widget.#var_name.default_data().#var = #value; 134 | } 135 | }); 136 | } 137 | } 138 | 139 | data_assignments.retain(|assignment| !assignment.binding); 140 | 141 | let mut parse_terminals = true; 142 | let mut events = Vec::new(); 143 | while parse_terminals { 144 | if input.parse::().is_ok() { 145 | let event_name = input.parse::()?; 146 | let handler_body = input.parse::()?; 147 | events.push(quote! { 148 | #event_name => { 149 | { #handler_body } 150 | } 151 | }); 152 | } else { 153 | parse_terminals = false; 154 | } 155 | } 156 | 157 | if !events.is_empty() { 158 | terminals.push({ 159 | quote! { 160 | std::stringify!(#var_name) => event in #var_name.default_event_queue() => { 161 | #(#events)* 162 | } 163 | } 164 | }); 165 | } 166 | 167 | let mut children = Vec::new(); 168 | if input.peek(syn::token::Brace) { 169 | let children_parse; 170 | syn::braced!(children_parse in input); 171 | 172 | let mut parse_child = true; 173 | while parse_child { 174 | if children_parse.is_empty() { 175 | parse_child = false; 176 | } else { 177 | let (node, found_comma) = parse_view(&children_parse, bindings, terminals, count)?; 178 | children.push(node); 179 | parse_child = found_comma; 180 | } 181 | } 182 | } 183 | 184 | let found_comma = input.parse::().is_ok(); 185 | 186 | Ok((WidgetNode { type_name, var_name, data_assignments, children }, found_comma)) 187 | } 188 | 189 | fn flatten_widget_node_tree(root: &WidgetNode, output: &mut Vec) { 190 | output.push(root.clone()); 191 | for child in &root.children { 192 | flatten_widget_node_tree(child, output); 193 | } 194 | } 195 | 196 | #[derive(Debug, Clone)] 197 | struct DataAssignment { 198 | var: syn::Ident, 199 | value: syn::Expr, 200 | binding: bool, 201 | } 202 | 203 | mod bind_syntax { 204 | syn::custom_keyword!(bind); 205 | } 206 | 207 | impl DataAssignment { 208 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 209 | let var = input.parse::()?; 210 | input.parse::()?; 211 | let binding = input.peek(bind_syntax::bind); 212 | let value = if binding { 213 | input.parse::()?; 214 | let value; 215 | syn::parenthesized!(value in input); 216 | value.parse::()? 217 | } else { 218 | input.parse::()? 219 | }; 220 | Ok(DataAssignment { var, value, binding }) 221 | } 222 | } 223 | 224 | #[derive(Debug)] 225 | pub(crate) struct RooftopData { 226 | struct_name: syn::Ident, 227 | output_event: syn::Type, 228 | data_fields: DataFieldList, 229 | widget_tree_root: WidgetNode, 230 | bindings: Vec, 231 | terminals: Vec, 232 | functions: Vec<(syn::Ident, syn::Block)>, 233 | vis: Option, 234 | } 235 | 236 | impl syn::parse::Parse for RooftopData { 237 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 238 | let vis = input.parse::().ok(); 239 | 240 | input.parse::()?; 241 | let struct_name = input.parse()?; 242 | input.parse::()?; 243 | let output_event = input.parse()?; 244 | let struct_content; 245 | syn::braced!(struct_content in input); 246 | 247 | let mut parse_fn = struct_content.peek(syn::Token![fn]); 248 | let mut view_body = None; 249 | let mut data_fields = None; 250 | let mut other_functions = Vec::new(); 251 | while parse_fn { 252 | let (fn_name, param_fields, body, next_fn) = parse_function(&struct_content)?; 253 | parse_fn = next_fn; 254 | 255 | match body { 256 | FunctionBody::View(body) => { 257 | view_body = Some(body); 258 | data_fields = param_fields.unwrap().into(); 259 | } 260 | FunctionBody::Other(body) => { 261 | other_functions.push((fn_name, body)); 262 | } 263 | } 264 | } 265 | 266 | let view_body = view_body.expect("no build() pseudo-function found"); 267 | 268 | let mut bindings = Vec::new(); 269 | let mut terminals = Vec::new(); 270 | let mut count = 0; 271 | let widget_tree_root = parse_view(&view_body, &mut bindings, &mut terminals, &mut count)?.0; 272 | 273 | Ok(RooftopData { 274 | struct_name, 275 | output_event, 276 | data_fields: data_fields 277 | .expect("failed to find data fields (parameters of build() pseudo-function)"), 278 | widget_tree_root, 279 | bindings, 280 | terminals, 281 | functions: other_functions, 282 | vis, 283 | }) 284 | } 285 | } 286 | 287 | impl RooftopData { 288 | pub(crate) fn compile(self) -> TokenStream { 289 | let struct_name = self.struct_name; 290 | let output_event = self.output_event; 291 | 292 | let data_fields: Vec = self 293 | .data_fields 294 | .list 295 | .iter() 296 | .map(|data_field| { 297 | let name = &data_field.name; 298 | let field_type = &data_field.field_type; 299 | quote! { 300 | pub #name: #field_type, 301 | } 302 | }) 303 | .collect(); 304 | 305 | let data_field_init: Vec = self 306 | .data_fields 307 | .list 308 | .iter() 309 | .map(|data_field| { 310 | let name = &data_field.name; 311 | let default = &data_field.default; 312 | quote! { 313 | #name: #default, 314 | } 315 | }) 316 | .collect(); 317 | 318 | let widget_name = quote::format_ident!("{}Widget", struct_name); 319 | 320 | let crate_name = quote::format_ident!("thunderclap"); 321 | 322 | let mut flattened_nodes = Vec::new(); 323 | flatten_widget_node_tree(&self.widget_tree_root, &mut flattened_nodes); 324 | 325 | let widget_declarations: Vec = flattened_nodes 326 | .iter() 327 | .map(|node| { 328 | let name = &node.var_name; 329 | let type_name = &node.type_name; 330 | let assignments: Vec = node 331 | .data_assignments 332 | .iter() 333 | .map(|assignment| { 334 | let var = &assignment.var; 335 | let value = &assignment.value; 336 | quote! { 337 | #var: #value, 338 | } 339 | }) 340 | .collect(); 341 | quote! { 342 | let mut #name = #crate_name::ui::WidgetConstructor::::construct(#type_name { 343 | #(#assignments)* 344 | ..<#type_name as #crate_name::ui::WidgetConstructor>::from_theme(theme) 345 | }, theme, u_aux); 346 | } 347 | }) 348 | .collect(); 349 | 350 | let widget_names: Vec = flattened_nodes 351 | .iter() 352 | .map(|node| { 353 | let name = &node.var_name; 354 | quote! { 355 | #name, 356 | } 357 | }) 358 | .collect(); 359 | 360 | let widgets_as_fields: Vec = flattened_nodes 361 | .iter() 362 | .rev() 363 | .map(|node| { 364 | let name = &node.var_name; 365 | let type_name = &node.type_name; 366 | quote! { 367 | #[widget_child] 368 | #[repaint_target] 369 | #name: <#type_name as #crate_name::ui::WidgetDataTarget>::Target, 370 | } 371 | }) 372 | .collect(); 373 | 374 | let bindings = &self.bindings; 375 | let terminals = &self.terminals; 376 | 377 | let build_graph = 378 | find_pseudo_function("build_graph", &self.functions).unwrap_or(quote! { { graph } }); 379 | let before_graph = 380 | find_pseudo_function("before_graph", &self.functions).unwrap_or_default(); 381 | let after_graph = find_pseudo_function("after_graph", &self.functions).unwrap_or_default(); 382 | let draw = find_pseudo_function("draw", &self.functions).unwrap_or(quote! { vec![] }); 383 | let setup = find_pseudo_function("setup", &self.functions).unwrap_or_default(); 384 | 385 | let define_layout = self.widget_tree_root.compile_layout(); 386 | 387 | let root_name = &self.widget_tree_root.var_name; 388 | let vis = self.vis; 389 | 390 | { 391 | quote! { 392 | #vis struct #struct_name { 393 | #(#data_fields)* 394 | } 395 | 396 | impl #struct_name { 397 | pub fn from_theme(theme: &dyn #crate_name::draw::Theme) -> Self { 398 | #struct_name { 399 | #(#data_field_init)* 400 | } 401 | } 402 | 403 | pub fn construct(self, theme: &dyn #crate_name::draw::Theme, u_aux: &mut U) -> #widget_name 404 | where 405 | U: #crate_name::base::UpdateAuxiliary, 406 | G: #crate_name::base::GraphicalAuxiliary, 407 | { 408 | let mut data = #crate_name::base::Observed::new(self); 409 | #(#widget_declarations)* 410 | #define_layout; 411 | 412 | use #crate_name::ui::DefaultEventQueue; 413 | let mut graph = #crate_name::reclutch::verbgraph::verbgraph! { 414 | #widget_name as widget, 415 | U as aux, 416 | "bind" => event in &data.on_change => { 417 | change => { 418 | use #crate_name::{ui::DefaultWidgetData, base::WidgetChildren}; 419 | let bind = &mut widget.data; 420 | #(#bindings)* 421 | for child in &mut widget.children_mut() { 422 | child.require_update(aux, "bind"); 423 | } 424 | } 425 | } 426 | #(#terminals)* 427 | }; 428 | 429 | graph = #build_graph; 430 | 431 | // emits false positive event to apply bindings 432 | data.get_mut(); 433 | 434 | let mut output_widget = #widget_name { 435 | event_queue: Default::default(), 436 | data, 437 | graph: graph.into(), 438 | parent_position: Default::default(), 439 | 440 | visibility: Default::default(), 441 | command_group: Default::default(), 442 | layout: Default::default(), 443 | drop_event: Default::default(), 444 | 445 | phantom_themed: Default::default(), 446 | phantom_g: Default::default(), 447 | 448 | #(#widget_names)* 449 | }; 450 | 451 | { 452 | use #crate_name::reclutch::widget::Widget; 453 | output_widget.update(u_aux); 454 | } 455 | 456 | output_widget.widget_setup(theme, u_aux); 457 | 458 | output_widget 459 | 460 | // Phew, all that parsing just to generate this 461 | } 462 | } 463 | 464 | impl #widget_name 465 | where 466 | U: #crate_name::base::UpdateAuxiliary, 467 | G: #crate_name::base::GraphicalAuxiliary, 468 | { 469 | #[doc = "Auto-generated function by `rooftop!`, called automatically."] 470 | fn widget_setup(&mut self, theme: &dyn #crate_name::draw::Theme, u_aux: &mut U) { 471 | #setup 472 | } 473 | } 474 | 475 | impl #crate_name::ui::WidgetDataTarget for #struct_name 476 | where 477 | U: #crate_name::base::UpdateAuxiliary, 478 | G: #crate_name::base::GraphicalAuxiliary, 479 | { 480 | type Target = #widget_name; 481 | } 482 | 483 | #[derive( 484 | WidgetChildren, 485 | LayableWidget, 486 | DropNotifier, 487 | HasVisibility, 488 | Repaintable, 489 | OperatesVerbGraph, 490 | )] 491 | #[widget_children_trait(base::WidgetChildren)] 492 | #[thunderclap_crate(#crate_name)] 493 | #vis struct #widget_name 494 | where 495 | U: #crate_name::base::UpdateAuxiliary, 496 | G: #crate_name::base::GraphicalAuxiliary, 497 | { 498 | pub event_queue: #crate_name::reclutch::event::RcEventQueue<#output_event>, 499 | pub data: #crate_name::base::Observed<#struct_name>, 500 | graph: #crate_name::reclutch::verbgraph::OptionVerbGraph, 501 | parent_position: #crate_name::geom::AbsolutePoint, 502 | 503 | #[widget_visibility] 504 | visibility: #crate_name::base::Visibility, 505 | #[repaint_target] 506 | command_group: #crate_name::reclutch::display::CommandGroup, 507 | #[widget_layout] 508 | layout: #crate_name::base::WidgetLayoutEvents, 509 | #[widget_drop_event] 510 | drop_event: #crate_name::reclutch::event::RcEventQueue<#crate_name::base::DropEvent>, 511 | 512 | #(#widgets_as_fields)* 513 | 514 | phantom_themed: #crate_name::draw::PhantomThemed, 515 | phantom_g: std::marker::PhantomData, 516 | } 517 | 518 | impl #widget_name 519 | where 520 | U: #crate_name::base::UpdateAuxiliary, 521 | G: #crate_name::base::GraphicalAuxiliary, 522 | { 523 | fn on_transform(&mut self) { 524 | use #crate_name::{base::{Repaintable}, geom::ContextuallyRectangular}; 525 | self.repaint(); 526 | self.layout.notify(self.#root_name.abs_rect()); 527 | } 528 | } 529 | 530 | impl #crate_name::reclutch::verbgraph::HasVerbGraph for #widget_name 531 | where 532 | U: base::UpdateAuxiliary, 533 | G: base::GraphicalAuxiliary, 534 | { 535 | fn verb_graph(&mut self) -> &mut #crate_name::reclutch::verbgraph::OptionVerbGraph { 536 | &mut self.graph 537 | } 538 | } 539 | 540 | impl #crate_name::reclutch::widget::Widget for #widget_name 541 | where 542 | U: #crate_name::base::UpdateAuxiliary, 543 | G: #crate_name::base::GraphicalAuxiliary, 544 | { 545 | type UpdateAux = U; 546 | type GraphicalAux = G; 547 | type DisplayObject = #crate_name::reclutch::display::DisplayCommand; 548 | 549 | #[inline] 550 | fn bounds(&self) -> #crate_name::reclutch::display::Rect { 551 | self.#root_name.bounds().cast_unit() 552 | } 553 | 554 | fn update(&mut self, aux: &mut U) { 555 | #crate_name::base::invoke_update(self, aux); 556 | 557 | #before_graph 558 | let mut graph = self.graph.take().unwrap(); 559 | graph.update_all(self, aux); 560 | self.graph = Some(graph); 561 | #after_graph 562 | if let Some(rect) = self.layout.receive() { 563 | use #crate_name::geom::ContextuallyRectangular; 564 | self.#root_name.set_ctxt_rect(rect); 565 | self.command_group.repaint(); 566 | } 567 | } 568 | 569 | fn draw(&mut self, display: &mut dyn #crate_name::reclutch::display::GraphicsDisplay, aux: &mut G) { 570 | self.command_group.push(display, &{ #draw }, Default::default(), None, None); 571 | } 572 | } 573 | 574 | impl #crate_name::base::Movable for #widget_name 575 | where 576 | U: #crate_name::base::UpdateAuxiliary, 577 | G: #crate_name::base::GraphicalAuxiliary, 578 | { 579 | #[inline] 580 | fn set_position(&mut self, position: #crate_name::geom::RelativePoint) { 581 | self.#root_name.set_position(position); 582 | } 583 | 584 | #[inline] 585 | fn position(&self) -> #crate_name::geom::RelativePoint { 586 | self.#root_name.position() 587 | } 588 | } 589 | 590 | impl #crate_name::base::Resizable for #widget_name 591 | where 592 | U: #crate_name::base::UpdateAuxiliary, 593 | G: #crate_name::base::GraphicalAuxiliary, 594 | { 595 | #[inline] 596 | fn set_size(&mut self, size: #crate_name::reclutch::display::Size) { 597 | self.#root_name.set_size(size); 598 | } 599 | 600 | #[inline] 601 | fn size(&self) -> #crate_name::reclutch::display::Size { 602 | self.#root_name.size() 603 | } 604 | } 605 | 606 | impl #crate_name::geom::StoresParentPosition for #widget_name 607 | where 608 | U: #crate_name::base::UpdateAuxiliary, 609 | G: #crate_name::base::GraphicalAuxiliary, 610 | { 611 | fn set_parent_position(&mut self, parent_pos: #crate_name::geom::AbsolutePoint) { 612 | self.parent_position = parent_pos; 613 | self.on_transform(); 614 | } 615 | 616 | #[inline(always)] 617 | fn parent_position(&self) -> #crate_name::geom::AbsolutePoint { 618 | self.parent_position 619 | } 620 | } 621 | 622 | impl #crate_name::draw::HasTheme for #widget_name 623 | where 624 | U: #crate_name::base::UpdateAuxiliary, 625 | G: #crate_name::base::GraphicalAuxiliary, 626 | { 627 | #[inline] 628 | fn theme(&mut self) -> &mut dyn #crate_name::draw::Themed { 629 | &mut self.phantom_themed 630 | } 631 | 632 | fn resize_from_theme(&mut self) {} 633 | } 634 | 635 | impl #crate_name::ui::DefaultEventQueue<#output_event> for #widget_name 636 | where 637 | U: #crate_name::base::UpdateAuxiliary, 638 | G: #crate_name::base::GraphicalAuxiliary, 639 | { 640 | #[inline] 641 | fn default_event_queue(&self) -> &#crate_name::reclutch::event::RcEventQueue<#output_event> { 642 | &self.event_queue 643 | } 644 | } 645 | 646 | impl #crate_name::ui::DefaultWidgetData<#struct_name> for #widget_name 647 | where 648 | U: #crate_name::base::UpdateAuxiliary, 649 | G: #crate_name::base::GraphicalAuxiliary, 650 | { 651 | #[inline] 652 | fn default_data(&mut self) -> &mut #crate_name::base::Observed<#struct_name> { 653 | &mut self.data 654 | } 655 | } 656 | 657 | impl Drop for #widget_name 658 | where 659 | U: #crate_name::base::UpdateAuxiliary, 660 | G: #crate_name::base::GraphicalAuxiliary, 661 | { 662 | fn drop(&mut self) { 663 | use #crate_name::reclutch::prelude::*; 664 | self.drop_event.emit_owned(#crate_name::base::DropEvent); 665 | } 666 | } 667 | } 668 | } 669 | .into() 670 | } 671 | } 672 | 673 | fn find_pseudo_function( 674 | name: &'static str, 675 | functions: &[(syn::Ident, syn::Block)], 676 | ) -> Option { 677 | use quote::ToTokens; 678 | let block = functions.iter().find(|func| func.0 == name)?.1.clone(); 679 | let mut tokens = proc_macro2::TokenStream::new(); 680 | block.to_tokens(&mut tokens); 681 | tokens.into() 682 | } 683 | -------------------------------------------------------------------------------- /thunderclap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thunderclap" 3 | version = "0.0.0" 4 | authors = ["jazzfool "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | description = "Ambitious Rust GUI Toolkit" 8 | homepage = "https://github.com/jazzfool/thunderclap" 9 | repository = "https://github.com/jazzfool/thunderclap" 10 | 11 | [features] 12 | default = ["core-widgets", "default-themes"] 13 | app = ["glutin", "reclutch/skia"] 14 | default-themes = [] 15 | core-widgets = [] 16 | extra-widgets = ["core-widgets"] 17 | 18 | [dependencies] 19 | reclutch = { git = "https://github.com/jazzfool/reclutch" } 20 | thunderclap-macros = { path = "../thunderclap-macros" } 21 | 22 | bitflags = "1.2" 23 | indexmap = "1.3" 24 | thiserror = "1.0" 25 | paste = "0.1" 26 | lazy_static = "1.4" 27 | ambassador = "0.2" 28 | 29 | glutin = { version = "0.23", optional = true } 30 | 31 | [[example]] 32 | name = "counter" 33 | required-features = ["app", "default-themes", "core-widgets"] 34 | -------------------------------------------------------------------------------- /thunderclap/examples/counter/main.rs: -------------------------------------------------------------------------------- 1 | use thunderclap::{ 2 | app, base, 3 | reclutch::display::Color, 4 | themes::Primer, 5 | ui::{Button, HStack, Label, Margins, ScrollBar, SideMargins, TextArea, VStack}, 6 | }; 7 | 8 | #[macro_use] 9 | extern crate reclutch; 10 | #[macro_use] 11 | extern crate thunderclap; 12 | 13 | rooftop! { 14 | struct Counter: () { 15 | fn build( 16 | count: i32 = 0, 17 | btn_color: Color = theme.data().scheme.control_outset, 18 | ) { 19 | Margins(margins=SideMargins::new_all_same(10.0)) { 20 | ScrollBar(), 21 | VStack(bottom_margin=5.0) { 22 | Label( 23 | text=bind(format!("Count: {}", bind.count).into()), 24 | wrap=false, 25 | ), 26 | HStack(left_margin=5.0) { 27 | Button( 28 | text="Count Up".into(), 29 | background=bind(bind.btn_color) 30 | ) 31 | @press { 32 | widget.data.count += 1; 33 | }, 34 | Button( 35 | text="Count Down".into(), 36 | background=bind(bind.btn_color) 37 | ) 38 | @press { 39 | widget.data.count -= 1; 40 | }, 41 | }, 42 | TextArea( 43 | placeholder="placeholder text!".into(), 44 | ), 45 | }, 46 | } 47 | } 48 | } 49 | } 50 | 51 | fn main() { 52 | let app = app::create( 53 | |_g_aux, display| Primer::new(display).unwrap(), 54 | |u_aux, theme| Counter { ..Counter::from_theme(theme) }.construct(theme, u_aux), 55 | app::AppOptions { name: "Showcase".to_string(), ..Default::default() }, 56 | ) 57 | .unwrap(); 58 | app.start(|_| None); 59 | } 60 | -------------------------------------------------------------------------------- /thunderclap/src/app.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{base, draw, error::AppError, geom::*}, 3 | glutin::{ 4 | event::{self, DeviceEvent, Event, WindowEvent}, 5 | event_loop::{ControlFlow, EventLoop}, 6 | window::WindowBuilder, 7 | ContextBuilder, PossiblyCurrent, WindowedContext, 8 | }, 9 | reclutch::{ 10 | display::{ 11 | self, skia, Color, CommandGroup, DisplayCommand, GraphicsDisplay, Point, Size, Vector, 12 | }, 13 | event::RcEventQueue, 14 | prelude::*, 15 | }, 16 | }; 17 | 18 | /// Creates an application with a given theme and root widget. 19 | /// The application uses the Skia OpenGL graphics backend. 20 | /// Small details of app creation can be controlled with `AppOptions`. 21 | pub fn create(theme: TF, root: RF, opts: AppOptions) -> Result, AppError> 22 | where 23 | R: base::WidgetChildren, 24 | T: draw::Theme, 25 | TF: FnOnce(&mut GAux, &mut dyn GraphicsDisplay) -> T, 26 | RF: FnOnce(&mut UAux, &T) -> R, 27 | { 28 | let event_loop = EventLoop::new(); 29 | 30 | let hidpi_factor = event_loop.primary_monitor().scale_factor(); 31 | 32 | let wb = WindowBuilder::new().with_title(opts.name).with_inner_size( 33 | glutin::dpi::PhysicalSize::new( 34 | opts.window_size.width as f64, 35 | opts.window_size.width as f64, 36 | ) 37 | .to_logical::(hidpi_factor), 38 | ); 39 | 40 | let context = ContextBuilder::new().with_vsync(true).build_windowed(wb, &event_loop).unwrap(); 41 | 42 | let context = unsafe { context.make_current().unwrap() }; 43 | 44 | let mut display = 45 | skia::SkiaGraphicsDisplay::new_gl_framebuffer(&skia::SkiaOpenGlFramebuffer { 46 | framebuffer_id: 0, 47 | size: (opts.window_size.width as _, opts.window_size.height as _), 48 | })?; 49 | 50 | let g_aux = GAux { scale: hidpi_factor as _ }; 51 | let mut u_aux = UAux { window_queue: RcEventQueue::new(), cursor: Default::default(), g_aux }; 52 | 53 | let theme = theme(&mut u_aux.g_aux, &mut display); 54 | let root = root(&mut u_aux, &theme); 55 | 56 | let mut app = App { 57 | root, 58 | background: opts.background, 59 | u_aux, 60 | display, 61 | context, 62 | size: opts.window_size, 63 | event_loop, 64 | 65 | command_group_pre: CommandGroup::new(), 66 | command_group_post: CommandGroup::new(), 67 | }; 68 | 69 | for _ in 0..opts.warmup { 70 | app.root.update(&mut app.u_aux); 71 | app.root.draw(&mut app.display, &mut app.u_aux.g_aux); 72 | } 73 | 74 | Ok(app) 75 | } 76 | 77 | fn convert_modifiers(modifiers: event::ModifiersState) -> base::KeyModifiers { 78 | base::KeyModifiers { 79 | shift: modifiers.shift(), 80 | ctrl: modifiers.ctrl(), 81 | alt: modifiers.alt(), 82 | logo: modifiers.logo(), 83 | } 84 | } 85 | 86 | /// Settings on how an app should be created. 87 | #[derive(Debug, Clone)] 88 | pub struct AppOptions { 89 | /// The name of the application; usually translates to the window title. 90 | pub name: String, 91 | /// The number warmup cycles (i.e. the amount of times `update` and `draw` should be called offscreen). 92 | pub warmup: u32, 93 | /// The background color of the window. 94 | pub background: Color, 95 | /// Initial size of the app window. 96 | pub window_size: Size, 97 | } 98 | 99 | impl Default for AppOptions { 100 | fn default() -> Self { 101 | AppOptions { 102 | name: "Thunderclap App".into(), 103 | warmup: 2, 104 | background: Color::new(1.0, 1.0, 1.0, 1.0), 105 | window_size: Size::new(500.0, 500.0), 106 | } 107 | } 108 | } 109 | 110 | /// Thunderclap/Reclutch based application. 111 | pub struct App 112 | where 113 | R: base::WidgetChildren, 114 | { 115 | /// Root widget. 116 | pub root: R, 117 | /// Background color. 118 | pub background: Color, 119 | /// Update auxiliary. 120 | pub u_aux: UAux, 121 | /// Graphics display (Skia backend). 122 | pub display: skia::SkiaGraphicsDisplay, 123 | /// OpenGL context/window. 124 | pub context: WindowedContext, 125 | size: Size, 126 | event_loop: EventLoop<()>, 127 | 128 | command_group_pre: CommandGroup, 129 | command_group_post: CommandGroup, 130 | } 131 | 132 | impl App 133 | where 134 | R: base::WidgetChildren, 135 | { 136 | /// Starts the event loop. 137 | pub fn start(self, mut f: F) -> ! 138 | where 139 | F: 'static + FnMut(Event<()>) -> Option, 140 | R: 'static, 141 | { 142 | let App { 143 | mut root, 144 | background, 145 | mut u_aux, 146 | mut display, 147 | context, 148 | mut size, 149 | event_loop, 150 | 151 | mut command_group_pre, 152 | mut command_group_post, 153 | } = self; 154 | 155 | let mut modifiers = 156 | base::KeyModifiers { shift: false, ctrl: false, alt: false, logo: false }; 157 | 158 | event_loop.run(move |event, _, control_flow| { 159 | *control_flow = ControlFlow::Wait; 160 | 161 | match event { 162 | Event::MainEventsCleared => context.window().request_redraw(), 163 | Event::RedrawRequested(..) => { 164 | if display.size().0 != size.width as _ || display.size().1 != size.height as _ { 165 | display.resize((size.width as _, size.height as _)).unwrap(); 166 | } 167 | 168 | command_group_pre.push( 169 | &mut display, 170 | &[ 171 | DisplayCommand::Save, 172 | DisplayCommand::Clear(background), 173 | DisplayCommand::Scale(Vector::new( 174 | u_aux.g_aux.scale, 175 | u_aux.g_aux.scale, 176 | )), 177 | ], 178 | display::ZOrder(std::i32::MIN), 179 | false, 180 | None, 181 | ); 182 | 183 | base::invoke_draw(&mut root, &mut display, &mut u_aux.g_aux); 184 | 185 | command_group_post.push( 186 | &mut display, 187 | &[DisplayCommand::Restore], 188 | display::ZOrder(std::i32::MAX), 189 | false, 190 | None, 191 | ); 192 | 193 | display.present(None).unwrap(); 194 | 195 | context.swap_buffers().unwrap(); 196 | } 197 | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { 198 | *control_flow = ControlFlow::Exit; 199 | } 200 | Event::WindowEvent { 201 | event: WindowEvent::ScaleFactorChanged { scale_factor: hidpi_factor, .. }, 202 | .. 203 | } => { 204 | u_aux.g_aux.scale = hidpi_factor as _; 205 | let window_size = context.window().inner_size(); 206 | size = Size::new(window_size.width as _, window_size.height as _); 207 | 208 | command_group_pre.repaint(); 209 | } 210 | Event::WindowEvent { event: WindowEvent::Resized(window_size), .. } => { 211 | size = Size::new(window_size.width as _, window_size.height as _); 212 | } 213 | Event::DeviceEvent { 214 | event: DeviceEvent::ModifiersChanged(key_modifiers), .. 215 | } => { 216 | modifiers = convert_modifiers(key_modifiers); 217 | } 218 | Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, .. } => { 219 | let position = position.to_logical::(u_aux.g_aux.scale as f64); 220 | let position = Point::new(position.x as _, position.y as _); 221 | 222 | u_aux.cursor = position.cast_unit(); 223 | 224 | u_aux.window_queue.emit_owned(base::WindowEvent::MouseMove( 225 | base::ConsumableEvent::new((position.cast_unit(), modifiers)), 226 | )); 227 | } 228 | Event::WindowEvent { 229 | event: WindowEvent::MouseInput { state, button, .. }, .. 230 | } => { 231 | let mouse_button = match button { 232 | event::MouseButton::Left => base::MouseButton::Left, 233 | event::MouseButton::Middle => base::MouseButton::Middle, 234 | event::MouseButton::Right => base::MouseButton::Right, 235 | _ => base::MouseButton::Left, 236 | }; 237 | 238 | u_aux.window_queue.emit_owned(base::WindowEvent::ClearFocus); 239 | 240 | u_aux.window_queue.emit_owned(match state { 241 | event::ElementState::Pressed => base::WindowEvent::MousePress( 242 | base::ConsumableEvent::new((u_aux.cursor, mouse_button, modifiers)), 243 | ), 244 | event::ElementState::Released => base::WindowEvent::MouseRelease( 245 | base::ConsumableEvent::new((u_aux.cursor, mouse_button, modifiers)), 246 | ), 247 | }); 248 | } 249 | Event::WindowEvent { event: WindowEvent::ReceivedCharacter(character), .. } => { 250 | u_aux.window_queue.emit_owned(base::WindowEvent::TextInput( 251 | base::ConsumableEvent::new(character), 252 | )); 253 | } 254 | Event::WindowEvent { 255 | event: 256 | WindowEvent::KeyboardInput { 257 | input: event::KeyboardInput { virtual_keycode, state, .. }, 258 | .. 259 | }, 260 | .. 261 | } => { 262 | if let Some(virtual_keycode) = virtual_keycode { 263 | let key_input: base::KeyInput = virtual_keycode.into(); 264 | 265 | u_aux.window_queue.emit_owned(match state { 266 | event::ElementState::Pressed => base::WindowEvent::KeyPress( 267 | base::ConsumableEvent::new((key_input, modifiers)), 268 | ), 269 | event::ElementState::Released => base::WindowEvent::KeyRelease( 270 | base::ConsumableEvent::new((key_input, modifiers)), 271 | ), 272 | }); 273 | } 274 | } 275 | Event::WindowEvent { event: WindowEvent::Focused(false), .. } => { 276 | u_aux.window_queue.emit_owned(base::WindowEvent::ClearFocus); 277 | } 278 | _ => return, 279 | } 280 | 281 | if let Some(cf) = f(event) { 282 | *control_flow = cf; 283 | } 284 | 285 | root.update(&mut u_aux); 286 | }) 287 | } 288 | } 289 | 290 | /// Rudimentary update auxiliary. 291 | pub struct UAux { 292 | pub window_queue: RcEventQueue, 293 | pub cursor: AbsolutePoint, 294 | pub g_aux: GAux, 295 | } 296 | 297 | impl base::UpdateAuxiliary for UAux { 298 | #[inline] 299 | fn window_queue(&self) -> &RcEventQueue { 300 | &self.window_queue 301 | } 302 | 303 | #[inline] 304 | fn window_queue_mut(&mut self) -> &mut RcEventQueue { 305 | &mut self.window_queue 306 | } 307 | 308 | #[inline] 309 | fn graphical(&self) -> &dyn base::GraphicalAuxiliary { 310 | &self.g_aux 311 | } 312 | 313 | #[inline] 314 | fn graphical_mut(&mut self) -> &mut dyn base::GraphicalAuxiliary { 315 | &mut self.g_aux 316 | } 317 | } 318 | 319 | /// Rudimentary graphical auxiliary. 320 | pub struct GAux { 321 | pub scale: f32, 322 | } 323 | 324 | impl base::GraphicalAuxiliary for GAux { 325 | #[inline] 326 | fn scaling(&self) -> f32 { 327 | self.scale 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /thunderclap/src/base.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{draw, geom::*}, 3 | reclutch::{ 4 | display::{Color, CommandGroup, DisplayClip, DisplayCommand, GraphicsDisplay, Rect, Size}, 5 | event::RcEventQueue, 6 | prelude::*, 7 | verbgraph, 8 | widget::Widget, 9 | }, 10 | std::{ 11 | cell::RefCell, 12 | collections::{HashMap, HashSet}, 13 | rc::Rc, 14 | sync::Mutex, 15 | }, 16 | }; 17 | 18 | /// Most straight-forward implementation of `Widget`: `update` and `draw` are propagated to children. 19 | /// 20 | /// # Example 21 | /// ```ignore 22 | /// struct MyWidget; 23 | /// lazy_propagate! { 24 | /// MyWidget, 25 | /// update_aux: MyUpdateAux, 26 | /// graphical_aux: MyGraphicalAux 27 | /// } 28 | /// ``` 29 | /// Rules for generic widgets are the same as the ones described in `lazy_widget!`: 30 | /// ```ignore 31 | /// lazy_propagate! { 32 | /// generic MyGenericWidget 33 | /// // notice we don't supply the aux types; that's the point of generic widgets. 34 | /// } 35 | /// ``` 36 | #[macro_export] 37 | macro_rules! lazy_propagate { 38 | ($name:ty,update_aux:$ua:ty,graphical_aux:$ga:ty) => { 39 | impl $crate::reclutch::Widget for $name { 40 | type UpdateAux = $ua; 41 | type GraphicalAux = $ga; 42 | type DisplayObject = $crate::reclutch::display::DisplayCommand; 43 | 44 | fn update(&mut self, aux: &mut $ua) { 45 | $crate::base::invoke_update(self, aux); 46 | } 47 | 48 | fn draw(&mut self, display: $crate::reclutch::display::GraphicsDisplay, aux: &mut $ga) { 49 | $crate::base::invoke_draw(self, display, aux); 50 | } 51 | } 52 | }; 53 | (generic $name:ty) => { 54 | impl 55 | $crate::reclutch::Widget for $name 56 | { 57 | type UpdateAux = U; 58 | type GraphicalAux = G; 59 | type DisplayObject = $crate::reclutch::display::DisplayCommand; 60 | 61 | fn update(&mut self, aux: &mut U) { 62 | $crate::base::invoke_update(self, aux); 63 | } 64 | 65 | fn draw(&mut self, display: $crate::reclutch::display::GraphicsDisplay, aux: &mut G) { 66 | $crate::base::invoke_draw(self, display, aux); 67 | } 68 | } 69 | }; 70 | } 71 | 72 | /// A custom widget children trait with additional bounds. 73 | /// This is used as an alternative to `reclutch::widget::WidgetChildren`. 74 | /// 75 | /// You can still use this with the derive macro as follows: 76 | /// ```ignore 77 | /// use reclutch::WidgetChildren; 78 | /// #[derive(WidgetChildren)] 79 | /// #[widget_children_trait(thunderclap::base::WidgetChildren)] 80 | /// struct MyWidget; 81 | /// ``` 82 | pub trait WidgetChildren: 83 | Widget 84 | + draw::HasTheme 85 | + Repaintable 86 | + HasVisibility 87 | + ContextuallyMovable 88 | + verbgraph::OperatesVerbGraph 89 | { 90 | /// Returns a list of all the children as a vector of immutable `dyn WidgetChildren`. 91 | fn children( 92 | &self, 93 | ) -> Vec< 94 | &dyn WidgetChildren< 95 | UpdateAux = Self::UpdateAux, 96 | GraphicalAux = Self::GraphicalAux, 97 | DisplayObject = Self::DisplayObject, 98 | >, 99 | > { 100 | Vec::new() 101 | } 102 | 103 | /// Returns a list of all the children as a vector of mutable `dyn WidgetChildren`. 104 | fn children_mut( 105 | &mut self, 106 | ) -> Vec< 107 | &mut dyn WidgetChildren< 108 | UpdateAux = Self::UpdateAux, 109 | GraphicalAux = Self::GraphicalAux, 110 | DisplayObject = Self::DisplayObject, 111 | >, 112 | > { 113 | Vec::new() 114 | } 115 | } 116 | 117 | /// Implemented by widgets that can be repainted. 118 | pub trait Repaintable: Widget { 119 | /// Repaints the widget (typically means invoking `repaint` on the inner command group). 120 | fn repaint(&mut self); 121 | } 122 | 123 | /// Implemented by widgets that can be moved/positioned. 124 | pub trait Movable: Widget { 125 | /// Changes the current position of the widget. 126 | fn set_position(&mut self, position: RelativePoint); 127 | /// Returns the current position of the widget. 128 | fn position(&self) -> RelativePoint; 129 | } 130 | 131 | /// Implemented by widgets that can be resized. 132 | pub trait Resizable: Widget { 133 | /// Changes the current size of the widget. 134 | fn set_size(&mut self, size: Size); 135 | /// Returns the current size of the widget. 136 | fn size(&self) -> Size; 137 | } 138 | 139 | /// Implemented by widgets that can be moved and resized. 140 | /// 141 | /// There's no need to implement this manually, as long as `Movable` and `Resizable` 142 | /// have been implemented, this will be automatically implemented alongside them. 143 | pub trait Rectangular: Widget + Movable + Resizable { 144 | /// Changes the rectangular bounds. 145 | /// 146 | /// If `Rectangular` is a blanket implementation, then this simply becomes 147 | /// `set_position()` and `set_size()`. 148 | fn set_rect(&mut self, rect: RelativeRect); 149 | /// Returns the rectangular bounds. 150 | /// 151 | /// If `Rectangular` is a blanket implementation, then this is simply a constructor 152 | /// for `Rect` based on the values returned from `position()` and `size()`. 153 | fn rect(&self) -> RelativeRect; 154 | } 155 | 156 | impl Rectangular for T 157 | where 158 | T: Widget + Movable + Resizable, 159 | { 160 | #[inline] 161 | fn set_rect(&mut self, rect: RelativeRect) { 162 | self.set_position(rect.origin); 163 | self.set_size(rect.size.cast_unit()); 164 | } 165 | 166 | #[inline] 167 | fn rect(&self) -> RelativeRect { 168 | RelativeRect::new(self.position(), self.size().cast_unit()) 169 | } 170 | } 171 | 172 | /// Describes the interactivity/visibility condition of a widget. 173 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 174 | pub enum Visibility { 175 | /// Is rendered and receives updates. 176 | Normal, 177 | /// Receives updates but isn't rendered. 178 | Invisible, 179 | /// Is rendered but doesn't receive updates. 180 | Static, 181 | /// Is neither rendered nor updated. 182 | None, 183 | } 184 | 185 | impl Default for Visibility { 186 | #[inline] 187 | fn default() -> Self { 188 | Visibility::Normal 189 | } 190 | } 191 | 192 | /// Implemented by widgets which are capable of tracking visibility. 193 | pub trait HasVisibility { 194 | /// Changes the widget visibility. 195 | fn set_visibility(&mut self, visibility: Visibility); 196 | /// Returns the widget visibility. 197 | fn visibility(&self) -> Visibility; 198 | } 199 | 200 | /// Trait required for any type passed as the `UpdateAux` type (seen as `U` in the widget type parameters) 201 | /// with accessors required for usage within Thunderclap-implemented widgets. 202 | pub trait UpdateAuxiliary: 'static { 203 | /// Returns the queue where window events (`WindowEvent`) are emitted, immutably. 204 | fn window_queue(&self) -> &RcEventQueue; 205 | /// Returns the queue where window events (`WindowEvent`) are emitted, mutably. 206 | fn window_queue_mut(&mut self) -> &mut RcEventQueue; 207 | /// Returns the respective graphical auxiliary. 208 | fn graphical(&self) -> &dyn GraphicalAuxiliary; 209 | /// Returns the respective graphical auxiliary mutably. 210 | fn graphical_mut(&mut self) -> &mut dyn GraphicalAuxiliary; 211 | } 212 | 213 | /// Trait required for any type passed as the `GraphicalAux` type (seen as `G` in the widget type parameters) 214 | /// with accessors required for usage within Thunderclap-implemented widgets. 215 | pub trait GraphicalAuxiliary: 'static { 216 | /// Returns the HiDPI scaling factor. 217 | fn scaling(&self) -> f32; 218 | } 219 | 220 | /// Propagates `update` to the children of a widget. 221 | pub fn invoke_update( 222 | widget: &mut dyn WidgetChildren< 223 | UpdateAux = U, 224 | GraphicalAux = G, 225 | DisplayObject = DisplayCommand, 226 | >, 227 | aux: &mut U, 228 | ) { 229 | // Iterate in reverse because most visually forefront widgets should get events first. 230 | for child in widget.children_mut().into_iter().rev() { 231 | match child.visibility() { 232 | Visibility::Static | Visibility::None => {} 233 | _ => child.update(aux), 234 | } 235 | } 236 | } 237 | 238 | #[derive(Clone, Debug, PartialEq)] 239 | struct ConsumableEventInner { 240 | marker: RefCell, 241 | data: T, 242 | } 243 | 244 | /// Event data that can be "consumed". This is needed for events such as clicking and typing. 245 | /// Those kinds of events aren't typically received by multiple widgets. 246 | /// 247 | /// As an example of this, say you have multiple buttons stacked atop each other. 248 | /// When you click that stack of buttons, only the one on top should receive the click event, 249 | /// as in, the event is *consumed*. 250 | /// 251 | /// Note that this primitive isn't very strict. The consumption conditions can be bypassed 252 | /// in case the data needs to be accessed regardless of state, and the predicate can be 253 | /// exploited to use the data without consuming it. 254 | /// 255 | /// Also note that the usage of "consume" is completely unrelated to the consume/move 256 | /// semantics of Rust. In fact, nothing is actually consumed in this implementation. 257 | #[derive(Debug, PartialEq)] 258 | pub struct ConsumableEvent(Rc>); 259 | 260 | impl ConsumableEvent { 261 | /// Creates a unconsumed event, initialized with `val`. 262 | pub fn new(val: T) -> Self { 263 | ConsumableEvent(Rc::new(ConsumableEventInner { marker: RefCell::new(true), data: val })) 264 | } 265 | 266 | /// Returns the event data as long as **both** the following conditions are satisfied: 267 | /// 1. The event hasn't been consumed yet. 268 | /// 2. The predicate returns true. 269 | /// 270 | /// The point of the predicate is to let the caller see if the event actually applies 271 | /// to them before consuming needlessly. 272 | pub fn with

(&self, mut pred: P) -> Option<&T> 273 | where 274 | P: FnMut(&T) -> bool, 275 | { 276 | let mut is_consumed = self.0.marker.borrow_mut(); 277 | if *is_consumed && pred(&self.0.data) { 278 | *is_consumed = false; 279 | Some(&self.0.data) 280 | } else { 281 | None 282 | } 283 | } 284 | 285 | /// Returns the inner event data regardless of consumption. 286 | #[inline(always)] 287 | pub fn get(&self) -> &T { 288 | &self.0.data 289 | } 290 | } 291 | 292 | impl Clone for ConsumableEvent { 293 | fn clone(&self) -> Self { 294 | ConsumableEvent(self.0.clone()) 295 | } 296 | } 297 | 298 | /// An event related to the window, e.g. input. 299 | #[derive(Event, Debug, Clone, PartialEq)] 300 | pub enum WindowEvent { 301 | /// The user pressed a mouse button. 302 | #[event_key(mouse_press)] 303 | MousePress(ConsumableEvent<(AbsolutePoint, MouseButton, KeyModifiers)>), 304 | /// The user released a mouse button. 305 | /// This event complements `MousePress`, which means it realistically can only 306 | /// be emitted after `MousePress` has been emitted. 307 | #[event_key(mouse_release)] 308 | MouseRelease(ConsumableEvent<(AbsolutePoint, MouseButton, KeyModifiers)>), 309 | /// The user moved the cursor. 310 | #[event_key(mouse_move)] 311 | MouseMove(ConsumableEvent<(AbsolutePoint, KeyModifiers)>), 312 | /// Emitted when a text input is received. 313 | #[event_key(text_input)] 314 | TextInput(ConsumableEvent), 315 | /// Emitted when a key is pressed. 316 | #[event_key(key_press)] 317 | KeyPress(ConsumableEvent<(KeyInput, KeyModifiers)>), 318 | /// Emitted when a key is released. 319 | #[event_key(key_release)] 320 | KeyRelease(ConsumableEvent<(KeyInput, KeyModifiers)>), 321 | /// Emitted immediately before an event which is capable of changing focus. 322 | /// If implementing a focus-able widget, to handle this event, simply clear 323 | /// the local "focused" flag (which should ideally be stored as `draw::state::InteractionState`). 324 | #[event_key(clear_focus)] 325 | ClearFocus, 326 | } 327 | 328 | // Most of these are copied from `winit`. 329 | // We can't reuse the `winit` types because `winit` is an optional dependency (app feature). 330 | 331 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 332 | pub struct KeyModifiers { 333 | pub shift: bool, 334 | pub ctrl: bool, 335 | pub alt: bool, 336 | pub logo: bool, 337 | } 338 | 339 | /// Button on a mouse. 340 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 341 | pub enum MouseButton { 342 | Left, 343 | Middle, 344 | Right, 345 | } 346 | 347 | // Previously: `std::mem::transmute::(virtual_key)`. 348 | // Now: `virtual_key.into()`. 349 | // :) 350 | macro_rules! keyboard_enum { 351 | ($name:ident as $other:ty { 352 | $($v:ident),*$(,)? 353 | }) => { 354 | #[doc = "Key on a keyboard."] 355 | #[repr(u32)] 356 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 357 | pub enum $name { 358 | $($v),* 359 | } 360 | 361 | #[cfg(feature = "app")] 362 | impl From<$other> for $name { 363 | fn from(other: $other) -> $name { 364 | match other { 365 | $(<$other>::$v => $name::$v),* 366 | } 367 | } 368 | } 369 | 370 | #[cfg(feature = "app")] 371 | impl Into<$other> for $name { 372 | fn into(self) -> $other { 373 | match self { 374 | $($name::$v => <$other>::$v),* 375 | } 376 | } 377 | } 378 | }; 379 | } 380 | 381 | keyboard_enum! { 382 | KeyInput as glutin::event::VirtualKeyCode { 383 | Key1, 384 | Key2, 385 | Key3, 386 | Key4, 387 | Key5, 388 | Key6, 389 | Key7, 390 | Key8, 391 | Key9, 392 | Key0, 393 | A, 394 | B, 395 | C, 396 | D, 397 | E, 398 | F, 399 | G, 400 | H, 401 | I, 402 | J, 403 | K, 404 | L, 405 | M, 406 | N, 407 | O, 408 | P, 409 | Q, 410 | R, 411 | S, 412 | T, 413 | U, 414 | V, 415 | W, 416 | X, 417 | Y, 418 | Z, 419 | Escape, 420 | F1, 421 | F2, 422 | F3, 423 | F4, 424 | F5, 425 | F6, 426 | F7, 427 | F8, 428 | F9, 429 | F10, 430 | F11, 431 | F12, 432 | F13, 433 | F14, 434 | F15, 435 | F16, 436 | F17, 437 | F18, 438 | F19, 439 | F20, 440 | F21, 441 | F22, 442 | F23, 443 | F24, 444 | Snapshot, 445 | Scroll, 446 | Pause, 447 | Insert, 448 | Home, 449 | Delete, 450 | End, 451 | PageDown, 452 | PageUp, 453 | Left, 454 | Up, 455 | Right, 456 | Down, 457 | Back, 458 | Return, 459 | Space, 460 | Compose, 461 | Caret, 462 | Numlock, 463 | Numpad0, 464 | Numpad1, 465 | Numpad2, 466 | Numpad3, 467 | Numpad4, 468 | Numpad5, 469 | Numpad6, 470 | Numpad7, 471 | Numpad8, 472 | Numpad9, 473 | AbntC1, 474 | AbntC2, 475 | Add, 476 | Apostrophe, 477 | Apps, 478 | At, 479 | Ax, 480 | Backslash, 481 | Calculator, 482 | Capital, 483 | Colon, 484 | Comma, 485 | Convert, 486 | Decimal, 487 | Divide, 488 | Equals, 489 | Grave, 490 | Kana, 491 | Kanji, 492 | LAlt, 493 | LBracket, 494 | LControl, 495 | LShift, 496 | LWin, 497 | Mail, 498 | MediaSelect, 499 | MediaStop, 500 | Minus, 501 | Multiply, 502 | Mute, 503 | MyComputer, 504 | NavigateForward, 505 | NavigateBackward, 506 | NextTrack, 507 | NoConvert, 508 | NumpadComma, 509 | NumpadEnter, 510 | NumpadEquals, 511 | OEM102, 512 | Period, 513 | PlayPause, 514 | Power, 515 | PrevTrack, 516 | RAlt, 517 | RBracket, 518 | RControl, 519 | RShift, 520 | RWin, 521 | Semicolon, 522 | Slash, 523 | Sleep, 524 | Stop, 525 | Subtract, 526 | Sysrq, 527 | Tab, 528 | Underline, 529 | Unlabeled, 530 | VolumeDown, 531 | VolumeUp, 532 | Wake, 533 | WebBack, 534 | WebFavorites, 535 | WebForward, 536 | WebHome, 537 | WebRefresh, 538 | WebSearch, 539 | WebStop, 540 | Yen, 541 | Copy, 542 | Paste, 543 | Cut, 544 | } 545 | } 546 | 547 | /// Information about a parent layout with a queue which receives updated rectangles. 548 | #[derive(Debug)] 549 | pub struct WidgetLayoutEventsInner { 550 | pub id: u64, 551 | pub evq: reclutch::event::bidir_single::Secondary, 552 | } 553 | 554 | /// Helper layout over `WidgetLayoutEventsInner`; optionally stores information about a parent layout. 555 | #[derive(Default, Debug)] 556 | pub struct WidgetLayoutEvents(Option); 557 | 558 | impl WidgetLayoutEvents { 559 | /// Creates `WidgetLayoutEvents` not connected to any layout. 560 | /// This means all the getters will return `None`. 561 | pub fn new() -> Self { 562 | Default::default() 563 | } 564 | 565 | /// Creates `WidgetLayoutEvents` from the given layout information. 566 | pub fn from_layout(layout: WidgetLayoutEventsInner) -> Self { 567 | WidgetLayoutEvents(Some(layout)) 568 | } 569 | 570 | /// Possibly returns the inner associated layout ID. 571 | pub fn id(&self) -> Option { 572 | self.0.as_ref().map(|inner| inner.id) 573 | } 574 | 575 | /// Possibly updates the layout information. 576 | pub fn update(&mut self, layout: impl Into>) { 577 | self.0 = layout.into(); 578 | } 579 | 580 | /// Notifies the layout that the widget rectangle has been updated from the widget side. 581 | pub fn notify(&mut self, rect: AbsoluteRect) { 582 | if let Some(inner) = &mut self.0 { 583 | inner.evq.emit_owned(rect); 584 | } 585 | } 586 | 587 | /// Returns the most up-to-date widget rectangle from the layout. 588 | pub fn receive(&mut self) -> Option { 589 | self.0.as_mut().and_then(|inner| inner.evq.retrieve_newest()) 590 | } 591 | } 592 | 593 | /// Widget that is capable of listening to layout events. 594 | pub trait LayableWidget: WidgetChildren + ContextuallyRectangular + DropNotifier { 595 | fn listen_to_layout(&mut self, layout: impl Into>); 596 | fn layout_id(&self) -> Option; 597 | } 598 | 599 | /// Widget which emits layout events to registered widgets. 600 | pub trait Layout: WidgetChildren + Rectangular + Sized { 601 | type PushData; 602 | 603 | /// "Registers" a widget to the layout. 604 | fn push(&mut self, data: Option, child: &mut impl LayableWidget); 605 | 606 | /// De-registers a widget from the layout, optionally restoring the original widget rectangle. 607 | fn remove(&mut self, child: &mut impl LayableWidget, restore_original: bool); 608 | } 609 | 610 | /// Empty event indicating `Observed` data has changed. 611 | #[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Hash)] 612 | #[event_key(drop)] 613 | pub struct DropEvent; 614 | 615 | /// Widget which has an event queue where a single event is emitted when the widget is dropped. 616 | pub trait DropNotifier: Widget { 617 | fn drop_event(&self) -> &RcEventQueue; 618 | } 619 | 620 | /// Empty event indicating `Observed` data has changed. 621 | #[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Hash)] 622 | #[event_key(change)] 623 | pub struct ObservedEvent; 624 | 625 | /// Wrapper which emits an event whenever the inner variable is changed. 626 | #[derive(Debug)] 627 | pub struct Observed { 628 | pub on_change: RcEventQueue, 629 | 630 | inner: T, 631 | } 632 | 633 | impl Observed { 634 | pub fn new(val: T) -> Self { 635 | Observed { on_change: RcEventQueue::new(), inner: val } 636 | } 637 | 638 | /// Updates the inner variable. 639 | /// Emits an event to `on_change` when invoked. 640 | #[inline] 641 | pub fn set(&mut self, val: T) { 642 | self.inner = val; 643 | self.on_change.emit_owned(ObservedEvent); 644 | } 645 | 646 | /// Returns an immutable reference to the inner variable. 647 | #[inline(always)] 648 | pub fn get(&self) -> &T { 649 | &self.inner 650 | } 651 | 652 | /// Returns a mutable reference to the inner variable. 653 | /// Emits an event to `on_change` when invoked. 654 | #[inline] 655 | pub fn get_mut(&mut self) -> &mut T { 656 | self.on_change.emit_owned(ObservedEvent); 657 | &mut self.inner 658 | } 659 | } 660 | 661 | impl std::ops::Deref for Observed { 662 | type Target = T; 663 | fn deref(&self) -> &T { 664 | &self.inner 665 | } 666 | } 667 | 668 | impl std::ops::DerefMut for Observed { 669 | fn deref_mut(&mut self) -> &mut T { 670 | self.on_change.emit_owned(ObservedEvent); 671 | &mut self.inner 672 | } 673 | } 674 | 675 | #[macro_export] 676 | macro_rules! observe { 677 | ($($x:ident),*) => { 678 | $(let $x = $crate::base::Observed::new($x);)* 679 | }; 680 | } 681 | 682 | lazy_static::lazy_static! { 683 | // Frame counter used by `invoke_draw`, resets back to 0 after 60 frames. 684 | // This is used to only clean up `CLIP_LIST` every 60 frames. 685 | static ref DRAW_COUNTER: Mutex = Mutex::new(0); 686 | // Map of pre/post command groups loosely linked to a widget by using the memory address as a unique identifier. 687 | static ref CLIP_LIST: Mutex> = 688 | Mutex::new(HashMap::new()); 689 | } 690 | 691 | fn invoke_draw_impl( 692 | widget: &mut dyn WidgetChildren< 693 | UpdateAux = U, 694 | GraphicalAux = G, 695 | DisplayObject = DisplayCommand, 696 | >, 697 | display: &mut dyn GraphicsDisplay, 698 | aux: &mut G, 699 | clip_list: &mut HashMap, 700 | checked: &mut Option>, 701 | ) { 702 | if widget.visibility() != Visibility::Invisible && widget.visibility() != Visibility::None { 703 | // we're not dereferencing the pointer so it's fine... right? 704 | #[allow(clippy::cast_ptr_alignment)] 705 | let id = widget as *const _ as *const usize as _; 706 | let (clip, restore) = 707 | clip_list.entry(id).or_insert_with(|| (CommandGroup::new(), CommandGroup::new())); 708 | let clip_rect = widget.abs_bounds(); 709 | clip.repaint(); 710 | restore.repaint(); 711 | // later on when partial repainting is implemented, this plays an important role in 712 | // making sure it works correctly. Essentially it forces widgets to be exact and explicit 713 | // in reporting their paint boundaries, otherwise it gets clipped. 714 | clip.push( 715 | display, 716 | &[ 717 | DisplayCommand::Save, 718 | DisplayCommand::Clip(DisplayClip::Rectangle { 719 | rect: clip_rect.cast_unit(), 720 | antialias: true, 721 | }), 722 | DisplayCommand::Save, 723 | ], 724 | Default::default(), 725 | false, 726 | None, 727 | ); 728 | 729 | widget.draw(display, aux); 730 | 731 | restore.push( 732 | display, 733 | &[DisplayCommand::Restore, DisplayCommand::Restore], 734 | Default::default(), 735 | false, 736 | None, 737 | ); 738 | 739 | if let Some(ref mut checked) = *checked { 740 | checked.insert(id); 741 | } 742 | } 743 | 744 | for child in widget.children_mut() { 745 | invoke_draw_impl(child, display, aux, clip_list, checked); 746 | } 747 | } 748 | 749 | /// Recursively invokes `draw`. 750 | /// This will invoke draw (with some extra steps, see below) 751 | /// for `widget`, then invoke `invoke_draw` all of `widget`s children. 752 | /// 753 | /// Extra processing steps: 754 | /// - Skip if widget visibility is `Invisible` or `None`. 755 | /// - Clip to absolute widget bounds. 756 | /// - Add widget position to auxiliary tracer. 757 | pub fn invoke_draw( 758 | widget: &mut dyn WidgetChildren< 759 | UpdateAux = U, 760 | GraphicalAux = G, 761 | DisplayObject = DisplayCommand, 762 | >, 763 | display: &mut dyn GraphicsDisplay, 764 | aux: &mut G, 765 | ) { 766 | let mut draw_counter = DRAW_COUNTER.lock().unwrap(); 767 | let mut clip_list = CLIP_LIST.lock().unwrap(); 768 | 769 | // Every 60 frames clean up CLIP_LIST. 770 | // To do so, gather information on which widget ptrs have been maintained. 771 | let mut checked = if *draw_counter >= 60 { Some(HashSet::new()) } else { None }; 772 | 773 | invoke_draw_impl(widget, display, aux, &mut clip_list, &mut checked); 774 | 775 | // Perform cleanup (checked is only contains a value if on 60th frame). 776 | if let Some(checked) = checked { 777 | *draw_counter = 0; 778 | clip_list.retain(|widget_ptr, _| checked.contains(widget_ptr)); 779 | } 780 | 781 | *draw_counter += 1; 782 | } 783 | 784 | /// Creates a color from 3 unsigned 8-bit components and an `f32` alpha. 785 | /// This replicates CSS syntax (e.g. `rgba(28, 196, 54, 0.3)`). 786 | pub fn color_from_urgba(r: u8, g: u8, b: u8, a: f32) -> Color { 787 | Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a) 788 | } 789 | 790 | /// Aligns a rectangle with regards to Skia anti-aliasing. 791 | pub fn sharp_align(rect: Rect) -> Rect { 792 | rect.round_in().inflate(0.5, 0.5) 793 | } 794 | -------------------------------------------------------------------------------- /thunderclap/src/draw/mod.rs: -------------------------------------------------------------------------------- 1 | //! Simple theme framework based on Flutter. 2 | 3 | pub mod state; 4 | 5 | use { 6 | crate::{base, geom::*}, 7 | reclutch::display::{Color, DisplayCommand, FontInfo, ResourceReference, Size}, 8 | }; 9 | 10 | /// Implemented by types which are capable of changing themes. 11 | pub trait Themed { 12 | /// Updates `self` from `theme`. 13 | fn load_theme(&mut self, theme: &dyn Theme, aux: &dyn base::GraphicalAuxiliary); 14 | } 15 | 16 | /// Empty `Themed` type to assist in satisfying `HasTheme` required by `WidgetChildren` 17 | /// for widgets which don't have a visual appearance (e.g. layout widgets). 18 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] 19 | pub struct PhantomThemed; 20 | 21 | impl Themed for PhantomThemed { 22 | fn load_theme(&mut self, _theme: &dyn Theme, _aux: &dyn base::GraphicalAuxiliary) {} 23 | } 24 | 25 | /// Object of a theme which paints a single state (which typically represents a single widget). 26 | pub trait Painter { 27 | /// Invokes the corresponding method from a given `Theme` to retrieve the same 28 | /// `Painter` which `self` was built from previously. 29 | fn invoke(&self, theme: &dyn Theme) -> Box>; 30 | /// Returns a stylistic size based on the state. 31 | fn size_hint(&self, state: T) -> Size; 32 | /// Returns the paint boundaries based on the inner bounds. 33 | fn paint_hint(&self, rect: RelativeRect) -> RelativeRect; 34 | /// Returns the mouse boundaries based on the inner bounds. 35 | fn mouse_hint(&self, rect: RelativeRect) -> RelativeRect; 36 | /// Returns a list of display commands which visualize `state`. 37 | fn draw(&mut self, state: T) -> Vec; 38 | } 39 | 40 | /// Lightens a color by a specified amount 41 | pub fn lighten(color: Color, amount: f32) -> Color { 42 | use reclutch::palette::Shade; 43 | Color::from_linear(color.into_linear().lighten(amount)) 44 | } 45 | 46 | /// Darkens a color by a specified amount 47 | pub fn darken(color: Color, amount: f32) -> Color { 48 | use reclutch::palette::Shade; 49 | Color::from_linear(color.into_linear().darken(amount)) 50 | } 51 | 52 | /// Darkens or lightens a color to contrast the theme. 53 | pub fn strengthen(color: Color, amount: f32, contrast: ThemeContrast) -> Color { 54 | match contrast { 55 | ThemeContrast::Light => darken(color, amount), 56 | ThemeContrast::Dark => lighten(color, amount), 57 | } 58 | } 59 | 60 | /// Darkens or lightens a color to get closer with the theme. 61 | pub fn weaken(color: Color, amount: f32, contrast: ThemeContrast) -> Color { 62 | match contrast { 63 | ThemeContrast::Light => lighten(color, amount), 64 | ThemeContrast::Dark => darken(color, amount), 65 | } 66 | } 67 | 68 | /// Returns the color with a different opacity. 69 | pub fn with_opacity(color: Color, opacity: f32) -> Color { 70 | Color::new(color.red, color.green, color.blue, opacity) 71 | } 72 | 73 | /// A consistent palette of colors used throughout the UI. 74 | #[derive(Debug, Clone, Copy, PartialEq)] 75 | pub struct ColorScheme { 76 | /// Background color. 77 | pub background: Color, 78 | /// A color which indicates an error. 79 | pub error: Color, 80 | /// A color which indicates component focus. 81 | pub focus: Color, 82 | /// A primary color used often. 83 | pub primary: Color, 84 | /// A control which is "outset", like a button. 85 | pub control_outset: Color, 86 | /// A control which is "inset", such as a text box. 87 | pub control_inset: Color, 88 | /// A color which appears clearly over `error`. 89 | pub over_error: Color, 90 | /// A color which appears clearly over `focus`. 91 | pub over_focus: Color, 92 | /// A color which appears clearly over `primary`. 93 | pub over_primary: Color, 94 | /// A color which appears clearly over `control_outset`. 95 | pub over_control_outset: Color, 96 | /// A color which appears clearly over `control_inset`. 97 | pub over_control_inset: Color, 98 | } 99 | 100 | /// A single typeface in 2 weights and italics. 101 | #[derive(Debug, Clone)] 102 | pub struct Typeface { 103 | pub regular: (ResourceReference, FontInfo), 104 | pub italic: (ResourceReference, FontInfo), 105 | pub bold: (ResourceReference, FontInfo), 106 | pub bold_italic: (ResourceReference, FontInfo), 107 | } 108 | 109 | impl PartialEq for Typeface { 110 | fn eq(&self, other: &Typeface) -> bool { 111 | self.regular.0 == other.regular.0 112 | && self.italic.0 == other.italic.0 113 | && self.bold.0 == other.bold.0 114 | && self.bold_italic.0 == other.bold_italic.0 115 | } 116 | } 117 | 118 | impl Eq for Typeface {} 119 | 120 | impl Typeface { 121 | pub fn pick(&self, style: TextStyle) -> (ResourceReference, FontInfo) { 122 | match style { 123 | TextStyle::Regular => self.regular.clone(), 124 | TextStyle::RegularItalic => self.italic.clone(), 125 | TextStyle::Bold => self.bold.clone(), 126 | TextStyle::BoldItalic => self.bold_italic.clone(), 127 | } 128 | } 129 | } 130 | 131 | /// Text weights and italics. 132 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 133 | pub enum TextStyle { 134 | /// "Baseline" font weight. 135 | Regular, 136 | /// Italicized variant of `Regular`. 137 | RegularItalic, 138 | /// Bold font weight. 139 | Bold, 140 | /// Italicized variant of `Bold`. 141 | BoldItalic, 142 | } 143 | 144 | /// A typeface with text size and text style. 145 | #[derive(Debug, Clone, PartialEq)] 146 | pub struct TypefaceStyle { 147 | pub typeface: Typeface, 148 | /// Text size in pixels. 149 | pub size: f32, 150 | /// Text style (regular, italic, etc). 151 | pub style: TextStyle, 152 | } 153 | 154 | /// List of typefaces used throughout the UI. 155 | #[derive(Debug, Clone)] 156 | pub struct Typography { 157 | /// Typeface used in headers, e.g. titles. 158 | pub header: TypefaceStyle, 159 | /// Typeface used in sub-headers, which typically appear underneath `header`s. 160 | pub sub_header: TypefaceStyle, 161 | /// Typeface used in regular text. 162 | pub body: TypefaceStyle, 163 | /// Typeface used in control widgets. 164 | pub button: TypefaceStyle, 165 | } 166 | 167 | /// The "contrast" mode of a theme, i.e. light or dark. 168 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 169 | pub enum ThemeContrast { 170 | Light, 171 | Dark, 172 | } 173 | 174 | /// Various information about a theme, including color scheme and fonts. 175 | #[derive(Debug, Clone)] 176 | pub struct ThemeData { 177 | /// Color scheme of the theme. 178 | pub scheme: ColorScheme, 179 | /// A list of typefaces used stylistically within the theme. 180 | pub typography: Typography, 181 | /// Contras mode of the theme. 182 | pub contrast: ThemeContrast, 183 | } 184 | 185 | /// Factory to create colors or `Painter`s which paint widgets with a specific visual theme. 186 | pub trait Theme { 187 | /// Constructs a painter for a button. 188 | fn button(&self) -> Box>; 189 | /// Constructs a painter for a checkbox. 190 | fn checkbox(&self) -> Box>; 191 | /// Constructs a painter for a text area. 192 | fn text_area(&self) -> Box>; 193 | /// Constructs a painter for a scroll bar. 194 | fn scroll_bar(&self) -> Box>; 195 | 196 | fn data(&self) -> &ThemeData; 197 | } 198 | 199 | /// Implemented by types which have an inner `Themed` (but usually widgets with 200 | /// an inner `Box>`, which implements `Themed`). 201 | pub trait HasTheme { 202 | /// Returns the inner `Themed`. 203 | fn theme(&mut self) -> &mut dyn Themed; 204 | /// *Possibly* invokes `size_hint` on the inner `Painter` and applies it. 205 | fn resize_from_theme(&mut self); 206 | } 207 | 208 | impl Themed for Box> { 209 | fn load_theme(&mut self, theme: &dyn Theme, _aux: &dyn base::GraphicalAuxiliary) { 210 | *self = self.invoke(theme); 211 | } 212 | } 213 | 214 | impl Themed for T 215 | where 216 | T: HasTheme, 217 | { 218 | fn load_theme(&mut self, theme: &dyn Theme, aux: &dyn base::GraphicalAuxiliary) { 219 | self.theme().load_theme(theme, aux); 220 | self.resize_from_theme(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /thunderclap/src/draw/state.rs: -------------------------------------------------------------------------------- 1 | //! The visual states for a widget. 2 | //! 3 | //! These are simply the fields relevant to rendering, existing only 4 | //! in the scope of the `draw` method. 5 | 6 | use crate::{geom::*, ui}; 7 | 8 | /// Visually relevant states of a [`Button`](../ui/struct.Button.html). 9 | #[derive(Debug, Clone)] 10 | pub struct ButtonState { 11 | pub rect: AbsoluteRect, 12 | pub data: ui::Button, 13 | pub interaction: InteractionState, 14 | } 15 | 16 | bitflags::bitflags! { 17 | pub struct InteractionState: u32 { 18 | const HOVERED = 1; 19 | const PRESSED = 1 << 1; 20 | const FOCUSED = 1 << 2; 21 | } 22 | } 23 | 24 | /// Visually relevant states of a [`Checkbox`](../ui/struct.Checkbox.html). 25 | #[derive(Debug, Clone, Copy, PartialEq)] 26 | pub struct CheckboxState { 27 | pub rect: AbsoluteRect, 28 | pub data: ui::Checkbox, 29 | pub interaction: InteractionState, 30 | } 31 | 32 | /// Visually relevant states of a [`TextArea`](../ui/struct.TextArea.html). 33 | #[derive(Debug, Clone, PartialEq)] 34 | pub struct TextAreaState { 35 | pub rect: AbsoluteRect, 36 | pub data: ui::TextArea, 37 | pub interaction: InteractionState, 38 | } 39 | 40 | /// Text which can either be display normally or as placeholder. 41 | #[derive(Debug, Clone, PartialEq, Eq)] 42 | pub enum InputText { 43 | Normal(String), 44 | Placeholder(String), 45 | } 46 | 47 | pub struct ScrollBarState { 48 | pub rect: AbsoluteRect, 49 | pub data: ui::ScrollBar, 50 | pub scroll_bar: AbsoluteRect, 51 | pub interaction: InteractionState, 52 | } 53 | -------------------------------------------------------------------------------- /thunderclap/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use {reclutch::error, thiserror::Error}; 3 | 4 | #[cfg(feature = "app")] 5 | #[derive(Error, Debug)] 6 | pub enum AppError { 7 | #[error("{0}")] 8 | SkiaError(#[from] error::SkiaError), 9 | #[error("{0}")] 10 | ResourceError(#[from] error::ResourceError), 11 | } 12 | 13 | #[cfg(feature = "default-themes")] 14 | #[derive(Error, Debug)] 15 | pub enum ThemeError { 16 | #[error("{0}")] 17 | ResourceError(#[from] error::ResourceError), 18 | #[error("{0}")] 19 | FontError(#[from] error::FontError), 20 | } 21 | -------------------------------------------------------------------------------- /thunderclap/src/geom.rs: -------------------------------------------------------------------------------- 1 | //! Widget positioning module. 2 | 3 | use crate::base; 4 | 5 | /// Unit of absolute widget space. 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub struct AbsoluteUnit; 8 | 9 | /// Point relative to the window instead of parent. 10 | pub type AbsolutePoint = reclutch::euclid::Point2D; 11 | /// Rectangle relative to the window instead of parent. 12 | pub type AbsoluteRect = reclutch::euclid::Rect; 13 | 14 | /// Unit of relative widget space. 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 16 | pub struct RelativeUnit; 17 | 18 | /// Point relative to the parent. 19 | pub type RelativePoint = reclutch::euclid::Point2D; 20 | /// Rectangle relative to the parent. 21 | pub type RelativeRect = reclutch::euclid::Rect; 22 | 23 | /// Getter/setter for widgets which store their parent's position. 24 | pub trait StoresParentPosition { 25 | fn set_parent_position(&mut self, parent_pos: AbsolutePoint); 26 | fn parent_position(&self) -> AbsolutePoint; 27 | } 28 | 29 | /// A point that can be either relative or absolute. 30 | #[derive(Debug, Clone, Copy, PartialEq)] 31 | pub enum AgnosticPoint { 32 | Relative(RelativePoint), 33 | Absolute(AbsolutePoint), 34 | } 35 | 36 | impl From for AgnosticPoint { 37 | fn from(pt: RelativePoint) -> Self { 38 | AgnosticPoint::Relative(pt) 39 | } 40 | } 41 | 42 | impl From for AgnosticPoint { 43 | fn from(pt: AbsolutePoint) -> Self { 44 | AgnosticPoint::Absolute(pt) 45 | } 46 | } 47 | 48 | /// A rectangle that can be either relative or absolute. 49 | #[derive(Debug, Clone, Copy, PartialEq)] 50 | pub enum AgnosticRect { 51 | Relative(RelativeRect), 52 | Absolute(AbsoluteRect), 53 | } 54 | 55 | impl From for AgnosticRect { 56 | fn from(rect: RelativeRect) -> Self { 57 | AgnosticRect::Relative(rect) 58 | } 59 | } 60 | 61 | impl From for AgnosticRect { 62 | fn from(rect: AbsoluteRect) -> Self { 63 | AgnosticRect::Absolute(rect) 64 | } 65 | } 66 | 67 | /// Getters/setters for context-aware 2D translation. 68 | pub trait ContextuallyMovable: base::Movable + StoresParentPosition { 69 | /// Changes the position to an agnostic point (i.e. accepts relative or absolute points). 70 | fn set_ctxt_position(&mut self, position: AgnosticPoint); 71 | 72 | /// Returns the position relative to the window. 73 | #[inline] 74 | fn abs_position(&self) -> AbsolutePoint { 75 | self.position().cast_unit() + self.parent_position().to_vector() 76 | } 77 | 78 | /// Returns the bounds with the position relative to the window. 79 | #[inline] 80 | fn abs_bounds(&self) -> AbsoluteRect { 81 | self.bounds().cast_unit().translate(self.parent_position().to_vector()) 82 | } 83 | 84 | /// Converts a point relative to this widget to an absolute point (relative to the window). 85 | #[inline] 86 | fn abs_convert_pt(&self, pt: RelativePoint) -> AbsolutePoint { 87 | pt.cast_unit() + self.parent_position().to_vector() 88 | } 89 | 90 | /// Converts an absolute point (relative to the window) to a point relative to this widget. 91 | #[inline] 92 | fn rel_convert_pt(&self, pt: AbsolutePoint) -> RelativePoint { 93 | pt.cast_unit() - self.parent_position().to_vector().cast_unit() 94 | } 95 | } 96 | 97 | impl ContextuallyMovable for W { 98 | #[inline] 99 | fn set_ctxt_position(&mut self, position: AgnosticPoint) { 100 | self.set_position(match position { 101 | AgnosticPoint::Relative(rel_pt) => rel_pt, 102 | AgnosticPoint::Absolute(abs_pt) => { 103 | abs_pt.cast_unit() - self.parent_position().to_vector().cast_unit() 104 | } 105 | }); 106 | update_parent_positions(self); 107 | } 108 | } 109 | 110 | /// Getters/setters for context-aware 2D rectangle translation. 111 | pub trait ContextuallyRectangular: ContextuallyMovable + base::Rectangular { 112 | /// Changes the rectangle to an agnostic rectangle (i.e. accepts relative and absolute coordinates). 113 | fn set_ctxt_rect(&mut self, rect: impl Into); 114 | 115 | /// Returns the relative rectangle in absolute coordinates (i.e. relative to the window). 116 | #[inline] 117 | fn abs_rect(&self) -> AbsoluteRect { 118 | self.rect().cast_unit().translate(self.parent_position().to_vector()) 119 | } 120 | 121 | /// Converts a rectangle relative to this widget into absolute coordinates (i.e. relative to the window). 122 | #[inline] 123 | fn abs_convert_rect(&self, mut rect: RelativeRect) -> AbsoluteRect { 124 | rect.origin = self.abs_convert_pt(rect.origin).cast_unit(); 125 | rect.cast_unit() 126 | } 127 | 128 | /// Converts an absolute rectangle (i.e. relative to the window) into relative coordinates. 129 | #[inline] 130 | fn rel_convert_rect(&self, mut rect: AbsoluteRect) -> RelativeRect { 131 | rect.origin = self.rel_convert_pt(rect.origin).cast_unit(); 132 | rect.cast_unit() 133 | } 134 | } 135 | 136 | impl ContextuallyRectangular for W { 137 | fn set_ctxt_rect(&mut self, rect: impl Into) { 138 | self.set_rect(match rect.into() { 139 | AgnosticRect::Relative(rel_rect) => rel_rect, 140 | AgnosticRect::Absolute(abs_rect) => { 141 | abs_rect.translate(-self.parent_position().to_vector()).cast_unit() 142 | } 143 | }); 144 | update_parent_positions(self); 145 | } 146 | } 147 | 148 | fn update_parent_positions( 149 | root: &mut dyn base::WidgetChildren, 150 | ) { 151 | let pos = root.abs_position(); 152 | for child in root.children_mut() { 153 | child.set_parent_position(pos); 154 | update_parent_positions(child); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /thunderclap/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Thunderclap aims to be a large widget toolkit for Reclutch. 2 | //! Beyond this, it also defines a framework to create widgets from. 3 | 4 | #[macro_use] 5 | pub extern crate reclutch; 6 | 7 | #[allow(unused_imports)] 8 | #[macro_use] 9 | extern crate thunderclap_macros; 10 | 11 | pub use thunderclap_macros::{ 12 | rooftop, widget, DropNotifier, HasVisibility, LayableWidget, Movable, Repaintable, Resizable, 13 | }; 14 | 15 | pub use paste; 16 | 17 | #[macro_use] 18 | pub mod base; 19 | pub mod draw; 20 | pub mod error; 21 | pub mod geom; 22 | #[cfg(feature = "core-widgets")] 23 | pub mod ui; 24 | 25 | #[cfg(feature = "app")] 26 | pub mod app; 27 | #[cfg(feature = "default-themes")] 28 | pub mod themes; 29 | 30 | pub mod prelude { 31 | pub use crate::{ 32 | base::{Layout, Movable, Rectangular, Repaintable, Resizable, WidgetChildren}, 33 | geom::{ContextuallyMovable, ContextuallyRectangular}, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /thunderclap/src/themes/assets/Inter-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzfool/thunderclap/7e11b25db19b58907e82223dd76b47b3ffa7f694/thunderclap/src/themes/assets/Inter-Italic.ttf -------------------------------------------------------------------------------- /thunderclap/src/themes/assets/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzfool/thunderclap/7e11b25db19b58907e82223dd76b47b3ffa7f694/thunderclap/src/themes/assets/Inter-Regular.ttf -------------------------------------------------------------------------------- /thunderclap/src/themes/assets/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzfool/thunderclap/7e11b25db19b58907e82223dd76b47b3ffa7f694/thunderclap/src/themes/assets/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /thunderclap/src/themes/assets/Inter-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzfool/thunderclap/7e11b25db19b58907e82223dd76b47b3ffa7f694/thunderclap/src/themes/assets/Inter-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /thunderclap/src/themes/dynamic.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /thunderclap/src/themes/mod.rs: -------------------------------------------------------------------------------- 1 | //! A collection of various themes to quickly get up and running with Thunderclap. 2 | 3 | use crate::draw::ThemeData; 4 | 5 | mod dynamic; 6 | mod primer; 7 | 8 | /// GitHub's "Primer" theme, based off the CSS widgets. 9 | pub struct Primer { 10 | data: ThemeData, 11 | } 12 | 13 | /// Theme generated from a RON (Rusty Object Notation) file. 14 | pub struct Dynamic { 15 | //data: ThemeData, 16 | } 17 | -------------------------------------------------------------------------------- /thunderclap/src/themes/primer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::Primer, 3 | crate::{ 4 | base, 5 | draw::{self, state}, 6 | error, 7 | geom::*, 8 | }, 9 | reclutch::display::{ 10 | self, Color, DisplayCommand, DisplayListBuilder, Filter, FontInfo, Gradient, 11 | GraphicsDisplay, GraphicsDisplayPaint, GraphicsDisplayStroke, Rect, ResourceData, 12 | ResourceDescriptor, ResourceReference, SharedData, Size, StyleColor, TextDisplayItem, 13 | Vector, VectorPath, VectorPathBuilder, 14 | }, 15 | }; 16 | 17 | fn check_mark_icon(rect: Rect) -> VectorPath { 18 | let mut builder = VectorPathBuilder::new(); 19 | 20 | // start at top-right 21 | builder.move_to(rect.origin + Size::new(rect.size.width, 0.0)); 22 | // line to bottom-middle (but a bit to the left) 23 | builder.line_to(rect.origin + Size::new((rect.size.width / 2.0) - 2.0, rect.size.height)); 24 | // line to left-middle 25 | builder.line_to(rect.origin + Size::new(0.0, rect.size.height / 2.0)); 26 | 27 | builder.build() 28 | } 29 | 30 | impl Primer { 31 | /// Creates an instance of the GitHub Primer theme. 32 | pub fn new(display: &mut dyn GraphicsDisplay) -> Result { 33 | let typeface = { 34 | let fonts = &[ 35 | std::sync::Arc::new(include_bytes!("assets/Inter-Regular.ttf").to_vec()), 36 | std::sync::Arc::new(include_bytes!("assets/Inter-Italic.ttf").to_vec()), 37 | std::sync::Arc::new(include_bytes!("assets/Inter-SemiBold.ttf").to_vec()), 38 | std::sync::Arc::new(include_bytes!("assets/Inter-SemiBoldItalic.ttf").to_vec()), 39 | ]; 40 | 41 | let fonts: Vec<(ResourceReference, FontInfo)> = fonts 42 | .iter() 43 | .map(|font| -> Result<(ResourceReference, FontInfo), error::ThemeError> { 44 | let font_info = FontInfo::from_data(font.clone(), 0)?; 45 | let font_resource = display.new_resource(ResourceDescriptor::Font( 46 | ResourceData::Data(SharedData::RefCount(font.clone())), 47 | ))?; 48 | 49 | Ok((font_resource, font_info)) 50 | }) 51 | .collect::, _>>()?; 52 | 53 | draw::Typeface { 54 | regular: fonts[0].clone(), 55 | italic: fonts[1].clone(), 56 | bold: fonts[2].clone(), 57 | bold_italic: fonts[3].clone(), 58 | } 59 | }; 60 | 61 | Ok(Primer { 62 | data: draw::ThemeData { 63 | scheme: draw::ColorScheme { 64 | background: base::color_from_urgba(255, 255, 255, 1.0), 65 | error: base::color_from_urgba(211, 50, 63, 1.0), 66 | focus: base::color_from_urgba(3, 102, 214, 0.3), 67 | primary: base::color_from_urgba(46, 186, 78, 1.0), 68 | control_outset: base::color_from_urgba(244, 247, 249, 1.0), 69 | control_inset: base::color_from_urgba(255, 255, 255, 1.0), 70 | over_error: base::color_from_urgba(255, 255, 255, 1.0), 71 | over_focus: base::color_from_urgba(255, 255, 255, 1.0), 72 | over_primary: base::color_from_urgba(255, 255, 255, 1.0), 73 | over_control_outset: base::color_from_urgba(36, 41, 46, 1.0), 74 | over_control_inset: base::color_from_urgba(36, 41, 46, 1.0), 75 | }, 76 | typography: draw::Typography { 77 | header: draw::TypefaceStyle { 78 | typeface: typeface.clone(), 79 | size: 32.0, 80 | style: draw::TextStyle::Bold, 81 | }, 82 | sub_header: draw::TypefaceStyle { 83 | typeface: typeface.clone(), 84 | size: 24.0, 85 | style: draw::TextStyle::Bold, 86 | }, 87 | body: draw::TypefaceStyle { 88 | typeface: typeface.clone(), 89 | size: 16.0, 90 | style: draw::TextStyle::Regular, 91 | }, 92 | button: draw::TypefaceStyle { 93 | typeface, 94 | size: 12.0, 95 | style: draw::TextStyle::Bold, 96 | }, 97 | }, 98 | contrast: draw::ThemeContrast::Light, 99 | }, 100 | }) 101 | } 102 | } 103 | 104 | impl draw::Theme for Primer { 105 | fn button(&self) -> Box> { 106 | Box::new(ButtonPainter) 107 | } 108 | 109 | fn checkbox(&self) -> Box> { 110 | Box::new(CheckboxPainter) 111 | } 112 | 113 | fn text_area(&self) -> Box> { 114 | Box::new(TextAreaPainter) 115 | } 116 | 117 | fn scroll_bar(&self) -> Box> { 118 | Box::new(ScrollBarPainter) 119 | } 120 | 121 | fn data(&self) -> &draw::ThemeData { 122 | &self.data 123 | } 124 | } 125 | 126 | struct ButtonPainter; 127 | 128 | impl ButtonPainter { 129 | fn make_text_item( 130 | &self, 131 | state: &state::ButtonState, 132 | color: StyleColor, 133 | centered: bool, 134 | ) -> TextDisplayItem { 135 | let typeface = state.data.typeface.typeface.pick(state.data.typeface.style); 136 | let mut text_item = TextDisplayItem { 137 | text: state.data.text.clone(), 138 | font: typeface.0, 139 | font_info: typeface.1, 140 | size: state.data.typeface.size, 141 | bottom_left: Default::default(), 142 | color, 143 | }; 144 | 145 | text_item.set_top_left(if centered { 146 | display::center(text_item.bounds().unwrap().size, state.rect.cast_unit()) 147 | } else { 148 | state.rect.origin.cast_unit() 149 | }); 150 | 151 | text_item 152 | } 153 | } 154 | 155 | impl draw::Painter for ButtonPainter { 156 | fn invoke(&self, theme: &dyn draw::Theme) -> Box> { 157 | theme.button() 158 | } 159 | 160 | fn size_hint(&self, state: state::ButtonState) -> Size { 161 | self.make_text_item(&state, Color::default().into(), false) 162 | .bounds() 163 | .unwrap() 164 | .inflate(10.0, 5.0) 165 | .size 166 | } 167 | 168 | fn paint_hint(&self, rect: RelativeRect) -> RelativeRect { 169 | // account for focus border 170 | rect.inflate(3.25, 3.25) 171 | } 172 | 173 | fn mouse_hint(&self, rect: RelativeRect) -> RelativeRect { 174 | rect 175 | } 176 | 177 | fn draw(&mut self, state: state::ButtonState) -> Vec { 178 | let (background, border, text, focus) = if state.data.disabled { 179 | ( 180 | draw::strengthen(state.data.background, 0.2, state.data.contrast).into(), 181 | draw::weaken(state.data.color, 0.4, state.data.contrast).into(), 182 | draw::weaken(state.data.color, 0.4, state.data.contrast).into(), 183 | state.data.focus.into(), 184 | ) 185 | } else if state.interaction.contains(state::InteractionState::PRESSED) { 186 | let background = draw::strengthen(state.data.background, 0.2, state.data.contrast); 187 | ( 188 | background.into(), 189 | draw::weaken(state.data.color, 0.3, state.data.contrast).into(), 190 | state.data.color.into(), 191 | state.data.focus.into(), 192 | ) 193 | } else if state.interaction.contains(state::InteractionState::HOVERED) { 194 | let background = draw::strengthen(state.data.background, 0.1, state.data.contrast); 195 | 196 | ( 197 | StyleColor::LinearGradient(Gradient { 198 | start: state.rect.origin.cast_unit(), 199 | end: state.rect.origin.cast_unit() + Size::new(0.0, state.rect.size.height), 200 | stops: vec![ 201 | (0.0, draw::lighten(background, 0.1)), 202 | (0.9, draw::darken(background, 0.1)), 203 | ], 204 | }), 205 | draw::weaken(state.data.color, 0.3, state.data.contrast).into(), 206 | state.data.color.into(), 207 | state.data.focus.into(), 208 | ) 209 | } else { 210 | ( 211 | StyleColor::LinearGradient(Gradient { 212 | start: state.rect.origin.cast_unit(), 213 | end: state.rect.origin.cast_unit() + Size::new(0.0, state.rect.size.height), 214 | stops: vec![ 215 | (0.0, draw::lighten(state.data.background, 0.1)), 216 | (0.9, draw::darken(state.data.background, 0.1)), 217 | ], 218 | }), 219 | draw::weaken(state.data.color, 0.4, state.data.contrast).into(), 220 | state.data.color.into(), 221 | state.data.focus.into(), 222 | ) 223 | }; 224 | 225 | let text_item = self.make_text_item(&state, text, true); 226 | 227 | let mut builder = DisplayListBuilder::new(); 228 | 229 | // Background 230 | builder.push_round_rectangle( 231 | base::sharp_align(state.rect.cast_unit()), 232 | [3.5; 4], 233 | GraphicsDisplayPaint::Fill(background), 234 | None, 235 | ); 236 | 237 | // Border 238 | builder.push_round_rectangle( 239 | base::sharp_align(state.rect.cast_unit()), 240 | [3.5; 4], 241 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 242 | thickness: 1.0 / 3.0, 243 | color: border, 244 | ..Default::default() 245 | }), 246 | None, 247 | ); 248 | 249 | // Text 250 | builder.push_text(text_item, None); 251 | 252 | // Focus rect 253 | if state.interaction.contains(state::InteractionState::FOCUSED) 254 | && !state.interaction.contains(state::InteractionState::PRESSED) 255 | { 256 | builder.push_round_rectangle( 257 | base::sharp_align(state.rect.cast_unit()).inflate(1.5, 1.5), 258 | [3.5; 4], 259 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 260 | thickness: 3.5, 261 | color: focus, 262 | ..Default::default() 263 | }), 264 | None, 265 | ); 266 | } 267 | 268 | // Pressed inset shadow 269 | if state.interaction.contains(state::InteractionState::PRESSED) { 270 | builder.push_round_rectangle_clip(base::sharp_align(state.rect.cast_unit()), [3.5; 4]); 271 | builder.push_round_rectangle( 272 | state.rect.cast_unit().inflate(10.0, 10.0).translate(Vector::new(0.0, 7.0)), 273 | [10.0; 4], 274 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 275 | thickness: 10.0, 276 | color: Color::new(0.0, 0.0, 0.0, 0.2).into(), 277 | ..Default::default() 278 | }), 279 | Some(Filter::Blur(3.0, 3.0)), 280 | ); 281 | } 282 | 283 | builder.build() 284 | } 285 | } 286 | 287 | struct CheckboxPainter; 288 | 289 | impl draw::Painter for CheckboxPainter { 290 | fn invoke(&self, theme: &dyn draw::Theme) -> Box> { 291 | theme.checkbox() 292 | } 293 | 294 | fn size_hint(&self, _state: state::CheckboxState) -> Size { 295 | Size::new(20.0, 20.0) 296 | } 297 | 298 | fn paint_hint(&self, rect: RelativeRect) -> RelativeRect { 299 | rect.inflate(3.25, 3.25) 300 | } 301 | 302 | fn mouse_hint(&self, rect: RelativeRect) -> RelativeRect { 303 | RelativeRect::new(rect.origin, Size::new(20.0, 20.0).cast_unit()) 304 | } 305 | 306 | fn draw(&mut self, mut state: state::CheckboxState) -> Vec { 307 | state.rect.size = Size::new(20.0, 20.0).cast_unit(); 308 | state.rect = base::sharp_align(state.rect.cast_unit()).cast_unit(); 309 | 310 | let (background, foreground, border, focus) = if state.data.checked { 311 | ( 312 | state.data.background, 313 | draw::weaken(state.data.foreground, 0.1, state.data.contrast).into(), 314 | draw::weaken(state.data.foreground, 0.4, state.data.contrast).into(), 315 | state.data.focus.into(), 316 | ) 317 | } else if state.interaction.contains(state::InteractionState::HOVERED) { 318 | ( 319 | draw::strengthen(state.data.background, 0.05, state.data.contrast), 320 | base::color_from_urgba(0, 0, 0, 0.0).into(), 321 | draw::weaken(state.data.foreground, 0.4, state.data.contrast).into(), 322 | state.data.focus.into(), 323 | ) 324 | } else { 325 | ( 326 | state.data.background, 327 | base::color_from_urgba(0, 0, 0, 0.0).into(), 328 | draw::weaken(state.data.foreground, 0.4, state.data.contrast).into(), 329 | state.data.focus.into(), 330 | ) 331 | }; 332 | 333 | let background = if state.interaction.contains(state::InteractionState::PRESSED) { 334 | draw::strengthen(background, 0.2, state.data.contrast) 335 | } else { 336 | background 337 | } 338 | .into(); 339 | 340 | let mut builder = DisplayListBuilder::new(); 341 | 342 | // Background 343 | builder.push_round_rectangle( 344 | state.rect.cast_unit(), 345 | [3.5; 4], 346 | GraphicsDisplayPaint::Fill(background), 347 | None, 348 | ); 349 | 350 | // Border 351 | builder.push_round_rectangle( 352 | state.rect.cast_unit(), 353 | [3.5; 4], 354 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 355 | thickness: 1.0 / 3.0, 356 | color: border, 357 | ..Default::default() 358 | }), 359 | None, 360 | ); 361 | 362 | // Foreground (check mark) 363 | builder.push_path( 364 | check_mark_icon(state.rect.cast_unit().inflate(-4.0, -4.0)), 365 | false, 366 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 367 | thickness: 2.5, 368 | color: foreground, 369 | ..Default::default() 370 | }), 371 | None, 372 | ); 373 | 374 | // Focus rect 375 | if state.interaction.contains(state::InteractionState::FOCUSED) 376 | && !state.interaction.contains(state::InteractionState::PRESSED) 377 | { 378 | builder.push_round_rectangle( 379 | state.rect.cast_unit().inflate(1.5, 1.5), 380 | [3.5; 4], 381 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 382 | thickness: 3.5, 383 | color: focus, 384 | ..Default::default() 385 | }), 386 | None, 387 | ); 388 | } 389 | 390 | builder.build() 391 | } 392 | } 393 | 394 | struct TextAreaPainter; 395 | 396 | impl TextAreaPainter { 397 | fn make_text_item(&self, state: &state::TextAreaState, color: StyleColor) -> TextDisplayItem { 398 | let typeface = state.data.typeface.typeface.pick(state.data.typeface.style); 399 | 400 | let mut text_item = TextDisplayItem { 401 | text: if state.data.text.is_empty() { 402 | state.data.placeholder.clone() 403 | } else { 404 | state.data.text.clone() 405 | } 406 | .into(), 407 | font: typeface.0, 408 | font_info: typeface.1, 409 | size: state.data.typeface.size, 410 | bottom_left: Default::default(), 411 | color, 412 | }; 413 | 414 | text_item.set_top_left(state.rect.origin.cast_unit()); 415 | 416 | text_item 417 | } 418 | } 419 | 420 | impl draw::Painter for TextAreaPainter { 421 | #[inline] 422 | fn invoke(&self, theme: &dyn draw::Theme) -> Box> { 423 | theme.text_area() 424 | } 425 | 426 | #[inline] 427 | fn size_hint(&self, state: state::TextAreaState) -> Size { 428 | self.make_text_item(&state, Color::default().into()).bounds().unwrap().size 429 | } 430 | 431 | #[inline] 432 | fn paint_hint(&self, rect: RelativeRect) -> RelativeRect { 433 | rect 434 | } 435 | 436 | #[inline] 437 | fn mouse_hint(&self, rect: RelativeRect) -> RelativeRect { 438 | rect 439 | } 440 | 441 | fn draw(&mut self, state: state::TextAreaState) -> Vec { 442 | let text = if state.data.text.is_empty() { 443 | state.data.placeholder_color 444 | } else { 445 | state.data.color 446 | } 447 | .into(); 448 | 449 | let text_item = self.make_text_item(&state, text); 450 | 451 | let cursor = if state.interaction.contains(state::InteractionState::FOCUSED) { 452 | let bounds = text_item.limited_bounds(state.data.cursor).unwrap(); 453 | Some((bounds.origin + Size::new(bounds.size.width, 0.0), bounds.origin + bounds.size)) 454 | } else { 455 | None 456 | }; 457 | 458 | let mut builder = DisplayListBuilder::new(); 459 | 460 | builder.push_rectangle_clip(state.rect.cast_unit(), true); 461 | 462 | if let Some((a, b)) = cursor { 463 | builder.push_line( 464 | a + Size::new(1.0, 0.0), 465 | b + Size::new(1.0, 0.0), 466 | GraphicsDisplayStroke { 467 | thickness: 1.0, 468 | color: state.data.cursor_color.into(), 469 | ..Default::default() 470 | }, 471 | None, 472 | ); 473 | } 474 | 475 | builder.push_text(text_item, None); 476 | 477 | builder.build() 478 | } 479 | } 480 | 481 | struct ScrollBarPainter; 482 | 483 | impl draw::Painter for ScrollBarPainter { 484 | fn invoke(&self, theme: &dyn draw::Theme) -> Box> { 485 | theme.scroll_bar() 486 | } 487 | 488 | fn size_hint(&self, state: state::ScrollBarState) -> Size { 489 | state.rect.size.cast_unit() 490 | } 491 | 492 | fn paint_hint(&self, rect: RelativeRect) -> RelativeRect { 493 | rect 494 | } 495 | 496 | fn mouse_hint(&self, rect: RelativeRect) -> RelativeRect { 497 | rect 498 | } 499 | 500 | fn draw(&mut self, mut state: state::ScrollBarState) -> Vec { 501 | state.rect = base::sharp_align(state.rect.cast_unit()).cast_unit(); 502 | state.scroll_bar = base::sharp_align(state.scroll_bar.cast_unit()).cast_unit(); 503 | 504 | let foreground = if state.interaction.contains(state::InteractionState::HOVERED) { 505 | draw::strengthen(state.data.foreground, 0.2, state.data.contrast) 506 | } else { 507 | state.data.foreground 508 | }; 509 | 510 | let border = draw::weaken(state.data.foreground, 0.4, state.data.contrast); 511 | 512 | let mut builder = DisplayListBuilder::new(); 513 | 514 | // Background blur 515 | builder.push_round_rectangle_backdrop( 516 | state.rect.cast_unit(), 517 | [3.5; 4], 518 | Filter::Blur(10.0, 10.0), 519 | ); 520 | 521 | // Scroll track (the background) 522 | builder.push_round_rectangle( 523 | state.rect.cast_unit(), 524 | [3.5; 4], 525 | GraphicsDisplayPaint::Fill(draw::with_opacity(state.data.background, 0.75).into()), 526 | None, 527 | ); 528 | 529 | // Border 530 | builder.push_round_rectangle( 531 | state.rect.cast_unit(), 532 | [3.5; 4], 533 | GraphicsDisplayPaint::Stroke(GraphicsDisplayStroke { 534 | thickness: 1.0 / 3.0, 535 | color: border.into(), 536 | ..Default::default() 537 | }), 538 | None, 539 | ); 540 | 541 | // Scroll bar 542 | builder.push_round_rectangle( 543 | state.rect.cast_unit(), 544 | [3.5; 4], 545 | GraphicsDisplayPaint::Fill(foreground.into()), 546 | None, 547 | ); 548 | 549 | builder.build() 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /thunderclap/src/ui/button.rs: -------------------------------------------------------------------------------- 1 | //! Button control widget. 2 | 3 | use { 4 | crate::{ 5 | base::{self, Repaintable}, 6 | draw::{self, state, HasTheme}, 7 | geom::*, 8 | ui, 9 | }, 10 | reclutch::{ 11 | display::{Color, DisplayCommand, DisplayText, GraphicsDisplay, Rect}, 12 | prelude::*, 13 | verbgraph as vg, 14 | widget::Widget, 15 | }, 16 | }; 17 | 18 | /// Events emitted by a button. 19 | #[derive(Event, Debug, Clone, Copy, PartialEq)] 20 | pub enum ButtonEvent { 21 | /// Emitted when the checkbox is pressed. 22 | #[event_key(press)] 23 | Press(AbsolutePoint), 24 | /// Emitted when the checkbox is released. 25 | #[event_key(release)] 26 | Release(AbsolutePoint), 27 | /// Emitted when the mouse enters the checkbox boundaries. 28 | #[event_key(begin_hover)] 29 | BeginHover(AbsolutePoint), 30 | /// Emitted when the mouse leaves the checkbox boundaries. 31 | #[event_key(end_hover)] 32 | EndHover(AbsolutePoint), 33 | /// Emitted when focus is gained. 34 | #[event_key(focus)] 35 | Focus, 36 | /// Emitted when focus is lost. 37 | #[event_key(blur)] 38 | Blur, 39 | } 40 | 41 | impl ui::InteractiveWidget for ButtonWidget 42 | where 43 | U: base::UpdateAuxiliary, 44 | G: base::GraphicalAuxiliary, 45 | { 46 | #[inline(always)] 47 | fn interaction(&mut self) -> &mut state::InteractionState { 48 | &mut self.interaction 49 | } 50 | 51 | #[inline] 52 | fn mouse_bounds(&self) -> RelativeRect { 53 | self.painter.mouse_hint(self.rect) 54 | } 55 | 56 | #[inline(always)] 57 | fn disabled(&self) -> bool { 58 | self.data.disabled 59 | } 60 | 61 | fn on_interaction_event(&mut self, event: ui::InteractionEvent) { 62 | self.repaint(); 63 | self.event_queue.emit_owned(match event { 64 | ui::InteractionEvent::Pressed(pos) => ButtonEvent::Press(pos), 65 | ui::InteractionEvent::Released(pos) => ButtonEvent::Release(pos), 66 | ui::InteractionEvent::BeginHover(pos) => ButtonEvent::BeginHover(pos), 67 | ui::InteractionEvent::EndHover(pos) => ButtonEvent::EndHover(pos), 68 | ui::InteractionEvent::Focus => ButtonEvent::Focus, 69 | ui::InteractionEvent::Blur => ButtonEvent::Blur, 70 | }); 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone, PartialEq)] 75 | pub struct Button { 76 | pub text: DisplayText, 77 | pub typeface: draw::TypefaceStyle, 78 | pub color: Color, 79 | pub background: Color, 80 | pub focus: Color, 81 | pub contrast: draw::ThemeContrast, 82 | pub disabled: bool, 83 | } 84 | 85 | impl ui::WidgetDataTarget for Button 86 | where 87 | U: base::UpdateAuxiliary, 88 | G: base::GraphicalAuxiliary, 89 | { 90 | type Target = ButtonWidget; 91 | } 92 | 93 | impl ui::WidgetConstructor for Button 94 | where 95 | U: base::UpdateAuxiliary, 96 | G: base::GraphicalAuxiliary, 97 | { 98 | fn from_theme(theme: &dyn draw::Theme) -> Self { 99 | let data = theme.data(); 100 | Button { 101 | text: "".to_string().into(), 102 | typeface: data.typography.button.clone(), 103 | color: data.scheme.over_control_outset, 104 | background: data.scheme.control_outset, 105 | focus: data.scheme.focus, 106 | contrast: data.contrast, 107 | disabled: false, 108 | } 109 | } 110 | 111 | fn construct(self, theme: &dyn draw::Theme, u_aux: &mut U) -> ButtonWidget 112 | where 113 | U: base::UpdateAuxiliary, 114 | G: base::GraphicalAuxiliary, 115 | { 116 | let data = base::Observed::new(self); 117 | 118 | let mut graph = vg::verbgraph! { 119 | ButtonWidget as obj, 120 | U as _aux, 121 | "bind" => _ev in &data.on_change => { 122 | change => { 123 | obj.resize_from_theme(); 124 | obj.command_group.repaint(); 125 | } 126 | } 127 | }; 128 | 129 | graph = graph.add( 130 | "interaction", 131 | ui::basic_interaction_handler::, U>().bind(u_aux.window_queue()), 132 | ); 133 | 134 | let painter = theme.button(); 135 | let rect = RelativeRect::new( 136 | Default::default(), 137 | painter 138 | .size_hint(state::ButtonState { 139 | rect: Default::default(), 140 | data: data.clone(), 141 | interaction: state::InteractionState::empty(), 142 | }) 143 | .cast_unit(), 144 | ); 145 | 146 | ButtonWidgetBuilder { 147 | rect, 148 | graph: graph.into(), 149 | 150 | data, 151 | painter, 152 | 153 | interaction: state::InteractionState::empty(), 154 | } 155 | .build() 156 | } 157 | } 158 | 159 | impl ui::core::CoreWidget for ButtonWidget 160 | where 161 | U: base::UpdateAuxiliary, 162 | G: base::GraphicalAuxiliary, 163 | { 164 | fn derive_state(&self) -> state::ButtonState { 165 | state::ButtonState { 166 | rect: self.abs_rect(), 167 | data: self.data.clone(), 168 | interaction: self.interaction, 169 | } 170 | } 171 | 172 | fn on_transform(&mut self) { 173 | self.repaint(); 174 | self.layout.notify(self.abs_rect()); 175 | } 176 | } 177 | 178 | use crate as thunderclap; 179 | crate::widget! { 180 | pub struct ButtonWidget { 181 | widget::MAX, 182 | 183 | EventQueue, 184 |