├── .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 |
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 |
--------------------------------------------------------------------------------