├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── calc-example.png ├── examples ├── Cargo.toml ├── basic.rs ├── calculator.rs └── flex_text.rs ├── imagine ├── Cargo.toml ├── resources │ └── FreeSans.ttf └── src │ ├── interactive.rs │ ├── layout.rs │ ├── lib.rs │ ├── render │ ├── display_list.rs │ └── mod.rs │ ├── systems │ ├── interaction_system.rs │ ├── layout_system.rs │ ├── mod.rs │ └── render_system.rs │ ├── text.rs │ └── widget.rs ├── imagine_toolkit ├── Cargo.toml └── src │ ├── button.rs │ ├── center.rs │ ├── fill_box.rs │ ├── flex.rs │ ├── label.rs │ ├── lib.rs │ ├── list.rs │ └── padding.rs └── rustfmt.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # VSCode config 13 | .vscode 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples", 4 | "imagine" 5 | ] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Connor Brewster 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Imagine 2 | 3 | An experimental Rust GUI framework. 4 | 5 | > **WARNING** 6 | > 7 | > This is a work in progress and is not meant to be used in production. 8 | 9 | The current implementation is quite messy, but it is meant as a proof of concept and I plan to rewrite the majority of the code if this approach seems viable. 10 | 11 | ### Basic Design 12 | 13 | Imagine uses an ECS library (specs) which maintains all the widgets in the world. 14 | Widgets are composed of various components which is used to determine layout, rendering, and interactivity. 15 | Currently the bulk of the layout and render work is done via trait objects and a single Widget component which stores the Widget trit object. 16 | This benefit of this approach is that it allows anyone to build their own custom widgets in a separate crate. 17 | To demonstrate this, all the available widgets are in the `imagine_toolkit` crate rather than the `imagine` crate. 18 | Widgets are identifiable by a `WidgetId` which is a wrapper over a specs `Entity`. A `WidgetId` is only constructed when an entity with a widget component has been added to the `World`. 19 | Widgets can store these `WidgetId`s to keep track of their children. 20 | End users use `WidgetId`s to build their UI hierarchy and to attach event listeners to widgets. 21 | 22 | ### Current Features 23 | 24 | - Widgets 25 | - Flex Box 26 | - Padding 27 | - Label 28 | - Button 29 | - Colored Box 30 | - Basic Interactivity 31 | - Hover 32 | - Click 33 | 34 | ### Example 35 | 36 | **Calculator** 37 | 38 | Calculator Example 39 | 40 | ### Inspiration 41 | 42 | The layout is done via Flutter's box constraints and I have taken a lot of inspiration from 43 | [druid](https://github.com/xi-editor/druid). 44 | -------------------------------------------------------------------------------- /calc-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrewster/imagine/8903600b5c6377f2cf6baada9eb6c5654d6c823d/calc-example.png -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imagine-examples" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Connor Brewster "] 6 | edition = "2018" 7 | 8 | [[bin]] 9 | name = "basic" 10 | path = "basic.rs" 11 | 12 | [[bin]] 13 | name = "calculator" 14 | path = "calculator.rs" 15 | 16 | [[bin]] 17 | name = "flex_text" 18 | path = "flex_text.rs" 19 | 20 | [dependencies] 21 | imagine = {path = "../imagine"} 22 | imagine_toolkit = {path = "../imagine_toolkit"} 23 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use imagine::{Application, ClickListener, Imagine, Size, WidgetContext, WidgetId}; 2 | use imagine_toolkit::{ 3 | Button, FillBox, Flex, FlexAlign, FlexDirection, FlexEvent, FlexItem, Label, Padding, 4 | }; 5 | 6 | enum BasicMessage { 7 | Add, 8 | Remove, 9 | } 10 | 11 | struct Basic { 12 | counter: usize, 13 | flex: Option, 14 | } 15 | 16 | impl Application for Basic { 17 | type Message = BasicMessage; 18 | 19 | fn build(&mut self, context: &mut WidgetContext) -> WidgetId { 20 | let rows = (0..2) 21 | .map(|_| FlexItem::Flex(flex_row(context), 1)) 22 | .collect(); 23 | let flex = 24 | context.create_widget(Flex::new(rows, FlexDirection::Vertical, FlexAlign::Middle)); 25 | self.flex = Some(flex); 26 | 27 | let add_button = Button::new(context, (0.0, 1.0, 0.0, 1.0), "Add"); 28 | let add_button = context.create_widget(add_button); 29 | 30 | let remove_button = Button::new(context, (1.0, 0.0, 0.0, 1.0), "Remove"); 31 | let remove_button = context.create_widget(remove_button); 32 | 33 | context.add_click_listener(add_button, ClickListener::new(|| BasicMessage::Add)); 34 | 35 | context.add_click_listener(remove_button, ClickListener::new(|| BasicMessage::Remove)); 36 | 37 | let buttons = context.create_widget(Flex::new( 38 | vec![ 39 | FlexItem::Flex(add_button, 1), 40 | FlexItem::Flex(remove_button, 1), 41 | ], 42 | FlexDirection::Horizontal, 43 | FlexAlign::Middle, 44 | )); 45 | 46 | context.create_widget(Flex::new( 47 | vec![FlexItem::Flex(flex, 9), FlexItem::Flex(buttons, 1)], 48 | FlexDirection::Vertical, 49 | FlexAlign::Middle, 50 | )) 51 | } 52 | 53 | fn handle_message( 54 | &mut self, 55 | message: Self::Message, 56 | context: &mut WidgetContext, 57 | ) { 58 | if let Some(flex) = self.flex { 59 | match message { 60 | BasicMessage::Add => { 61 | self.counter += 1; 62 | let new_row = 63 | context.create_widget(Label::new(format!("New Item {}", self.counter))); 64 | let padded = context.create_widget(Padding::new(2.0, 2.0, 2.0, 2.0, new_row)); 65 | context.send_message(flex, FlexEvent::AddChild(FlexItem::Flex(padded, 1))) 66 | } 67 | BasicMessage::Remove => { 68 | context.send_message(flex, FlexEvent::RemoveChild); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | fn main() { 76 | let mut imagine = Imagine::new(Basic { 77 | counter: 0, 78 | flex: None, 79 | }); 80 | 81 | imagine.create_window("Basic Demo!", Size::new(1024.0, 768.0)); 82 | imagine.run(); 83 | } 84 | 85 | fn flex_row(context: &mut WidgetContext) -> WidgetId { 86 | let children = (0..5) 87 | .map(|_| { 88 | let block = 89 | context.create_widget(FillBox::new(Size::new(20.0, 20.0), (1.0, 0.0, 0.0, 1.0))); 90 | FlexItem::Flex( 91 | context.create_widget(Padding::new(2.0, 2.0, 2.0, 2.0, block)), 92 | 1, 93 | ) 94 | }) 95 | .collect(); 96 | 97 | context.create_widget(Flex::new( 98 | children, 99 | FlexDirection::Horizontal, 100 | FlexAlign::Middle, 101 | )) 102 | } 103 | -------------------------------------------------------------------------------- /examples/calculator.rs: -------------------------------------------------------------------------------- 1 | use imagine::{Application, ClickListener, Imagine, Size, WidgetContext, WidgetId}; 2 | use imagine_toolkit::{ 3 | Button, Flex, FlexAlign, FlexDirection, FlexItem, Label, LabelMessage, Padding, 4 | }; 5 | 6 | #[derive(Clone, Debug)] 7 | enum CalcMessage { 8 | Num(f32), 9 | Op(Op), 10 | Decimal, 11 | Negative, 12 | Eq, 13 | Clear, 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | enum Op { 18 | Add, 19 | Sub, 20 | Mul, 21 | Div, 22 | Mod, 23 | } 24 | 25 | struct Calculator { 26 | current: f32, 27 | operator: Option<(f32, Op)>, 28 | decimal: Option, 29 | display: Option, 30 | } 31 | 32 | impl Calculator { 33 | fn new() -> Calculator { 34 | Calculator { 35 | current: 0.0, 36 | operator: None, 37 | decimal: None, 38 | display: None, 39 | } 40 | } 41 | } 42 | 43 | impl Application for Calculator { 44 | type Message = CalcMessage; 45 | 46 | fn build(&mut self, context: &mut WidgetContext) -> WidgetId { 47 | let result = context.create_widget(Label::new("0")); 48 | 49 | let display = context.create_widget(Padding::new(5.0, 5.0, 5.0, 5.0, result)); 50 | self.display = Some(result); 51 | 52 | let rows = vec![ 53 | FlexItem::NonFlex(display), 54 | FlexItem::Flex( 55 | calc_row( 56 | context, 57 | vec![ 58 | ("C", 1, CalcMessage::Clear), 59 | ("+/-", 1, CalcMessage::Negative), 60 | ("%", 1, CalcMessage::Op(Op::Mod)), 61 | ("/", 1, CalcMessage::Op(Op::Div)), 62 | ], 63 | ), 64 | 1, 65 | ), 66 | FlexItem::Flex( 67 | calc_row( 68 | context, 69 | vec![ 70 | ("7", 1, CalcMessage::Num(7.0)), 71 | ("8", 1, CalcMessage::Num(8.0)), 72 | ("9", 1, CalcMessage::Num(9.0)), 73 | ("*", 1, CalcMessage::Op(Op::Mul)), 74 | ], 75 | ), 76 | 1, 77 | ), 78 | FlexItem::Flex( 79 | calc_row( 80 | context, 81 | vec![ 82 | ("4", 1, CalcMessage::Num(4.0)), 83 | ("5", 1, CalcMessage::Num(5.0)), 84 | ("6", 1, CalcMessage::Num(6.0)), 85 | ("-", 1, CalcMessage::Op(Op::Sub)), 86 | ], 87 | ), 88 | 1, 89 | ), 90 | FlexItem::Flex( 91 | calc_row( 92 | context, 93 | vec![ 94 | ("1", 1, CalcMessage::Num(1.0)), 95 | ("2", 1, CalcMessage::Num(2.0)), 96 | ("3", 1, CalcMessage::Num(3.0)), 97 | ("+", 1, CalcMessage::Op(Op::Add)), 98 | ], 99 | ), 100 | 1, 101 | ), 102 | FlexItem::Flex( 103 | calc_row( 104 | context, 105 | vec![ 106 | ("0", 2, CalcMessage::Num(0.0)), 107 | (".", 1, CalcMessage::Decimal), 108 | ("=", 1, CalcMessage::Eq), 109 | ], 110 | ), 111 | 1, 112 | ), 113 | ]; 114 | 115 | context.create_widget(Flex::new( 116 | rows, 117 | FlexDirection::Vertical, 118 | FlexAlign::Baseline, 119 | )) 120 | } 121 | 122 | fn handle_message(&mut self, message: CalcMessage, context: &mut WidgetContext) { 123 | println!("Received Message: {:?}", message); 124 | match message { 125 | CalcMessage::Num(number) => { 126 | if let Some(decimal) = self.decimal { 127 | self.current += decimal * number; 128 | self.decimal = Some(decimal / 10.0); 129 | } else { 130 | self.current *= 10.0; 131 | self.current += number; 132 | } 133 | } 134 | CalcMessage::Eq => { 135 | if let Some((prev, op)) = self.operator.take() { 136 | self.current = match op { 137 | Op::Add => prev + self.current, 138 | Op::Sub => prev - self.current, 139 | Op::Mul => prev * self.current, 140 | Op::Div => prev / self.current, 141 | Op::Mod => prev % self.current, 142 | } 143 | } 144 | } 145 | CalcMessage::Negative => { 146 | self.current *= -1.0; 147 | } 148 | CalcMessage::Op(op) => match &self.operator { 149 | None => { 150 | self.operator = Some((self.current, op)); 151 | self.current = 0.0; 152 | self.decimal = None; 153 | } 154 | Some((prev, old_op)) => { 155 | self.current = match old_op { 156 | Op::Add => prev + self.current, 157 | Op::Sub => prev - self.current, 158 | Op::Mul => prev * self.current, 159 | Op::Div => prev / self.current, 160 | Op::Mod => prev % self.current, 161 | }; 162 | self.operator = Some((self.current, op)); 163 | self.current = 0.0; 164 | self.decimal = None; 165 | } 166 | }, 167 | CalcMessage::Clear => { 168 | self.current = 0.0; 169 | self.operator = None; 170 | self.decimal = None; 171 | } 172 | CalcMessage::Decimal => { 173 | if self.decimal.is_none() { 174 | self.decimal = Some(0.1); 175 | } 176 | } 177 | } 178 | 179 | let current = format!("{}", self.current); 180 | if let Some(display) = self.display { 181 | context.send_message(display, LabelMessage::SetText(current)); 182 | } 183 | } 184 | } 185 | 186 | fn main() { 187 | let mut imagine = Imagine::new(Calculator::new()); 188 | 189 | imagine.create_window("Calculator!", Size::new(280.0, 350.0)); 190 | imagine.run(); 191 | } 192 | 193 | fn calc_row( 194 | context: &mut WidgetContext, 195 | items: Vec<(&str, usize, CalcMessage)>, 196 | ) -> WidgetId { 197 | let children = items 198 | .into_iter() 199 | .map(|(item, flex, message)| { 200 | let button = Button::new(context, (0.957, 0.586, 0.16, 1.0), item); 201 | let button = context.create_widget(button); 202 | context.add_click_listener(button, ClickListener::new(move || message.clone())); 203 | FlexItem::Flex( 204 | context.create_widget(Padding::new(2.0, 2.0, 2.0, 2.0, button)), 205 | flex, 206 | ) 207 | }) 208 | .collect(); 209 | 210 | context.create_widget(Flex::new( 211 | children, 212 | FlexDirection::Horizontal, 213 | FlexAlign::Middle, 214 | )) 215 | } 216 | -------------------------------------------------------------------------------- /examples/flex_text.rs: -------------------------------------------------------------------------------- 1 | use imagine::{Application, Imagine, Size, WidgetContext, WidgetId}; 2 | use imagine_toolkit::{FillBox, Flex, FlexAlign, FlexDirection, FlexItem, Label}; 3 | 4 | struct FlexText; 5 | 6 | impl Application for FlexText { 7 | type Message = (); 8 | 9 | fn build(&mut self, context: &mut WidgetContext) -> WidgetId { 10 | let left = context.create_widget(Label::new("Left Text")); 11 | let right = context.create_widget(Label::new("Right Text")); 12 | let fill = context.create_widget(FillBox::new(Size::new(10.0, 10.0), (0.0, 0.0, 0.0, 0.0))); 13 | 14 | context.create_widget(Flex::new( 15 | vec![ 16 | FlexItem::NonFlex(left), 17 | FlexItem::Flex(fill, 1), 18 | FlexItem::NonFlex(right), 19 | ], 20 | FlexDirection::Horizontal, 21 | FlexAlign::Middle, 22 | )) 23 | } 24 | } 25 | 26 | fn main() { 27 | let mut imagine = Imagine::new(FlexText); 28 | imagine.create_window("Flex Text", Size::new(1024.0, 768.0)); 29 | imagine.run(); 30 | } 31 | -------------------------------------------------------------------------------- /imagine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imagine" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Connor Brewster "] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | app_units = "0.7.1" 10 | gleam = "0.10.0" 11 | glutin = "0.19.0" 12 | rusttype = "0.7.3" 13 | specs = "0.14.1" 14 | webrender = {git="https://github.com/servo/webrender/"} 15 | -------------------------------------------------------------------------------- /imagine/resources/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbrewster/imagine/8903600b5c6377f2cf6baada9eb6c5654d6c823d/imagine/resources/FreeSans.ttf -------------------------------------------------------------------------------- /imagine/src/interactive.rs: -------------------------------------------------------------------------------- 1 | use crate::{Widget, WidgetComponent, WidgetId}; 2 | use specs::{Component, DenseVecStorage, Entities, WriteStorage}; 3 | use std::any::Any; 4 | 5 | pub trait Message: Any + Send + Sync {} 6 | 7 | impl Message for T where T: Any + Send + Sync {} 8 | 9 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 10 | pub enum Interaction { 11 | Hovered(bool), 12 | MouseDown, 13 | MouseUp, 14 | } 15 | 16 | pub struct WidgetContext<'a, 'b, M: Message> { 17 | pub(crate) entities: &'a Entities<'b>, 18 | pub(crate) widgets: &'a mut WriteStorage<'b, WidgetComponent>, 19 | pub(crate) click_listeners: &'a mut WriteStorage<'b, ClickListener>, 20 | } 21 | 22 | impl<'a, 'b, M: Message> WidgetContext<'a, 'b, M> { 23 | pub(crate) fn new( 24 | entities: &'a Entities<'b>, 25 | widgets: &'a mut WriteStorage<'b, WidgetComponent>, 26 | click_listeners: &'a mut WriteStorage<'b, ClickListener>, 27 | ) -> WidgetContext<'a, 'b, M> { 28 | WidgetContext { 29 | entities, 30 | widgets, 31 | click_listeners, 32 | } 33 | } 34 | 35 | pub fn send_message(&mut self, widget_id: WidgetId, message: T) { 36 | let removed = if let Some(widget) = self.widgets.get_mut(widget_id.0) { 37 | widget.update(Box::new(message)) 38 | } else { 39 | None 40 | }; 41 | 42 | fn remove_widgets<'a, 'b>( 43 | entities: &'a Entities<'b>, 44 | widgets: &'a mut WriteStorage<'b, WidgetComponent>, 45 | ids: &'a [WidgetId], 46 | ) { 47 | for id in ids { 48 | let widget = widgets.get(id.0).unwrap(); 49 | remove_widgets(entities, widgets, &widget.children()); 50 | entities.delete(id.0).ok(); 51 | } 52 | } 53 | 54 | if let Some(removed) = removed { 55 | remove_widgets(self.entities, self.widgets, &removed); 56 | } 57 | } 58 | 59 | pub fn add_click_listener(&mut self, widget_id: WidgetId, listener: ClickListener) { 60 | self.click_listeners.insert(widget_id.0, listener).ok(); 61 | } 62 | 63 | pub fn remove_widget(&mut self, widget_id: WidgetId) { 64 | self.entities.delete(widget_id.0).ok(); 65 | } 66 | 67 | pub fn create_widget(&mut self, widget: W) -> WidgetId { 68 | WidgetId( 69 | self.entities 70 | .build_entity() 71 | .with( 72 | WidgetComponent { 73 | inner: Box::new(widget), 74 | }, 75 | self.widgets, 76 | ) 77 | .build(), 78 | ) 79 | } 80 | } 81 | 82 | pub struct ClickListener { 83 | pub(crate) on_click: Box M + Send + Sync + 'static>, 84 | } 85 | 86 | impl ClickListener { 87 | pub fn new(click: F) -> ClickListener 88 | where 89 | F: Fn() -> M + Send + Sync + 'static, 90 | { 91 | ClickListener { 92 | on_click: Box::new(click), 93 | } 94 | } 95 | } 96 | 97 | impl Component for ClickListener { 98 | type Storage = DenseVecStorage; 99 | } 100 | 101 | pub(crate) struct Event { 102 | pub(crate) event: Interaction, 103 | } 104 | 105 | impl Event { 106 | pub(crate) fn new(event: Interaction) -> Event { 107 | Event { event } 108 | } 109 | } 110 | 111 | impl Component for Event { 112 | type Storage = DenseVecStorage; 113 | } 114 | 115 | pub(crate) struct Interactive { 116 | pub(crate) tag: u64, 117 | } 118 | 119 | impl Interactive { 120 | pub(crate) fn new(tag: u64) -> Interactive { 121 | Interactive { tag } 122 | } 123 | } 124 | 125 | impl Component for Interactive { 126 | type Storage = DenseVecStorage; 127 | } 128 | -------------------------------------------------------------------------------- /imagine/src/layout.rs: -------------------------------------------------------------------------------- 1 | use crate::{text::FinalText, WidgetComponent, WidgetId}; 2 | use rusttype::Font; 3 | use specs::{Component, DenseVecStorage, ReadStorage, WriteStorage}; 4 | 5 | #[derive(Copy, Clone, PartialEq, Debug)] 6 | pub struct Geometry { 7 | pub position: Position, 8 | pub size: Size, 9 | } 10 | 11 | impl Geometry { 12 | pub fn new(position: Position, size: Size) -> Geometry { 13 | Geometry { position, size } 14 | } 15 | } 16 | 17 | #[derive(Copy, Clone, PartialEq, Debug)] 18 | pub struct Position { 19 | pub x: f32, 20 | pub y: f32, 21 | } 22 | 23 | impl Position { 24 | pub fn new(x: f32, y: f32) -> Position { 25 | Position { x, y } 26 | } 27 | 28 | pub fn zero() -> Position { 29 | Position { x: 0.0, y: 0.0 } 30 | } 31 | } 32 | 33 | #[derive(Copy, Clone, PartialEq, Debug)] 34 | pub struct Size { 35 | pub width: f32, 36 | pub height: f32, 37 | } 38 | 39 | impl Size { 40 | pub fn new(width: f32, height: f32) -> Size { 41 | Size { width, height } 42 | } 43 | 44 | pub fn zero() -> Size { 45 | Size { 46 | width: 0.0, 47 | height: 0.0, 48 | } 49 | } 50 | } 51 | 52 | #[derive(Copy, Clone, PartialEq, Debug)] 53 | pub struct BoxConstraint { 54 | pub min: Size, 55 | pub max: Size, 56 | } 57 | 58 | impl BoxConstraint { 59 | pub fn new(min: Size, max: Size) -> BoxConstraint { 60 | BoxConstraint { min, max } 61 | } 62 | 63 | pub fn tight(size: Size) -> BoxConstraint { 64 | BoxConstraint { 65 | min: size, 66 | max: size, 67 | } 68 | } 69 | 70 | pub fn constrain(&self, size: Size) -> Size { 71 | Size::new( 72 | clamp(size.width, self.min.width, self.max.width), 73 | clamp(size.height, self.min.height, self.max.height), 74 | ) 75 | } 76 | 77 | pub fn is_tight(&self) -> bool { 78 | (self.min.width - self.max.width).abs() < std::f32::EPSILON 79 | && (self.min.height - self.max.height).abs() < std::f32::EPSILON 80 | } 81 | } 82 | 83 | fn clamp(input: f32, min: f32, max: f32) -> f32 { 84 | if input < min { 85 | min 86 | } else if input > max { 87 | max 88 | } else { 89 | input 90 | } 91 | } 92 | 93 | impl Component for Position { 94 | type Storage = DenseVecStorage; 95 | } 96 | 97 | impl Component for Size { 98 | type Storage = DenseVecStorage; 99 | } 100 | 101 | pub struct LayoutContext<'a, 'b> { 102 | positions: &'a mut WriteStorage<'b, Position>, 103 | sizes: &'a mut WriteStorage<'b, Size>, 104 | text: &'a mut WriteStorage<'b, FinalText>, 105 | widgets: &'a ReadStorage<'b, WidgetComponent>, 106 | font: &'a Font<'static>, 107 | } 108 | 109 | impl<'a, 'b> LayoutContext<'a, 'b> { 110 | pub(crate) fn new( 111 | positions: &'a mut WriteStorage<'b, Position>, 112 | sizes: &'a mut WriteStorage<'b, Size>, 113 | text: &'a mut WriteStorage<'b, FinalText>, 114 | widgets: &'a ReadStorage<'b, WidgetComponent>, 115 | font: &'a Font<'static>, 116 | ) -> LayoutContext<'a, 'b> { 117 | LayoutContext { 118 | positions, 119 | sizes, 120 | text, 121 | widgets, 122 | font, 123 | } 124 | } 125 | 126 | pub fn set_position(&mut self, widget: WidgetId, position: Position) { 127 | self.positions.insert(widget.0, position).ok(); 128 | } 129 | 130 | pub fn get_size(&mut self, widget: WidgetId) -> Size { 131 | *self.sizes.get(widget.0).unwrap() 132 | } 133 | 134 | pub fn layout_text(&self, text: &str) -> FinalText { 135 | FinalText::new(self.font, text) 136 | } 137 | 138 | pub fn set_text(&mut self, widget: WidgetId, text: FinalText) { 139 | self.text.insert(widget.0, text).ok(); 140 | } 141 | 142 | pub fn layout_widget(&mut self, widget_id: WidgetId, box_constraint: BoxConstraint) -> Size { 143 | let widget = self 144 | .widgets 145 | .get(widget_id.0) 146 | .expect("Could not find widget during layout."); 147 | let size = widget.layout(widget_id, self, box_constraint); 148 | self.sizes.insert(widget_id.0, size).ok(); 149 | size 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /imagine/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod interactive; 2 | mod layout; 3 | mod render; 4 | mod systems; 5 | pub mod text; 6 | mod widget; 7 | 8 | use self::{ 9 | interactive::{Event, Interactive}, 10 | systems::{InteractionSystem, LayoutSystem, RenderSystem}, 11 | widget::WidgetComponent, 12 | }; 13 | use app_units::Au; 14 | use gleam::gl; 15 | use glutin::GlContext; 16 | use glutin::{EventsLoop, WindowBuilder}; 17 | use rusttype::Font; 18 | use specs::{ 19 | Builder, Component, DenseVecStorage, Dispatcher, DispatcherBuilder, Entity, Join, World, 20 | }; 21 | use std::collections::HashMap; 22 | use std::mem; 23 | use webrender::api::*; 24 | use webrender::api::units::*; 25 | 26 | pub use self::{ 27 | interactive::{ClickListener, Interaction, Message, WidgetContext}, 28 | layout::{BoxConstraint, Geometry, LayoutContext, Position, Size}, 29 | render::RenderContext, 30 | widget::{Widget, WidgetId}, 31 | }; 32 | 33 | const FONT_DATA: &[u8] = include_bytes!("../resources/FreeSans.ttf"); 34 | 35 | pub trait Application { 36 | type Message: Message; 37 | 38 | fn build(&mut self, context: &mut WidgetContext) -> WidgetId; 39 | 40 | fn handle_message( 41 | &mut self, 42 | _message: Self::Message, 43 | _context: &mut WidgetContext, 44 | ) { 45 | } 46 | } 47 | 48 | pub(crate) struct MessageQueue(Vec); 49 | 50 | impl Default for MessageQueue { 51 | fn default() -> MessageQueue { 52 | MessageQueue(Vec::new()) 53 | } 54 | } 55 | 56 | pub struct Imagine<'a, 'b, A: Application> { 57 | world: World, 58 | dispatcher: Dispatcher<'a, 'b>, 59 | events_loop: EventsLoop, 60 | windows: HashMap, 61 | renderers: Vec, 62 | application: A, 63 | } 64 | 65 | impl<'a, 'b, A: Application> Imagine<'a, 'b, A> { 66 | pub fn new(application: A) -> Imagine<'a, 'b, A> { 67 | let mut world = World::new(); 68 | world.add_resource(MessageQueue::(Vec::new())); 69 | let mut dispatcher = DispatcherBuilder::new() 70 | .with( 71 | InteractionSystem::::default(), 72 | "interaction", 73 | &[], 74 | ) 75 | .with(LayoutSystem, "layout", &["interaction"]) 76 | .with(RenderSystem, "render", &["interaction", "layout"]) 77 | .build(); 78 | 79 | dispatcher.setup(&mut world.res); 80 | 81 | let events_loop = EventsLoop::new(); 82 | 83 | Imagine { 84 | world, 85 | dispatcher, 86 | events_loop, 87 | windows: HashMap::new(), 88 | renderers: Vec::new(), 89 | application, 90 | } 91 | } 92 | 93 | pub fn create_window(&mut self, title: &str, size: Size) { 94 | // TODO: Generate Unique PipelineIds per Window. 95 | let pipeline_id = PipelineId(0, 0); 96 | let window_entity = self.world.create_entity().build(); 97 | let render_window = 98 | RenderWindow::new(title, &self.events_loop, window_entity, size, pipeline_id).unwrap(); 99 | let mut windows = self.world.write_storage::(); 100 | let entities = self.world.entities(); 101 | let mut widgets = self.world.write_storage::(); 102 | let mut click_listeners = self.world.write_storage::>(); 103 | let mut context = WidgetContext::new(&entities, &mut widgets, &mut click_listeners); 104 | let root = self.application.build(&mut context); 105 | windows 106 | .insert( 107 | window_entity, 108 | WindowComponent { 109 | root, 110 | layout_size: LayoutSize::zero(), 111 | dirty: true, 112 | pipeline_id, 113 | display_list_builder: None, 114 | hovered: None, 115 | clicked: None, 116 | font_instance_key: render_window.font_instance_key, 117 | font: Font::from_bytes(FONT_DATA).unwrap(), 118 | }, 119 | ) 120 | .ok(); 121 | self.windows 122 | .insert(render_window.window.id(), render_window); 123 | } 124 | 125 | pub fn run(self) { 126 | let Imagine { 127 | mut events_loop, 128 | mut dispatcher, 129 | mut windows, 130 | mut world, 131 | mut renderers, 132 | mut application, 133 | } = self; 134 | 135 | events_loop.run_forever(|event| { 136 | if let glutin::Event::WindowEvent { event, window_id } = event { 137 | let mut response = EventResponse::Continue; 138 | if let Some(window) = windows.get_mut(&window_id) { 139 | response = window.handle_event::(event, &world); 140 | } 141 | match response { 142 | EventResponse::Quit => { 143 | if let Some(window) = windows.remove(&window_id) { 144 | renderers.push(window.renderer); 145 | } 146 | } 147 | EventResponse::Dirty => { 148 | if let Some(window) = windows.get(&window_id) { 149 | let mut window_components = world.write_storage::(); 150 | 151 | let window_component = window_components 152 | .get_mut(window.entity) 153 | .expect("Could not find window component"); 154 | window_component.set_dirty(true); 155 | } 156 | } 157 | EventResponse::Continue => {} 158 | } 159 | } 160 | 161 | for window in windows.values() { 162 | let mut window_components = world.write_storage::(); 163 | let window_component = window_components 164 | .get_mut(window.entity) 165 | .expect("Could not find window component"); 166 | 167 | if !window_component.dirty() { 168 | continue; 169 | } 170 | 171 | let hidpi_factor = window.window.get_hidpi_factor(); 172 | 173 | let framebuffer_size = { 174 | let size = window 175 | .window 176 | .get_inner_size() 177 | .unwrap() 178 | .to_physical(hidpi_factor); 179 | DeviceIntSize::new(size.width as i32, size.height as i32) 180 | }; 181 | 182 | let layout_size: LayoutSize = 183 | framebuffer_size.to_f32() / euclid::Scale::new(hidpi_factor as f32); 184 | 185 | window_component.layout_size = layout_size; 186 | } 187 | 188 | dispatcher.dispatch(&world.res); 189 | world.maintain(); 190 | 191 | let entities = world.entities(); 192 | let mut widgets = world.write_storage::(); 193 | let mut click_listeners = world.write_storage::>(); 194 | let mut context = WidgetContext::new(&entities, &mut widgets, &mut click_listeners); 195 | 196 | let mut message_queue = world.write_resource::>(); 197 | let messages = mem::replace(&mut *message_queue, MessageQueue::default()); 198 | 199 | for message in messages.0 { 200 | application.handle_message(message, &mut context); 201 | } 202 | 203 | let mut window_components = world.write_storage::(); 204 | 205 | for window in windows.values_mut() { 206 | let window_component = window_components 207 | .get_mut(window.entity) 208 | .expect("Could not find window component"); 209 | 210 | unsafe { 211 | window.window.make_current().ok(); 212 | } 213 | 214 | let hidpi_factor = window.window.get_hidpi_factor(); 215 | let framebuffer_size = { 216 | let size = window 217 | .window 218 | .get_inner_size() 219 | .unwrap() 220 | .to_physical(hidpi_factor); 221 | DeviceIntSize::new(size.width as i32, size.height as i32) 222 | }; 223 | 224 | if window_component.dirty() { 225 | window_component.set_dirty(false); 226 | 227 | if let Some(builder) = window_component.display_list_builder.take() { 228 | let mut txn = Transaction::new(); 229 | 230 | txn.set_display_list( 231 | window.epoch, 232 | None, 233 | window_component.layout_size, 234 | builder.finalize(), 235 | true, 236 | ); 237 | txn.set_root_pipeline(window_component.pipeline_id); 238 | txn.generate_frame(); 239 | window.api.send_transaction(window.document_id, txn); 240 | } 241 | } 242 | 243 | window.renderer.update(); 244 | window.renderer.render(framebuffer_size).unwrap(); 245 | window.window.swap_buffers().ok(); 246 | } 247 | 248 | if windows.is_empty() { 249 | glutin::ControlFlow::Break 250 | } else { 251 | glutin::ControlFlow::Continue 252 | } 253 | }); 254 | 255 | for renderer in renderers { 256 | renderer.deinit(); 257 | } 258 | } 259 | } 260 | 261 | pub(crate) struct WindowComponent { 262 | root: WidgetId, 263 | layout_size: LayoutSize, 264 | dirty: bool, 265 | pipeline_id: PipelineId, 266 | hovered: Option, 267 | clicked: Option, 268 | pub(crate) display_list_builder: Option, 269 | pub(crate) font_instance_key: FontInstanceKey, 270 | pub(crate) font: Font<'static>, 271 | } 272 | 273 | impl WindowComponent { 274 | pub fn layout_size(&self) -> LayoutSize { 275 | self.layout_size 276 | } 277 | 278 | pub fn dirty(&self) -> bool { 279 | self.dirty 280 | } 281 | 282 | pub fn set_dirty(&mut self, dirty: bool) { 283 | self.dirty = dirty 284 | } 285 | } 286 | 287 | impl Component for WindowComponent { 288 | type Storage = DenseVecStorage; 289 | } 290 | 291 | pub struct RenderWindow { 292 | window: glutin::GlWindow, 293 | renderer: webrender::Renderer, 294 | document_id: DocumentId, 295 | epoch: Epoch, 296 | api: RenderApi, 297 | entity: Entity, 298 | pipeline_id: PipelineId, 299 | font_instance_key: FontInstanceKey, 300 | show_profiler: bool, 301 | } 302 | 303 | impl RenderWindow { 304 | pub fn new( 305 | title: &str, 306 | events_loop: &EventsLoop, 307 | entity: Entity, 308 | size: Size, 309 | pipeline_id: PipelineId, 310 | ) -> Result { 311 | let window_builder = WindowBuilder::new() 312 | .with_title(title) 313 | .with_dimensions((f64::from(size.width), f64::from(size.height)).into()); 314 | let context = glutin::ContextBuilder::new(); 315 | let window = glutin::GlWindow::new(window_builder, context, events_loop)?; 316 | 317 | unsafe { 318 | window.make_current().ok(); 319 | } 320 | 321 | let gl = match window.get_api() { 322 | glutin::Api::OpenGl => unsafe { 323 | gl::GlFns::load_with(|symbol| window.get_proc_address(symbol) as *const _) 324 | }, 325 | glutin::Api::OpenGlEs => unsafe { 326 | gl::GlesFns::load_with(|symbol| window.get_proc_address(symbol) as *const _) 327 | }, 328 | glutin::Api::WebGl => unimplemented!(), 329 | }; 330 | 331 | let hidpi_factor = window.get_hidpi_factor(); 332 | 333 | let opts = webrender::RendererOptions { 334 | device_pixel_ratio: hidpi_factor as f32, 335 | clear_color: Some(ColorF::new(0.98, 0.98, 0.98, 1.0)), 336 | debug_flags: webrender::DebugFlags::empty(), 337 | ..webrender::RendererOptions::default() 338 | }; 339 | 340 | let framebuffer_size = { 341 | let size = window.get_inner_size().unwrap().to_physical(hidpi_factor); 342 | DeviceIntSize::new(size.width as i32, size.height as i32) 343 | }; 344 | let notifier = Box::new(Notifier::new(events_loop.create_proxy())); 345 | let (renderer, sender) = 346 | webrender::Renderer::new(gl.clone(), notifier, opts, None, framebuffer_size).unwrap(); 347 | let api = sender.create_api(); 348 | let document_id = api.add_document(framebuffer_size, 0); 349 | let epoch = Epoch(0); 350 | 351 | let mut txn = Transaction::new(); 352 | 353 | let font_key = api.generate_font_key(); 354 | txn.add_raw_font(font_key, Vec::from(FONT_DATA), 0); 355 | 356 | let font_instance_key = api.generate_font_instance_key(); 357 | txn.add_font_instance( 358 | font_instance_key, 359 | font_key, 360 | Au::from_px(32), 361 | None, 362 | None, 363 | Vec::new(), 364 | ); 365 | 366 | api.send_transaction(document_id, txn); 367 | 368 | Ok(RenderWindow { 369 | window, 370 | renderer, 371 | api, 372 | epoch, 373 | document_id, 374 | entity, 375 | pipeline_id, 376 | font_instance_key, 377 | show_profiler: false, 378 | }) 379 | } 380 | 381 | fn handle_event( 382 | &mut self, 383 | event: glutin::WindowEvent, 384 | world: &World, 385 | ) -> EventResponse { 386 | let mut window_components = world.write_storage::(); 387 | let mut window_component = window_components.get_mut(self.entity).unwrap(); 388 | 389 | match event { 390 | glutin::WindowEvent::CloseRequested 391 | | glutin::WindowEvent::KeyboardInput { 392 | input: 393 | glutin::KeyboardInput { 394 | virtual_keycode: Some(glutin::VirtualKeyCode::Escape), 395 | .. 396 | }, 397 | .. 398 | } => EventResponse::Quit, 399 | glutin::WindowEvent::Resized(size) => { 400 | let hidpi_factor = self.window.get_hidpi_factor(); 401 | self.window.resize(size.to_physical(hidpi_factor)); 402 | let framebuffer_size = { 403 | let size = self 404 | .window 405 | .get_inner_size() 406 | .unwrap() 407 | .to_physical(hidpi_factor); 408 | DeviceIntSize::new(size.width as i32, size.height as i32) 409 | }; 410 | 411 | self.api.set_document_view( 412 | self.document_id, 413 | DeviceIntRect::new(DeviceIntPoint::zero(), framebuffer_size), 414 | hidpi_factor as f32, 415 | ); 416 | EventResponse::Dirty 417 | } 418 | glutin::WindowEvent::KeyboardInput { 419 | input: 420 | glutin::KeyboardInput { 421 | state: glutin::ElementState::Pressed, 422 | virtual_keycode: Some(glutin::VirtualKeyCode::P), 423 | .. 424 | }, 425 | .. 426 | } => { 427 | if !self.show_profiler { 428 | self.renderer 429 | .set_debug_flags(webrender::DebugFlags::PROFILER_DBG); 430 | } else { 431 | self.renderer 432 | .set_debug_flags(webrender::DebugFlags::empty()); 433 | } 434 | self.show_profiler = !self.show_profiler; 435 | EventResponse::Continue 436 | } 437 | glutin::WindowEvent::CursorMoved { position, .. } => { 438 | if window_component.clicked.is_some() { 439 | return EventResponse::Continue; 440 | } 441 | let world_position = WorldPoint::new(position.x as f32, position.y as f32); 442 | let results = self.api.hit_test( 443 | self.document_id, 444 | Some(self.pipeline_id), 445 | world_position, 446 | HitTestFlags::empty(), 447 | ); 448 | let interactive = world.read_storage::(); 449 | let entities = world.entities(); 450 | let mut events = world.write_storage::(); 451 | let hit = results 452 | .items 453 | .iter() 454 | .map(|item| item.tag.0) 455 | .next() 456 | .and_then(|id| { 457 | (&entities, &interactive) 458 | .join() 459 | .filter(|(e, _)| entities.is_alive(*e)) 460 | .find(|(_, i)| i.tag == id) 461 | .map(|(e, _)| e) 462 | }); 463 | 464 | let changed = hit != window_component.hovered; 465 | 466 | if changed { 467 | match (hit, window_component.hovered) { 468 | (Some(new), Some(old)) => { 469 | events 470 | .insert(old, Event::new(Interaction::Hovered(false))) 471 | .ok(); 472 | events 473 | .insert(new, Event::new(Interaction::Hovered(true))) 474 | .ok(); 475 | } 476 | (Some(new), None) => { 477 | events 478 | .insert(new, Event::new(Interaction::Hovered(true))) 479 | .ok(); 480 | } 481 | (None, Some(old)) => { 482 | events 483 | .insert(old, Event::new(Interaction::Hovered(false))) 484 | .ok(); 485 | } 486 | _ => {} 487 | } 488 | } 489 | 490 | window_component.hovered = hit; 491 | 492 | if changed { 493 | EventResponse::Dirty 494 | } else { 495 | EventResponse::Continue 496 | } 497 | } 498 | glutin::WindowEvent::MouseInput { 499 | button: glutin::MouseButton::Left, 500 | state, 501 | .. 502 | } => { 503 | match state { 504 | glutin::ElementState::Pressed => { 505 | if let Some(entity) = window_component.hovered { 506 | let mut events = world.write_storage::(); 507 | events 508 | .insert(entity, Event::new(Interaction::MouseDown)) 509 | .ok(); 510 | window_component.clicked = Some(entity); 511 | } 512 | } 513 | glutin::ElementState::Released => { 514 | if let Some(entity) = window_component.clicked.take() { 515 | let mut events = world.write_storage::(); 516 | events.insert(entity, Event::new(Interaction::MouseUp)).ok(); 517 | } 518 | } 519 | } 520 | 521 | EventResponse::Dirty 522 | } 523 | _ => EventResponse::Continue, 524 | } 525 | } 526 | } 527 | 528 | enum EventResponse { 529 | Continue, 530 | Quit, 531 | Dirty, 532 | } 533 | 534 | struct Notifier { 535 | events_proxy: glutin::EventsLoopProxy, 536 | } 537 | 538 | impl Notifier { 539 | fn new(events_proxy: glutin::EventsLoopProxy) -> Notifier { 540 | Notifier { events_proxy } 541 | } 542 | } 543 | 544 | impl RenderNotifier for Notifier { 545 | fn clone(&self) -> Box { 546 | Box::new(Notifier { 547 | events_proxy: self.events_proxy.clone(), 548 | }) 549 | } 550 | 551 | fn wake_up(&self) { 552 | let _ = self.events_proxy.wakeup(); 553 | } 554 | 555 | fn new_frame_ready( 556 | &self, 557 | _: DocumentId, 558 | _scrolled: bool, 559 | _composite_needed: bool, 560 | _render_time: Option, 561 | ) { 562 | self.wake_up(); 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /imagine/src/render/display_list.rs: -------------------------------------------------------------------------------- 1 | // pub use webrender::api::*; 2 | 3 | // pub struct BaseDisplayItem { 4 | // bounds: LayoutRect, 5 | // clip_rect: LayoutRect, 6 | // clipping: 7 | // } 8 | -------------------------------------------------------------------------------- /imagine/src/render/mod.rs: -------------------------------------------------------------------------------- 1 | use webrender::api::{DisplayListBuilder, FontInstanceKey, SpaceAndClipInfo, PipelineId}; 2 | 3 | pub struct RenderContext<'a> { 4 | pub builder: &'a mut DisplayListBuilder, 5 | pub current_space_and_clip: SpaceAndClipInfo, 6 | next_tag_identifier: u64, 7 | font_instance_key: FontInstanceKey, 8 | } 9 | 10 | impl<'a> RenderContext<'a> { 11 | pub(crate) fn new( 12 | builder: &'a mut DisplayListBuilder, 13 | font_instance_key: FontInstanceKey, 14 | pipeline_id: PipelineId, 15 | ) -> RenderContext<'a> { 16 | RenderContext { 17 | builder, 18 | next_tag_identifier: 0, 19 | font_instance_key, 20 | current_space_and_clip: SpaceAndClipInfo::root_scroll(pipeline_id), 21 | } 22 | } 23 | 24 | pub fn next_tag_identifier(&mut self) -> u64 { 25 | let identifier = self.next_tag_identifier; 26 | self.next_tag_identifier += 1; 27 | identifier 28 | } 29 | 30 | pub fn font_instance_key(&self) -> FontInstanceKey { 31 | self.font_instance_key 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /imagine/src/systems/interaction_system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | interactive::{Event, Interaction}, 3 | ClickListener, Message, MessageQueue, WidgetComponent, 4 | }; 5 | use specs::{Entities, Join, System, Write, WriteStorage, ReadStorage}; 6 | use std::marker::PhantomData; 7 | 8 | pub(crate) struct InteractionSystem { 9 | phantom: PhantomData, 10 | } 11 | 12 | impl Default for InteractionSystem { 13 | fn default() -> InteractionSystem { 14 | InteractionSystem { 15 | phantom: PhantomData, 16 | } 17 | } 18 | } 19 | 20 | impl<'a, M: Message> System<'a> for InteractionSystem { 21 | type SystemData = ( 22 | Entities<'a>, 23 | WriteStorage<'a, Event>, 24 | WriteStorage<'a, WidgetComponent>, 25 | ReadStorage<'a, ClickListener>, 26 | Write<'a, MessageQueue>, 27 | ); 28 | 29 | fn run( 30 | &mut self, 31 | (entities, mut events, mut widgets, listeners, mut queue): Self::SystemData, 32 | ) { 33 | for (event, widget) in (&mut events, &mut widgets).join() { 34 | widget.handle_interaction(event.event); 35 | } 36 | 37 | for (entity, event) in (&entities, &mut events).join() { 38 | if let Interaction::MouseDown = event.event { 39 | if let Some(listener) = listeners.get(entity) { 40 | queue.0.push((listener.on_click)()); 41 | } 42 | } 43 | } 44 | 45 | events.clear(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /imagine/src/systems/layout_system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | layout::{BoxConstraint, LayoutContext, Position, Size}, 3 | text::FinalText, 4 | widget::WidgetComponent, 5 | WindowComponent, 6 | }; 7 | use specs::{Join, ReadStorage, System, WriteStorage}; 8 | 9 | pub(crate) struct LayoutSystem; 10 | 11 | impl<'a> System<'a> for LayoutSystem { 12 | type SystemData = ( 13 | WriteStorage<'a, Size>, 14 | WriteStorage<'a, Position>, 15 | WriteStorage<'a, FinalText>, 16 | ReadStorage<'a, WidgetComponent>, 17 | ReadStorage<'a, WindowComponent>, 18 | ); 19 | 20 | fn run(&mut self, (mut sizes, mut positions, mut text, widgets, windows): Self::SystemData) { 21 | for window in windows.join() { 22 | if !window.dirty() { 23 | continue; 24 | } 25 | let layout_size = window.layout_size(); 26 | let constraint = BoxConstraint::new( 27 | Size::zero(), 28 | Size::new(layout_size.width, layout_size.height), 29 | ); 30 | 31 | let mut layout_context = LayoutContext::new( 32 | &mut positions, 33 | &mut sizes, 34 | &mut text, 35 | &widgets, 36 | &window.font, 37 | ); 38 | layout_context.layout_widget(window.root, constraint); 39 | positions.insert(window.root.0, Position::zero()).ok(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /imagine/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | mod interaction_system; 2 | mod layout_system; 3 | mod render_system; 4 | 5 | pub(crate) use self::interaction_system::InteractionSystem; 6 | pub(crate) use self::layout_system::LayoutSystem; 7 | pub(crate) use self::render_system::RenderSystem; 8 | -------------------------------------------------------------------------------- /imagine/src/systems/render_system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | text::FinalText, Geometry, Interactive, Position, RenderContext, Size, WidgetComponent, 3 | WidgetId, WindowComponent, 4 | }; 5 | use specs::{Entities, Join, ReadStorage, System, WriteStorage}; 6 | use webrender::api::*; 7 | use webrender::api::units::*; 8 | 9 | pub(crate) struct RenderSystem; 10 | 11 | impl<'a> System<'a> for RenderSystem { 12 | type SystemData = ( 13 | Entities<'a>, 14 | ReadStorage<'a, Size>, 15 | ReadStorage<'a, Position>, 16 | ReadStorage<'a, WidgetComponent>, 17 | ReadStorage<'a, FinalText>, 18 | WriteStorage<'a, WindowComponent>, 19 | WriteStorage<'a, Interactive>, 20 | ); 21 | 22 | fn run( 23 | &mut self, 24 | (entities, sizes, positions, widgets, text, mut windows, mut interactive): Self::SystemData, 25 | ) { 26 | for window in (&mut windows).join() { 27 | if !window.dirty() { 28 | continue; 29 | } 30 | 31 | let mut builder = DisplayListBuilder::new(window.pipeline_id, window.layout_size()); 32 | 33 | builder.push_stacking_context( 34 | LayoutPoint::zero(), 35 | SpatialId::root_reference_frame(window.pipeline_id), 36 | PrimitiveFlags::empty(), 37 | None, 38 | TransformStyle::Flat, 39 | MixBlendMode::Normal, 40 | &[], 41 | &[], 42 | &[], 43 | RasterSpace::Screen, 44 | false, 45 | false, 46 | ); 47 | 48 | let mut render_context = RenderContext::new(&mut builder, window.font_instance_key, window.pipeline_id); 49 | 50 | fn render_entities( 51 | children: &[WidgetId], 52 | data: &( 53 | &ReadStorage, 54 | &ReadStorage, 55 | &ReadStorage, 56 | ), 57 | texts: &ReadStorage, 58 | interactive: &mut WriteStorage, 59 | render_context: &mut RenderContext, 60 | entities: &Entities, 61 | offset: Position, 62 | ) { 63 | for widget_id in children { 64 | let (position, size, widget) = data.join().get(widget_id.0, entities).unwrap(); 65 | let new_position = Position::new(offset.x + position.x, offset.y + position.y); 66 | let text = texts.get(widget_id.0); 67 | let box_size = Geometry::new(new_position, *size); 68 | 69 | match widget.render(*widget_id, box_size, text, render_context) { 70 | Some(tag) => { 71 | if let Some(interactive) = interactive.get_mut(widget_id.0) { 72 | interactive.tag = tag; 73 | } else { 74 | interactive.insert(widget_id.0, Interactive::new(tag)).ok(); 75 | } 76 | } 77 | None => { 78 | interactive.remove(widget_id.0); 79 | } 80 | } 81 | 82 | render_entities( 83 | &widget.children(), 84 | data, 85 | texts, 86 | interactive, 87 | render_context, 88 | entities, 89 | new_position, 90 | ); 91 | } 92 | } 93 | 94 | render_entities( 95 | &[window.root], 96 | &(&positions, &sizes, &widgets), 97 | &text, 98 | &mut interactive, 99 | &mut render_context, 100 | &entities, 101 | Position::zero(), 102 | ); 103 | 104 | builder.pop_stacking_context(); 105 | 106 | window.display_list_builder = Some(builder); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /imagine/src/text.rs: -------------------------------------------------------------------------------- 1 | use rusttype::{point, Font, Scale}; 2 | use webrender::api::*; 3 | use webrender::api::units::*; 4 | use specs::{Component, DenseVecStorage}; 5 | 6 | #[derive(Debug)] 7 | pub struct FinalText { 8 | glyphs: Vec, 9 | width: f32, 10 | } 11 | 12 | impl Component for FinalText { 13 | type Storage = DenseVecStorage; 14 | } 15 | 16 | impl FinalText { 17 | pub(crate) fn new(font: &Font, text: &str) -> FinalText { 18 | let height: f32 = 20.0; 19 | 20 | let scale = Scale { 21 | x: height * 2.0, 22 | y: height, 23 | }; 24 | let v_metrics = font.v_metrics(scale); 25 | let offset = point(0.0, v_metrics.ascent + 10.0); 26 | 27 | let (glyphs, width) = font.layout(text, scale, offset).fold( 28 | (Vec::new(), 0.0f32), 29 | |(mut glyphs, max_width), glyph| { 30 | let position = glyph.position(); 31 | glyphs.push(GlyphInstance { 32 | index: glyph.id().0, 33 | point: LayoutPoint::new(position.x, position.y), 34 | }); 35 | ( 36 | glyphs, 37 | max_width.max(position.x + glyph.unpositioned().h_metrics().advance_width), 38 | ) 39 | }, 40 | ); 41 | 42 | FinalText { glyphs, width } 43 | } 44 | 45 | pub fn render( 46 | &self, 47 | info: &CommonItemProperties, 48 | bounds: LayoutRect, 49 | origin: LayoutPoint, 50 | builder: &mut DisplayListBuilder, 51 | font_instance_key: FontInstanceKey, 52 | ) { 53 | let glyphs = self 54 | .glyphs 55 | .iter() 56 | .map(|glyph| GlyphInstance { 57 | index: glyph.index, 58 | point: LayoutPoint::new(glyph.point.x + origin.x, glyph.point.y + origin.y), 59 | }) 60 | .collect::>(); 61 | builder.push_text(info, bounds, &glyphs, font_instance_key, ColorF::BLACK, None); 62 | } 63 | 64 | pub fn width(&self) -> f32 { 65 | self.width 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /imagine/src/widget.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | interactive::Interaction, text::FinalText, BoxConstraint, Geometry, LayoutContext, Message, 3 | RenderContext, Size, WidgetContext, 4 | }; 5 | use specs::{Component, DenseVecStorage, Entity}; 6 | use std::any::Any; 7 | 8 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 9 | pub struct WidgetId(pub(crate) Entity); 10 | 11 | pub trait WidgetBuilder { 12 | fn build(self, context: &mut WidgetContext) -> WidgetId; 13 | } 14 | 15 | pub trait Widget: Send + Sync { 16 | fn layout( 17 | &self, 18 | id: WidgetId, 19 | layout_context: &mut LayoutContext, 20 | box_constraint: BoxConstraint, 21 | ) -> Size; 22 | 23 | fn children(&self) -> Vec; 24 | 25 | fn render( 26 | &self, 27 | _id: WidgetId, 28 | _geometry: Geometry, 29 | _text: Option<&FinalText>, 30 | _render_context: &mut RenderContext, 31 | ) -> Option { 32 | None 33 | } 34 | 35 | fn handle_interaction(&mut self, _interaction: Interaction) {} 36 | 37 | fn update(&mut self, _event: Box) -> Option> { 38 | None 39 | } 40 | } 41 | 42 | pub(crate) struct WidgetComponent { 43 | pub(crate) inner: Box, 44 | } 45 | 46 | impl std::ops::Deref for WidgetComponent { 47 | type Target = dyn Widget; 48 | 49 | fn deref(&self) -> &(dyn Widget + 'static) { 50 | self.inner.deref() 51 | } 52 | } 53 | 54 | impl std::ops::DerefMut for WidgetComponent { 55 | fn deref_mut(&mut self) -> &mut (dyn Widget + 'static) { 56 | self.inner.deref_mut() 57 | } 58 | } 59 | 60 | impl Component for WidgetComponent { 61 | type Storage = DenseVecStorage; 62 | } 63 | -------------------------------------------------------------------------------- /imagine_toolkit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imagine_toolkit" 3 | license = "MIT" 4 | version = "0.1.0" 5 | authors = ["Connor Brewster "] 6 | edition = "2018" 7 | 8 | [dependencies] 9 | imagine = {path = "../imagine"} 10 | webrender = {git="https://github.com/servo/webrender/"} 11 | -------------------------------------------------------------------------------- /imagine_toolkit/src/button.rs: -------------------------------------------------------------------------------- 1 | use crate::{Center, Label, Padding}; 2 | use imagine::{ 3 | text::FinalText, BoxConstraint, Geometry, Interaction, LayoutContext, Position, RenderContext, 4 | Size, Widget, WidgetContext, WidgetId, 5 | }; 6 | use webrender::api::*; 7 | use webrender::api::units::*; 8 | 9 | pub struct Button { 10 | pub color: (f32, f32, f32, f32), 11 | hovered: bool, 12 | down: bool, 13 | child: WidgetId, 14 | } 15 | 16 | impl Button { 17 | pub fn new, M: 'static + Send + Sync>( 18 | context: &mut WidgetContext, 19 | color: (f32, f32, f32, f32), 20 | text: T, 21 | ) -> Button { 22 | let label = context.create_widget(Label::new(text)); 23 | let center = context.create_widget(Center::new(label)); 24 | let child = context.create_widget(Padding::new(10.0, 10.0, 10.0, 10.0, center)); 25 | 26 | Button { 27 | color, 28 | down: false, 29 | hovered: false, 30 | child, 31 | } 32 | } 33 | } 34 | 35 | impl Widget for Button { 36 | fn layout( 37 | &self, 38 | _id: WidgetId, 39 | layout_context: &mut LayoutContext, 40 | box_constraint: BoxConstraint, 41 | ) -> Size { 42 | let child_size = layout_context.layout_widget( 43 | self.child, 44 | BoxConstraint::new(Size::zero(), box_constraint.max), 45 | ); 46 | layout_context.set_position(self.child, Position::zero()); 47 | box_constraint.constrain(child_size) 48 | } 49 | 50 | fn handle_interaction(&mut self, interaction: Interaction) { 51 | match interaction { 52 | Interaction::Hovered(hovered) => self.hovered = hovered, 53 | Interaction::MouseDown => self.down = true, 54 | Interaction::MouseUp => self.down = false, 55 | } 56 | } 57 | 58 | fn children(&self) -> Vec { 59 | vec![self.child] 60 | } 61 | 62 | fn render( 63 | &self, 64 | _id: WidgetId, 65 | geometry: Geometry, 66 | _text: Option<&FinalText>, 67 | render_context: &mut RenderContext, 68 | ) -> Option { 69 | let mut rect = LayoutRect::new( 70 | LayoutPoint::new(geometry.position.x, geometry.position.y), 71 | LayoutSize::new(geometry.size.width, geometry.size.height), 72 | ); 73 | 74 | if self.down { 75 | rect = rect.inflate(-2.0, -2.0); 76 | } 77 | 78 | let identifier = render_context.next_tag_identifier(); 79 | 80 | let border_radius = BorderRadius::uniform(4.0); 81 | 82 | let clip_id = render_context.builder.define_clip( 83 | &render_context.current_space_and_clip, 84 | rect, 85 | vec![ComplexClipRegion::new( 86 | rect, 87 | border_radius, 88 | ClipMode::Clip, 89 | )], 90 | None, 91 | ); 92 | 93 | let (r, g, b, a) = self.color; 94 | 95 | render_context 96 | .builder 97 | .push_rect( 98 | &CommonItemProperties { 99 | clip_rect: rect, 100 | clip_id, 101 | spatial_id: render_context.current_space_and_clip.spatial_id, 102 | hit_info: Some((identifier, 0)), 103 | flags: PrimitiveFlags::empty(), 104 | }, 105 | ColorF::new(r, g, b, a) 106 | ); 107 | 108 | if self.hovered && !self.down { 109 | render_context.builder.push_box_shadow( 110 | &CommonItemProperties::new(rect.inflate(4.0, 4.0), render_context.current_space_and_clip), 111 | rect, 112 | LayoutVector2D::new(0.0, 3.0), 113 | ColorF::new(0.0, 0.0, 0.0, 0.2), 114 | 4.0, 115 | 0.0, 116 | border_radius, 117 | BoxShadowClipMode::Outset, 118 | ); 119 | } 120 | 121 | Some(identifier) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /imagine_toolkit/src/center.rs: -------------------------------------------------------------------------------- 1 | use imagine::{BoxConstraint, LayoutContext, Position, Size, Widget, WidgetId}; 2 | 3 | pub struct Center { 4 | child: WidgetId, 5 | } 6 | 7 | impl Center { 8 | pub fn new(child: WidgetId) -> Center { 9 | Center { child } 10 | } 11 | } 12 | 13 | impl Widget for Center { 14 | fn layout( 15 | &self, 16 | _id: WidgetId, 17 | layout_context: &mut LayoutContext, 18 | box_constraint: BoxConstraint, 19 | ) -> Size { 20 | let child_size = layout_context.layout_widget(self.child, box_constraint); 21 | let xdiff = box_constraint.max.width - child_size.width; 22 | let ydiff = box_constraint.max.height - child_size.height; 23 | layout_context.set_position(self.child, Position::new(xdiff / 2.0, ydiff / 2.0)); 24 | box_constraint.max 25 | } 26 | 27 | fn children(&self) -> Vec { 28 | vec![self.child] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /imagine_toolkit/src/fill_box.rs: -------------------------------------------------------------------------------- 1 | use imagine::{ 2 | text::FinalText, BoxConstraint, Geometry, Interaction, LayoutContext, RenderContext, Size, 3 | Widget, WidgetId, 4 | }; 5 | use webrender::api::*; 6 | use webrender::api::units::*; 7 | 8 | pub struct FillBox { 9 | pub size: Size, 10 | pub color: (f32, f32, f32, f32), 11 | hovered: bool, 12 | down: bool, 13 | } 14 | 15 | impl FillBox { 16 | pub fn new(size: Size, color: (f32, f32, f32, f32)) -> FillBox { 17 | FillBox { 18 | size, 19 | color, 20 | down: false, 21 | hovered: false, 22 | } 23 | } 24 | } 25 | 26 | impl Widget for FillBox { 27 | fn layout( 28 | &self, 29 | _id: WidgetId, 30 | _layout_context: &mut LayoutContext, 31 | box_constraint: BoxConstraint, 32 | ) -> Size { 33 | box_constraint.constrain(self.size) 34 | } 35 | 36 | fn handle_interaction(&mut self, interaction: Interaction) { 37 | match interaction { 38 | Interaction::Hovered(hovered) => self.hovered = hovered, 39 | Interaction::MouseDown => self.down = true, 40 | Interaction::MouseUp => self.down = false, 41 | } 42 | } 43 | 44 | fn children(&self) -> Vec { 45 | vec![] 46 | } 47 | 48 | fn render( 49 | &self, 50 | _id: WidgetId, 51 | geometry: Geometry, 52 | _text: Option<&FinalText>, 53 | render_context: &mut RenderContext, 54 | ) -> Option { 55 | let rect = LayoutRect::new( 56 | LayoutPoint::new(geometry.position.x, geometry.position.y), 57 | LayoutSize::new(geometry.size.width, geometry.size.height), 58 | ); 59 | let identifier = render_context.next_tag_identifier(); 60 | 61 | let border_radius = BorderRadius::uniform(4.0); 62 | 63 | let clip_id = render_context.builder.define_clip( 64 | &render_context.current_space_and_clip, 65 | rect, 66 | vec![ComplexClipRegion::new( 67 | rect, 68 | border_radius, 69 | ClipMode::Clip, 70 | )], 71 | None, 72 | ); 73 | 74 | let (r, g, b, mut a) = self.color; 75 | 76 | if self.down { 77 | a = 0.5; 78 | } 79 | 80 | render_context 81 | .builder 82 | .push_rect( 83 | &CommonItemProperties { 84 | clip_rect: rect, 85 | clip_id, 86 | spatial_id: render_context.current_space_and_clip.spatial_id, 87 | hit_info: Some((identifier, 0)), 88 | flags: PrimitiveFlags::empty(), 89 | }, 90 | ColorF::new(r, g, b, a) 91 | ); 92 | 93 | if self.hovered { 94 | render_context.builder.push_box_shadow( 95 | &CommonItemProperties::new(rect.inflate(4.0, 4.0), render_context.current_space_and_clip), 96 | rect, 97 | LayoutVector2D::new(0.0, 3.0), 98 | ColorF::new(0.0, 0.0, 0.0, 0.2), 99 | 4.0, 100 | 0.0, 101 | border_radius, 102 | BoxShadowClipMode::Inset, 103 | ); 104 | } 105 | 106 | Some(identifier) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /imagine_toolkit/src/flex.rs: -------------------------------------------------------------------------------- 1 | use imagine::{BoxConstraint, LayoutContext, Position, Size, Widget, WidgetId}; 2 | use std::any::Any; 3 | 4 | #[derive(Copy, Clone)] 5 | pub enum FlexDirection { 6 | Horizontal, 7 | Vertical, 8 | } 9 | 10 | pub enum FlexEvent { 11 | AddChild(FlexItem), 12 | RemoveChild, 13 | } 14 | 15 | impl FlexDirection { 16 | pub fn major_axis(self, size: Size) -> f32 { 17 | match self { 18 | FlexDirection::Horizontal => size.width, 19 | FlexDirection::Vertical => size.height, 20 | } 21 | } 22 | 23 | pub fn minor_axis(self, size: Size) -> f32 { 24 | match self { 25 | FlexDirection::Horizontal => size.height, 26 | FlexDirection::Vertical => size.width, 27 | } 28 | } 29 | 30 | pub fn major_minor_to_position(self, major: f32, minor: f32) -> Position { 31 | match self { 32 | FlexDirection::Horizontal => Position::new(major, minor), 33 | FlexDirection::Vertical => Position::new(minor, major), 34 | } 35 | } 36 | 37 | pub fn major_minor_to_size(self, major: f32, minor: f32) -> Size { 38 | match self { 39 | FlexDirection::Horizontal => Size::new(major, minor), 40 | FlexDirection::Vertical => Size::new(minor, major), 41 | } 42 | } 43 | } 44 | 45 | #[derive(Copy, Clone)] 46 | pub enum FlexAlign { 47 | Top, 48 | Middle, 49 | Baseline, 50 | } 51 | 52 | #[derive(Copy, Clone, Debug)] 53 | pub enum FlexItem { 54 | NonFlex(WidgetId), 55 | Flex(WidgetId, usize), 56 | } 57 | 58 | impl FlexItem { 59 | pub fn widget(self) -> WidgetId { 60 | match self { 61 | FlexItem::Flex(widget, _) => widget, 62 | FlexItem::NonFlex(widget) => widget, 63 | } 64 | } 65 | } 66 | 67 | pub struct Flex { 68 | children: Vec, 69 | flex_direction: FlexDirection, 70 | flex_align: FlexAlign, 71 | } 72 | 73 | impl Flex { 74 | pub fn new( 75 | children: Vec, 76 | flex_direction: FlexDirection, 77 | flex_align: FlexAlign, 78 | ) -> Flex { 79 | Flex { 80 | children, 81 | flex_direction, 82 | flex_align, 83 | } 84 | } 85 | } 86 | 87 | impl Widget for Flex { 88 | fn layout( 89 | &self, 90 | _id: WidgetId, 91 | layout_context: &mut LayoutContext, 92 | box_constraint: BoxConstraint, 93 | ) -> Size { 94 | let mut flex_widgets = vec![]; 95 | let mut non_flex_widgets = vec![]; 96 | let mut total_flex = 0; 97 | let mut non_flex_major = 0.0; 98 | let mut max_minor: f32 = 0.0; 99 | 100 | // Separate out flex and non-flex children 101 | for child in &self.children { 102 | match child { 103 | FlexItem::Flex(widget, flex) => { 104 | total_flex += flex; 105 | flex_widgets.push((widget, flex)); 106 | } 107 | FlexItem::NonFlex(widget) => { 108 | non_flex_widgets.push(widget); 109 | } 110 | } 111 | } 112 | 113 | // Layout non-flex children first 114 | for widget in non_flex_widgets { 115 | let child_constraint = match self.flex_direction { 116 | FlexDirection::Horizontal => BoxConstraint::new( 117 | Size::new(0.0, box_constraint.min.height), 118 | Size::new(std::f32::INFINITY, box_constraint.max.height), 119 | ), 120 | FlexDirection::Vertical => BoxConstraint::new( 121 | Size::new(box_constraint.min.width, 0.0), 122 | Size::new(box_constraint.max.width, std::f32::INFINITY), 123 | ), 124 | }; 125 | let child_size = layout_context.layout_widget(*widget, child_constraint); 126 | max_minor = max_minor.max(self.flex_direction.minor_axis(child_size)); 127 | non_flex_major += self.flex_direction.major_axis(child_size); 128 | } 129 | 130 | // Layout flex children 131 | for (widget, flex) in flex_widgets { 132 | let total_major = self.flex_direction.major_axis(box_constraint.max); 133 | let remaining = (total_major - non_flex_major).max(0.0); 134 | let major = remaining * (*flex as f32 / total_flex as f32); 135 | 136 | let child_constraint = match self.flex_direction { 137 | FlexDirection::Horizontal => BoxConstraint::new( 138 | Size::new(major, box_constraint.min.height), 139 | Size::new(major, box_constraint.max.height), 140 | ), 141 | FlexDirection::Vertical => BoxConstraint::new( 142 | Size::new(box_constraint.min.width, major), 143 | Size::new(box_constraint.max.width, major), 144 | ), 145 | }; 146 | 147 | let child_size = layout_context.layout_widget(*widget, child_constraint); 148 | max_minor = max_minor.max(self.flex_direction.minor_axis(child_size)); 149 | } 150 | 151 | let mut current_major = 0.0; 152 | for child in self.children.iter().map(|item| item.widget()) { 153 | let size = layout_context.get_size(child); 154 | let minor = match self.flex_align { 155 | FlexAlign::Top => 0.0, 156 | FlexAlign::Middle => (max_minor - self.flex_direction.minor_axis(size)) / 2.0, 157 | FlexAlign::Baseline => max_minor - self.flex_direction.minor_axis(size), 158 | }; 159 | 160 | let position = self 161 | .flex_direction 162 | .major_minor_to_position(current_major, minor); 163 | 164 | layout_context.set_position(child, position); 165 | 166 | current_major += self.flex_direction.major_axis(size); 167 | } 168 | 169 | self.flex_direction 170 | .major_minor_to_size(current_major, max_minor) 171 | } 172 | 173 | fn children(&self) -> Vec { 174 | self.children 175 | .iter() 176 | .map(|item| match item { 177 | FlexItem::NonFlex(child) => *child, 178 | FlexItem::Flex(child, _) => *child, 179 | }) 180 | .collect() 181 | } 182 | 183 | fn update(&mut self, event: Box) -> Option> { 184 | if let Ok(event) = event.downcast::() { 185 | match *event { 186 | FlexEvent::AddChild(child) => { 187 | self.children.push(child); 188 | } 189 | FlexEvent::RemoveChild => { 190 | return self 191 | .children 192 | .pop() 193 | .map(|item| match item { 194 | FlexItem::NonFlex(id) => id, 195 | FlexItem::Flex(id, _) => id, 196 | }) 197 | .map(|item| vec![item]); 198 | } 199 | } 200 | } 201 | None 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /imagine_toolkit/src/label.rs: -------------------------------------------------------------------------------- 1 | use imagine::{ 2 | text::FinalText, BoxConstraint, Geometry, LayoutContext, RenderContext, Size, Widget, WidgetId, 3 | }; 4 | use std::any::Any; 5 | use webrender::api::*; 6 | use webrender::api::units::*; 7 | 8 | pub enum LabelMessage { 9 | SetText(String), 10 | } 11 | 12 | pub struct Label { 13 | text: String, 14 | } 15 | 16 | impl Label { 17 | pub fn new>(text: T) -> Label { 18 | Label { text: text.into() } 19 | } 20 | } 21 | 22 | impl Widget for Label { 23 | fn children(&self) -> Vec { 24 | vec![] 25 | } 26 | 27 | fn layout( 28 | &self, 29 | id: WidgetId, 30 | layout_context: &mut LayoutContext, 31 | box_constraint: BoxConstraint, 32 | ) -> Size { 33 | let final_text = layout_context.layout_text(&self.text); 34 | let width = final_text.width(); 35 | layout_context.set_text(id, final_text); 36 | box_constraint.constrain(Size::new(width, 32.0)) 37 | } 38 | 39 | fn render( 40 | &self, 41 | _id: WidgetId, 42 | geometry: Geometry, 43 | text: Option<&FinalText>, 44 | render_context: &mut RenderContext, 45 | ) -> Option { 46 | let origin = LayoutPoint::new(geometry.position.x, geometry.position.y); 47 | let rect = LayoutRect::new( 48 | origin, 49 | LayoutSize::new(geometry.size.width, geometry.size.height), 50 | ); 51 | 52 | if let Some(final_text) = text { 53 | final_text.render( 54 | &CommonItemProperties::new(rect, render_context.current_space_and_clip), 55 | rect, 56 | origin, 57 | render_context.builder, 58 | render_context.font_instance_key(), 59 | ); 60 | } 61 | None 62 | } 63 | 64 | fn update(&mut self, event: Box) -> Option> { 65 | if let Ok(event) = event.downcast::() { 66 | match *event { 67 | LabelMessage::SetText(text) => self.text = text, 68 | } 69 | } 70 | None 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /imagine_toolkit/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod button; 2 | pub mod center; 3 | pub mod fill_box; 4 | pub mod flex; 5 | pub mod label; 6 | pub mod list; 7 | pub mod padding; 8 | 9 | pub use self::{ 10 | button::Button, 11 | center::Center, 12 | fill_box::FillBox, 13 | flex::{Flex, FlexAlign, FlexDirection, FlexEvent, FlexItem}, 14 | label::{Label, LabelMessage}, 15 | list::List, 16 | padding::Padding, 17 | }; 18 | -------------------------------------------------------------------------------- /imagine_toolkit/src/list.rs: -------------------------------------------------------------------------------- 1 | use imagine::{BoxConstraint, LayoutContext, Position, Size, Widget, WidgetId}; 2 | 3 | pub struct List { 4 | widgets: Vec, 5 | } 6 | 7 | impl List { 8 | pub fn new(widgets: Vec) -> List { 9 | List { widgets } 10 | } 11 | } 12 | 13 | impl Widget for List { 14 | fn children(&self) -> Vec { 15 | self.widgets.clone() 16 | } 17 | 18 | fn layout( 19 | &self, 20 | _id: WidgetId, 21 | layout_context: &mut LayoutContext, 22 | box_constraint: BoxConstraint, 23 | ) -> Size { 24 | let mut current_y = 0.0; 25 | for child in &self.widgets { 26 | let child_size = layout_context.layout_widget( 27 | *child, 28 | BoxConstraint::new( 29 | Size::new(box_constraint.max.width, 0.0), 30 | Size::new(box_constraint.max.width, std::f32::INFINITY), 31 | ), 32 | ); 33 | 34 | layout_context.set_position(*child, Position::new(0.0, current_y)); 35 | current_y += child_size.height; 36 | } 37 | box_constraint.constrain(Size::new(box_constraint.max.width, current_y)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /imagine_toolkit/src/padding.rs: -------------------------------------------------------------------------------- 1 | use imagine::{BoxConstraint, LayoutContext, Position, Size, Widget, WidgetId}; 2 | 3 | pub struct Padding { 4 | top: f32, 5 | bottom: f32, 6 | left: f32, 7 | right: f32, 8 | child: WidgetId, 9 | } 10 | 11 | impl Padding { 12 | pub fn new(top: f32, bottom: f32, left: f32, right: f32, child: WidgetId) -> Padding { 13 | Padding { 14 | top, 15 | bottom, 16 | left, 17 | right, 18 | child, 19 | } 20 | } 21 | } 22 | 23 | impl Widget for Padding { 24 | fn layout( 25 | &self, 26 | _id: WidgetId, 27 | layout_context: &mut LayoutContext, 28 | box_constraint: BoxConstraint, 29 | ) -> Size { 30 | let child_constraint = BoxConstraint::new( 31 | Size::new( 32 | box_constraint.min.width - (self.right + self.left), 33 | box_constraint.min.height - (self.top + self.bottom), 34 | ), 35 | Size::new( 36 | box_constraint.max.width - (self.right + self.left), 37 | box_constraint.max.height - (self.top + self.bottom), 38 | ), 39 | ); 40 | let child_size = layout_context.layout_widget(self.child, child_constraint); 41 | layout_context.set_position(self.child, Position::new(self.top, self.left)); 42 | Size::new( 43 | child_size.width + (self.right + self.left), 44 | child_size.height + (self.top + self.bottom), 45 | ) 46 | } 47 | 48 | fn children(&self) -> Vec { 49 | vec![self.child] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | --------------------------------------------------------------------------------