├── .gitignore ├── Cargo.toml ├── README.md ├── data └── OpenSans-Regular.ttf ├── examples ├── calc.rs ├── hello.rs ├── test.rs └── test2.rs ├── misc └── example.png └── src ├── backend └── mod.rs ├── canvas.rs ├── draw.rs ├── element.rs ├── event ├── click.rs └── mod.rs ├── layout.rs ├── lib.rs ├── state.rs └── widget ├── button.rs ├── label.rs ├── list.rs ├── mod.rs ├── padded.rs └── toggle.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gui" 3 | version = "0.1.0" 4 | authors = ["Joshua Barretto "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | vek = "0.12" 11 | minifb = "0.25" 12 | andrew = "0.3" 13 | piet = "0.6.2" 14 | piet-cairo = "0.6.2" 15 | cairo-rs = { version = "0.16.7", default-features = false } 16 | 17 | [profile.dev] 18 | opt-level = 2 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The current trend in Rust UI libraries seems to be towards relaxed, 2 | immediate-mode APIs that guarantee little and have fairly significant refresh 3 | overhead. This crate is an attempt by me to shake things up and build something 4 | declarative, fast, structured, type-safe and with a clean separation between 5 | data and UI state. Judge the success of this attempt for yourself. 6 | 7 | ## Example 8 | 9 | Here follows an example of a fully-functioning calculator app written using this 10 | API. 11 | 12 | # It's a calculator! 13 | 14 | ```rust 15 | #[derive(Default)] 16 | struct Data { 17 | screen: String, 18 | second: f64, 19 | op: Option f64>, 20 | } 21 | 22 | impl Data { 23 | fn start_op(&mut self, op: fn(f64, f64) -> f64) { 24 | self.second = std::mem::take(&mut self.screen).parse().unwrap(); 25 | self.op = Some(op); 26 | } 27 | 28 | fn calc(&mut self) { 29 | let screen = self.screen.parse().unwrap_or(0.0); 30 | self.screen = match self.op.take() { 31 | Some(op) => op(self.second, screen), 32 | None => return, 33 | }.to_string(); 34 | } 35 | } 36 | 37 | let num_button = |n: &'static str| Button::::default_state() 38 | .containing(Label::::with_state(n).padded(16.0)) 39 | .on(Click, move |ctx| ctx.data.screen.push_str(&n)) 40 | .padded(8.0); 41 | 42 | let ui = List::::vertical() 43 | .push(Button::::default_state() 44 | .containing(Label::::bind_state(|d| &mut d.screen).padded(16.0)) 45 | .padded(8.0)) 46 | .push(List::::horizontal() 47 | .push(num_button("1")) 48 | .push(num_button("2")) 49 | .push(num_button("3")) 50 | .push(Button::::default_state() 51 | .containing(Label::::with_state("+").padded(16.0)) 52 | .on(Click, |ctx| ctx.data.start_op(|x, y| x + y)) 53 | .padded(8.0))) 54 | .push(List::::horizontal() 55 | .push(num_button("4")) 56 | .push(num_button("5")) 57 | .push(num_button("6")) 58 | .push(Button::::default_state() 59 | .containing(Label::::with_state("-").padded(16.0)) 60 | .on(Click, |ctx| ctx.data.start_op(|x, y| x - y)) 61 | .padded(8.0))) 62 | .push(List::::horizontal() 63 | .push(num_button("7")) 64 | .push(num_button("8")) 65 | .push(num_button("9")) 66 | .push(Button::::default_state() 67 | .containing(Label::::with_state("*").padded(16.0)) 68 | .on(Click, |ctx| ctx.data.start_op(|x, y| x * y)) 69 | .padded(8.0))) 70 | .push(List::::horizontal() 71 | .push(Button::::default_state() 72 | .containing(Label::::with_state("C").padded(16.0)) 73 | .on(Click, |ctx| ctx.data.screen.clear()) 74 | .padded(8.0)) 75 | .push(num_button("0")) 76 | .push(Button::::default_state() 77 | .containing(Label::::with_state("=").padded(16.0)) 78 | .on(Click, |ctx| ctx.data.calc()) 79 | .padded(8.0)) 80 | .push(Button::::default_state() 81 | .containing(Label::::with_state("/").padded(16.0)) 82 | .on(Click, |ctx| ctx.data.start_op(|x, y| x / y)) 83 | .padded(8.0))) 84 | .padded(8.0); 85 | 86 | Window::new(ui).run(Data::default()) 87 | ``` 88 | 89 | ## Design 90 | 91 | Here follows a brief and very rough description of the philosophy underpinning 92 | the API. 93 | 94 | - UIs are composed of widgets that sit in a tree hierarchy 95 | - Widget trees are created using an expressive-yet-intuitive builder pattern 96 | - Events are recursively passed down from parent widgets to child widgets 97 | - Children may request various layout properties from their parents (expand, 98 | minimum size, etc.) 99 | - The data that the UI represents is independent of the UI and should, where 100 | possible, not require special sauce to be reflected in the UI (i.e: if you 101 | want to create a list, you just use a `Vec` in the data model) 102 | - Widget trees may be constructed hierarchically such that each part of the UI 103 | can only 'see' part of the underlying data model. It is possible to integrate 104 | sub-trees into a parent in a modular way by providing a mapping between the 105 | parent's data model and the child's data model 106 | - The API for creating widgets that have state (i.e: text boxes, toggle buttons) 107 | is consistent 108 | - 'Transformations' such as padding are generic across widgets and use widget 109 | wrapper types 110 | - The widget tree should never be recreated from scratch to improve performance 111 | - Rendering is performed on a canvas that emits simple primitives (lines, 112 | rectanges, text, etc.) that allow for easy porting to many backends 113 | -------------------------------------------------------------------------------- /data/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zesterer/gui/76665066dee270db35d7147c9a0aac7a64e51b17/data/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /examples/calc.rs: -------------------------------------------------------------------------------- 1 | use gui::{ 2 | widget::{Toggle, Button, Label, List}, 3 | layout::Direction, 4 | event::Click, 5 | Window, Widget, 6 | }; 7 | 8 | fn main() { 9 | #[derive(Default)] 10 | struct Data { 11 | screen: String, 12 | second: f64, 13 | op: Option f64>, 14 | } 15 | 16 | impl Data { 17 | fn start_op(&mut self, op: fn(f64, f64) -> f64) { 18 | self.second = std::mem::take(&mut self.screen).parse().unwrap(); 19 | self.op = Some(op); 20 | } 21 | 22 | fn calc(&mut self) { 23 | let screen = self.screen.parse().unwrap_or(0.0); 24 | self.screen = match self.op.take() { 25 | Some(op) => op(self.second, screen), 26 | None => return, 27 | }.to_string(); 28 | } 29 | } 30 | 31 | let num_button = |n: &'static str| Button::::default_state() 32 | .containing(Label::::with_state(n).padded(16.0)) 33 | .on(Click, move |ctx| ctx.data.screen.push_str(&n)) 34 | .padded(8.0); 35 | 36 | let ui = List::::vertical() 37 | .push(Button::::default_state() 38 | .containing(Label::::bind_state(|d| &mut d.screen).padded(16.0)) 39 | .padded(8.0)) 40 | .push(List::::horizontal() 41 | .push(num_button("1")) 42 | .push(num_button("2")) 43 | .push(num_button("3")) 44 | .push(Button::::default_state() 45 | .containing(Label::::with_state("+").padded(16.0)) 46 | .on(Click, |ctx| ctx.data.start_op(|x, y| x + y)) 47 | .padded(8.0))) 48 | .push(List::::horizontal() 49 | .push(num_button("4")) 50 | .push(num_button("5")) 51 | .push(num_button("6")) 52 | .push(Button::::default_state() 53 | .containing(Label::::with_state("-").padded(16.0)) 54 | .on(Click, |ctx| ctx.data.start_op(|x, y| x - y)) 55 | .padded(8.0))) 56 | .push(List::::horizontal() 57 | .push(num_button("7")) 58 | .push(num_button("8")) 59 | .push(num_button("9")) 60 | .push(Button::::default_state() 61 | .containing(Label::::with_state("*").padded(16.0)) 62 | .on(Click, |ctx| ctx.data.start_op(|x, y| x * y)) 63 | .padded(8.0))) 64 | .push(List::::horizontal() 65 | .push(Button::::default_state() 66 | .containing(Label::::with_state("C").padded(16.0)) 67 | .on(Click, |ctx| ctx.data.screen.clear()) 68 | .padded(8.0)) 69 | .push(num_button("0")) 70 | .push(Button::::default_state() 71 | .containing(Label::::with_state("=").padded(16.0)) 72 | .on(Click, |ctx| ctx.data.calc()) 73 | .padded(8.0)) 74 | .push(Button::::default_state() 75 | .containing(Label::::with_state("/").padded(16.0)) 76 | .on(Click, |ctx| ctx.data.start_op(|x, y| x / y)) 77 | .padded(8.0))) 78 | .padded(8.0); 79 | 80 | Window::new(ui).run(Data::default()) 81 | } 82 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | use gui::{ 2 | widget::{Toggle, Button, Label, List}, 3 | layout::Direction, 4 | event::Click, 5 | Window, Widget, 6 | }; 7 | 8 | fn main() { 9 | struct Data { 10 | counter: i32, 11 | } 12 | 13 | let ui = List::::vertical() 14 | .push(Button::::default_state() 15 | .containing(Label::::gen_state(|d| format!("{}", d.counter)) 16 | .padded(16.0)) 17 | .on(Click, |ctx| ctx.data.counter = 0) 18 | .padded(8.0)) 19 | .push(List::::horizontal() 20 | .push(Button::::default_state() 21 | .containing(Label::::with_state("+").padded(16.0)) 22 | .on(Click, |ctx| ctx.data.counter += 1) 23 | .padded(8.0)) 24 | .push(Button::::default_state() 25 | .containing(Label::::with_state("-").padded(16.0)) 26 | .on(Click, |ctx| ctx.data.counter -= 1) 27 | .padded(8.0))) 28 | .padded(8.0); 29 | 30 | Window::new(ui) 31 | .run(Data { 32 | counter: 0, 33 | }) 34 | } 35 | 36 | /* 37 | Label::new("Hello, world!") 38 | 39 | Label::bind(|state| &mut state.my_text) 40 | 41 | TextEntry::bind(|state| &mut state.my_text) 42 | 43 | VBox::new() 44 | .push(Toggle::new(Label::new("Hello!"))) 45 | .push(Label::new("World!")) 46 | 47 | VBox::bind(|state| &mut state.entries) 48 | 49 | Button::new(Label::new("Click me!")) 50 | .on::(|_| println!("Hello, world!")) 51 | */ 52 | -------------------------------------------------------------------------------- /examples/test.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | any::{Any, TypeId}, 4 | }; 5 | 6 | pub trait Widget: Default { 7 | type State; 8 | 9 | fn new>(state: S) -> Element { 10 | Element { 11 | state: State::Inner(state.into()), 12 | widget: Default::default(), 13 | handlers: HashMap::default(), 14 | } 15 | } 16 | 17 | fn bind FnMut(&'a mut D) -> &'a mut Self::State + 'static>(f: F) -> Element { 18 | Element { 19 | state: State::Map(Box::new(f)), 20 | widget: Default::default(), 21 | handlers: HashMap::default(), 22 | } 23 | } 24 | } 25 | 26 | enum State { 27 | Map(Box FnMut(&'a mut D) -> &'a mut S>), 28 | Inner(S), 29 | } 30 | 31 | impl State { 32 | fn get_mut<'a, 'b: 'a>(&'b mut self, data: &'a mut D) -> &'a mut S { 33 | match self { 34 | State::Map(f) => f(data), 35 | State::Inner(s) => s, 36 | } 37 | } 38 | } 39 | 40 | pub struct Element { 41 | state: State, 42 | widget: W, 43 | handlers: HashMap>, 44 | } 45 | 46 | impl Element { 47 | pub fn on, F: for<'a> FnMut(&'a mut D) + 'static>(mut self, _: E, f: F) -> Self { 48 | self.handlers.insert(TypeId::of::(), Box::new(f)); 49 | self 50 | } 51 | } 52 | 53 | pub trait Event: Any {} 54 | 55 | // Switch 56 | 57 | #[derive(Default)] 58 | pub struct Switch; 59 | 60 | impl Widget for Switch { 61 | type State = bool; 62 | } 63 | 64 | // Events 65 | 66 | pub struct Click; 67 | 68 | impl Event for Click {} 69 | 70 | // Window 71 | 72 | pub struct Window { 73 | root: Element, 74 | } 75 | 76 | impl Window { 77 | pub fn new(root: Element) -> Self { 78 | Self { root } 79 | } 80 | 81 | pub fn run(self, data: D) { 82 | // TODO 83 | } 84 | } 85 | 86 | fn main() { 87 | struct Data { 88 | enabled: bool, 89 | } 90 | 91 | let switch = Switch::bind::(|d| &mut d.enabled) 92 | .on(Click, |_| println!("Hello, world!")); 93 | 94 | let switch2 = Switch::new(false); 95 | 96 | let list = List::new([ 97 | switch, 98 | switch2, 99 | ]); 100 | 101 | Window::new(switch) 102 | .run(Data { 103 | enabled: true, 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /examples/test2.rs: -------------------------------------------------------------------------------- 1 | pub trait Widget { 2 | fn on(mut self, f: impl FnMut(E, EventCtx) + 'static) -> Self 3 | where Self: Sized + Handler 4 | { 5 | self.attach(f); 6 | self 7 | } 8 | 9 | fn finish<'a>(self) -> Element<'a, D> where Self: Sized + 'a { 10 | Element { 11 | widget: Box::new(self), 12 | } 13 | } 14 | } 15 | 16 | pub struct Element<'a, D> { 17 | widget: Box + 'a>, 18 | } 19 | 20 | // State 21 | 22 | enum State { 23 | Map(Box FnMut(&'a mut D) -> &'a mut S>), 24 | Inner(S), 25 | } 26 | 27 | impl State { 28 | fn get_mut<'a, 'b: 'a>(&'b mut self, data: &'a mut D) -> &'a mut S { 29 | match self { 30 | State::Map(f) => f(data), 31 | State::Inner(s) => s, 32 | } 33 | } 34 | } 35 | 36 | // Events 37 | 38 | pub trait Handler { 39 | fn attach(&mut self, f: impl FnMut(E, EventCtx) + 'static); 40 | } 41 | 42 | pub struct EventCtx<'a, D, W> { 43 | pub widget: &'a mut W, 44 | pub data: &'a mut D, 45 | } 46 | 47 | pub trait HandlerFn = FnMut(E, EventCtx); 48 | 49 | pub type Handle = Option)>>; 50 | 51 | pub struct Click; 52 | 53 | // Toggle 54 | 55 | pub struct Toggle { 56 | state: State, 57 | on_click: Handle, 58 | } 59 | 60 | impl Toggle { 61 | pub fn new(state: impl Into) -> Self { 62 | Self { 63 | state: State::Inner(state.into()), 64 | on_click: None, 65 | } 66 | } 67 | 68 | pub fn bind(f: impl for<'a> FnMut(&'a mut D) -> &'a mut bool + 'static) -> Self { 69 | Self { 70 | state: State::Map(Box::new(f)), 71 | on_click: None, 72 | } 73 | } 74 | } 75 | 76 | impl Widget for Toggle {} 77 | 78 | impl Handler for Toggle { 79 | fn attach(&mut self, f: impl FnMut(Click, EventCtx) + 'static) { 80 | self.on_click = Some(Box::new(f)); 81 | } 82 | } 83 | 84 | // Label 85 | 86 | pub struct Label { 87 | state: State, 88 | } 89 | 90 | impl Label { 91 | pub fn new(state: impl Into) -> Self { 92 | Self { 93 | state: State::Inner(state.into()), 94 | } 95 | } 96 | 97 | pub fn bind(f: impl for<'a> FnMut(&'a mut D) -> &'a mut String + 'static) -> Self { 98 | Self { 99 | state: State::Map(Box::new(f)), 100 | } 101 | } 102 | } 103 | 104 | impl Widget for Label {} 105 | 106 | fn main() { 107 | struct Data { 108 | enabled: bool, 109 | } 110 | 111 | Label::::new("Hello, world!") 112 | .finish(); 113 | 114 | Toggle::::new(false) 115 | .finish(); 116 | 117 | Toggle::::bind(|s| &mut s.enabled) 118 | .on(|Click, _| println!("Toggled!")) 119 | .finish(); 120 | } 121 | -------------------------------------------------------------------------------- /misc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zesterer/gui/76665066dee270db35d7147c9a0aac7a64e51b17/misc/example.png -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Fill, 3 | layout::Bounds, 4 | canvas::{Primitive, Canvas}, 5 | Widget, Event, Element, MouseButton, Response, 6 | }; 7 | use std::{ 8 | convert::{TryFrom, TryInto}, 9 | ops::Deref, 10 | }; 11 | use cairo::{Format, ImageSurface, Context}; 12 | use piet::{kurbo::{Rect, Line}, RenderContext, Text, TextLayoutBuilder, TextAttribute}; 13 | use piet_cairo::CairoRenderContext; 14 | 15 | pub struct Window<'a, D> { 16 | win: minifb::Window, 17 | element: Element<'a, D>, 18 | font_data: Vec, 19 | } 20 | 21 | impl<'a, D> Window<'a, D> { 22 | pub fn new(root: impl Widget<'a, D> + 'a) -> Self { 23 | Self { 24 | win: minifb::Window::new( 25 | "Test", 26 | 640, 27 | 480, 28 | minifb::WindowOptions { 29 | resize: true, 30 | ..Default::default() 31 | }, 32 | ).unwrap(), 33 | element: root.finish(), 34 | font_data: include_bytes!("../../data/OpenSans-Regular.ttf").to_vec(), 35 | } 36 | } 37 | 38 | pub fn run(mut self, mut data: D) { 39 | let mut last_mouse_pos = None; 40 | let mut last_size = (0, 0); 41 | let mut mouse_down = false; 42 | 43 | //let mut buf = Vec::new(); 44 | let mut surf = ImageSurface::create(Format::ARgb32, self.win.get_size().0 as i32, self.win.get_size().1 as i32).unwrap(); 45 | let mut maybe_font = None; 46 | let mut redraw = true; 47 | 48 | while self.win.is_open() { 49 | let (w, h) = self.win.get_size(); 50 | 51 | // Collect events 52 | let mut events = Vec::new(); 53 | let mouse_pos = self.win.get_mouse_pos(minifb::MouseMode::Pass); 54 | if mouse_pos != last_mouse_pos { 55 | mouse_pos.map(|mouse_pos| events.push(Event::CursorMove([mouse_pos.0, mouse_pos.1]))); 56 | last_mouse_pos = mouse_pos; 57 | } 58 | mouse_down = if self.win.get_mouse_down(minifb::MouseButton::Left) { 59 | if !mouse_down { 60 | if let Some(mouse_pos) = mouse_pos { 61 | events.push(Event::Click([mouse_pos.0, mouse_pos.1], MouseButton::Left)); 62 | } 63 | } 64 | true 65 | } else { 66 | false 67 | }; 68 | 69 | if last_size != self.win.get_size() { 70 | last_size = self.win.get_size(); 71 | surf = ImageSurface::create(Format::ARgb32, self.win.get_size().0 as i32, self.win.get_size().1 as i32).unwrap(); 72 | redraw = true; 73 | self.element.get_layout_req(); 74 | self.element.fit_bounds(Bounds::global([w as f32, h as f32])); 75 | } 76 | 77 | for event in events { 78 | let mut resp = Response { 79 | redraw: false, 80 | }; 81 | self.element.handle(&mut data, &event, &mut resp); 82 | redraw |= resp.redraw; 83 | } 84 | 85 | if redraw { 86 | //buf.resize_with(w * h * 4, || 0); 87 | 88 | // Collect widget primitives 89 | let mut prim_canvas = Canvas::default(); 90 | self.element.draw(&mut data, &mut prim_canvas); 91 | 92 | // Draw primitives 93 | //let mut canvas = andrew::Canvas::new(&mut buf, w, h, w * 4, andrew::Endian::native()); 94 | //canvas.pixel_size = 4; 95 | 96 | { 97 | let cx = Context::new(&surf).unwrap(); 98 | let mut rcx = CairoRenderContext::new(&cx); 99 | rcx.clear(None, piet::Color::grey(0.5)); 100 | 101 | for prim in prim_canvas.primitives { 102 | match prim { 103 | Primitive::Rect { rect, fill } => { 104 | let brush = match fill { 105 | Fill::Color(col) => rcx.solid_brush(piet::Color::rgba8(col.r, col.g, col.b, col.a)), 106 | }; 107 | rcx.fill(Rect::new(rect.x as f64, rect.y as f64, (rect.x + rect.w) as f64, (rect.y + rect.h) as f64), &brush); 108 | 109 | // canvas 110 | // .draw(&andrew::shapes::rectangle::Rectangle::new( 111 | // rect.position().map(|e| e as usize).into_tuple(), 112 | // rect.extent().map(|e| e as usize).into_tuple(), 113 | // None, 114 | // Some(match fill { 115 | // Fill::Color(col) => [col.a, col.r, col.g, col.b], 116 | // }), 117 | // )) 118 | }, 119 | Primitive::Line { line, stroke } => { 120 | let brush = match stroke.fill { 121 | Fill::Color(col) => rcx.solid_brush(piet::Color::rgba8(col.r, col.g, col.b, col.a)), 122 | }; 123 | rcx.stroke(Line::new( 124 | line.start.map(|e| e as f64).into_tuple(), 125 | line.end.map(|e| e as f64).into_tuple(), 126 | ), &brush, stroke.width as f64); 127 | // canvas 128 | // .draw(&andrew::line::Line::new( 129 | // line.start.map(|e| e as usize).into_tuple(), 130 | // line.end.map(|e| e as usize).into_tuple(), 131 | // match stroke.fill { 132 | // Fill::Color(col) => [col.a, col.r, col.g, col.b], 133 | // }, 134 | // true, 135 | // )) 136 | }, 137 | Primitive::Text { pos, text, height, col } => { 138 | let mut text_state = rcx.text(); 139 | 140 | let font = maybe_font.take().unwrap_or_else(|| { 141 | text_state.font_family("Open Sans").unwrap() 142 | }); 143 | 144 | let layout = text_state 145 | .new_text_layout(text) 146 | .text_color(piet::Color::rgba8(col.r, col.g, col.b, col.a)) 147 | .default_attribute(TextAttribute::FontSize(16.0)) 148 | .build() 149 | .unwrap(); 150 | 151 | rcx.draw_text(&layout, pos.map(|e| e as f64).into_tuple()); 152 | 153 | maybe_font = Some(font); 154 | 155 | // canvas 156 | // .draw(&andrew::text::Text::new( 157 | // pos.map(|e| e as usize).into_tuple(), 158 | // [col.a, col.r, col.g, col.b], 159 | // &self.font_data, 160 | // height, 161 | // 1.0, 162 | // text, 163 | // )) 164 | }, 165 | } 166 | } 167 | } 168 | 169 | self.win.update_with_buffer(unsafe { std::mem::transmute(surf.data().unwrap().deref()) }, w, h).unwrap(); 170 | 171 | redraw = false; 172 | } else { 173 | self.win.update(); 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/canvas.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::{Fill, Stroke, Color}, 3 | layout::Bounds, 4 | }; 5 | use vek::*; 6 | 7 | pub enum Primitive { 8 | Rect { rect: Rect, fill: Fill }, 9 | Line { line: LineSegment2, stroke: Stroke }, 10 | Text { pos: Vec2, text: String, height: f32, col: Color }, 11 | } 12 | 13 | #[derive(Default)] 14 | pub struct Canvas { 15 | pub(crate) primitives: Vec, 16 | } 17 | 18 | impl Canvas { 19 | pub fn bounded<'a>(&'a mut self, bounds: Bounds) -> BoundedCanvas<'a> { 20 | BoundedCanvas { 21 | aabr: Aabr { 22 | min: Vec2::from(bounds.pos()), 23 | max: Vec2::from(bounds.pos()) + Vec2::from(bounds.size()), 24 | }, 25 | canvas: self, 26 | } 27 | } 28 | } 29 | 30 | pub struct BoundedCanvas<'a> { 31 | aabr: Aabr, 32 | canvas: &'a mut Canvas, 33 | } 34 | 35 | impl<'a> BoundedCanvas<'a> { 36 | pub fn size(&self) -> [f32; 2] { self.aabr.size().into_array() } 37 | 38 | pub fn bounds(&self) -> Bounds { 39 | Bounds { rect: self.aabr.into() } 40 | } 41 | 42 | pub fn draw_text(&mut self, pos: [f32; 2], text: impl Into, height: f32, col: impl Into) { 43 | self.canvas.primitives.push(Primitive::Text { 44 | pos: self.aabr.min + Vec2::from(pos), 45 | text: text.into(), 46 | height, 47 | col: col.into(), 48 | }); 49 | } 50 | 51 | pub fn draw_line(&mut self, from: [f32; 2], to: [f32; 2], stroke: impl Into) { 52 | self.canvas.primitives.push(Primitive::Line { 53 | line: LineSegment2 { 54 | start: self.aabr.min + Vec2::from(from), 55 | end: self.aabr.min + Vec2::from(to), 56 | }, 57 | stroke: stroke.into(), 58 | }); 59 | } 60 | 61 | pub fn draw_rect(&mut self, pos: [f32; 2], size: [f32; 2], fill: impl Into) { 62 | self.canvas.primitives.push(Primitive::Rect { 63 | rect: Rect::new( 64 | self.aabr.min.x + pos[0], 65 | self.aabr.min.y + pos[1], 66 | size[0], 67 | size[1], 68 | ), 69 | fill: fill.into(), 70 | }); 71 | } 72 | 73 | pub fn fill(&mut self, fill: impl Into) { 74 | self.draw_rect([0.0; 2], self.size(), fill) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | pub struct Color { 2 | pub r: u8, 3 | pub g: u8, 4 | pub b: u8, 5 | pub a: u8, 6 | } 7 | 8 | impl Color { 9 | pub const BLACK: Self = Color::new(0x0, 0x0, 0x0, 0xFF); 10 | pub const WHITE: Self = Color::new(0xFF, 0xFF, 0xFF, 0xFF); 11 | pub const RED: Self = Color::new(0xFF, 0x0, 0x0, 0xFF); 12 | pub const GREEN: Self = Color::new(0x0, 0xFF, 0x0, 0xFF); 13 | pub const BLUE: Self = Color::new(0x0, 0x0, 0xFF, 0xFF); 14 | pub const YELLOW: Self = Color::new(0xFF, 0xFF, 0x0, 0xFF); 15 | 16 | pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 17 | Self { r, g, b, a } 18 | } 19 | } 20 | 21 | impl From for [u8; 4] { 22 | fn from(color: Color) -> Self { 23 | [color.r, color.g, color.b, color.a] 24 | } 25 | } 26 | 27 | pub enum Fill { 28 | Color(Color), 29 | } 30 | 31 | impl From for Fill { 32 | fn from(color: Color) -> Self { 33 | Fill::Color(color) 34 | } 35 | } 36 | 37 | pub struct Stroke { 38 | pub width: f32, 39 | pub fill: Fill, 40 | // TODO: Join, Hip, etc. 41 | } 42 | 43 | impl> From for Stroke { 44 | fn from(fill: T) -> Self { 45 | Self { 46 | width: 1.0, 47 | fill: fill.into(), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/element.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | canvas::Canvas, 3 | layout::{Bounds, LayoutReq}, 4 | Widget, Event, Response, 5 | }; 6 | use vek::*; 7 | use std::ops::{Deref, DerefMut}; 8 | 9 | pub struct Element<'a, D> { 10 | widget: Box + 'a>, 11 | bounds: Bounds, 12 | last_layout: LayoutReq, 13 | } 14 | 15 | impl<'a, D> Element<'a, D> { 16 | pub(crate) fn from_widget(widget: impl Widget<'a, D> + 'a) -> Self { 17 | Self { 18 | widget: Box::new(widget), 19 | bounds: Bounds::global([0.0; 2]), 20 | last_layout: LayoutReq::any(), 21 | } 22 | } 23 | 24 | pub fn last_layout_req(&self) -> LayoutReq { 25 | self.last_layout 26 | } 27 | 28 | pub(crate) fn get_layout_req(&mut self) -> LayoutReq { 29 | let layout = self.widget.get_layout_req(); 30 | self.last_layout = layout; 31 | layout 32 | } 33 | 34 | pub(crate) fn fit_bounds(&mut self, bounds: Bounds) { 35 | self.bounds = bounds; 36 | self.widget.fit_bounds(bounds); 37 | } 38 | 39 | pub(crate) fn handle( 40 | &mut self, 41 | data: &mut D, 42 | event: &Event, 43 | resp: &mut Response, 44 | ) -> bool { 45 | self.widget.handle(data, event, self.bounds, resp) 46 | } 47 | 48 | pub(crate) fn draw( 49 | &mut self, 50 | data: &mut D, 51 | canvas: &mut Canvas, 52 | ) { 53 | self.widget.draw(data, self.bounds, canvas) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/event/click.rs: -------------------------------------------------------------------------------- 1 | pub struct Click; 2 | -------------------------------------------------------------------------------- /src/event/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod click; 2 | 3 | pub use self::{ 4 | click::Click, 5 | }; 6 | 7 | pub trait Handler<'a, D, E> { 8 | fn attach(&mut self, f: impl FnMut(EventCtx) + 'a); 9 | } 10 | 11 | pub struct EventCtx<'a, D, E, W> { 12 | pub widget: std::marker::PhantomData<(E, W)>,//&'a mut W, 13 | pub data: &'a mut D, 14 | } 15 | 16 | impl<'a, D, E, W> EventCtx<'a, D, E, W> { 17 | pub fn map(self) -> EventCtx<'a, D, E, U> { 18 | EventCtx { 19 | widget: std::marker::PhantomData, 20 | data: self.data, 21 | } 22 | } 23 | } 24 | 25 | pub type Handle<'a, D, E, W> = Option) + 'a>>; 26 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use vek::*; 2 | 3 | #[repr(usize)] 4 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 5 | pub enum Direction { 6 | Horizontal = 0, 7 | Vertical = 1, 8 | } 9 | 10 | #[derive(Copy, Clone)] 11 | pub struct Bounds { 12 | pub(crate) rect: Rect, 13 | } 14 | 15 | impl Bounds { 16 | pub fn global(size: [f32; 2]) -> Self { 17 | Self { 18 | rect: Rect::new(0.0, 0.0, size[0], size[1]), 19 | } 20 | } 21 | 22 | pub fn pos(&self) -> [f32; 2] { 23 | self.rect.position().into_array() 24 | } 25 | 26 | pub fn size(&self) -> [f32; 2] { 27 | self.rect.extent().into_array() 28 | } 29 | 30 | pub fn window(&self, pos: [f32; 2], size: [f32; 2]) -> Self { 31 | let pos = [pos[0].min(self.rect.w), pos[1].min(self.rect.h)]; 32 | Self { 33 | rect: Rect::new( 34 | self.rect.x + pos[0], 35 | self.rect.y + pos[1], 36 | size[0].min(self.rect.w - pos[0]), 37 | size[1].min(self.rect.h - pos[1]), 38 | ), 39 | } 40 | } 41 | 42 | pub fn padded_window(&self, padding: f32) -> Self { 43 | Self { 44 | rect: Rect::new( 45 | self.rect.x + padding.min(self.rect.w / 2.0), 46 | self.rect.y + padding.min(self.rect.h / 2.0), 47 | self.rect.w - padding.min(self.rect.w / 2.0) * 2.0, 48 | self.rect.h - padding.min(self.rect.h / 2.0) * 2.0, 49 | ), 50 | } 51 | } 52 | 53 | pub fn contains(&self, point: impl Into>) -> bool { 54 | self.rect.contains_point(point.into()) 55 | } 56 | } 57 | 58 | #[derive(Copy, Clone, Debug)] 59 | pub struct Span { 60 | pub(crate) min: f32, 61 | max: Option, 62 | } 63 | 64 | impl Span { 65 | pub fn fill() -> Self { 66 | Self::min(0.0) 67 | } 68 | 69 | pub fn min(min: f32) -> Self { 70 | Self { 71 | min, 72 | max: None, 73 | } 74 | } 75 | 76 | pub fn exactly(x: f32) -> Self { 77 | Self { 78 | min: x, 79 | max: Some(x), 80 | } 81 | } 82 | 83 | pub fn zero() -> Self { 84 | Self { 85 | min: 0.0, 86 | max: Some(0.0), 87 | } 88 | } 89 | 90 | pub fn max(self, other: Self) -> Self { 91 | Self { 92 | min: self.min.max(other.min), 93 | max: match (self.max, other.max) { 94 | (Some(s), Some(o)) => Some(s.min(o)), 95 | (s, o) => s.or(o), 96 | }, 97 | } 98 | } 99 | } 100 | 101 | impl std::ops::Add for Span { 102 | type Output = Self; 103 | 104 | fn add(self, other: Self) -> Self { 105 | Self { 106 | min: self.min + other.min, 107 | max: match (self.max, other.max) { 108 | (Some(s), Some(o)) => Some(s + o), 109 | (_, _) => None, 110 | }, 111 | } 112 | } 113 | } 114 | 115 | #[derive(Copy, Clone, Debug)] 116 | pub struct LayoutReq { 117 | span: Extent2, 118 | } 119 | 120 | impl LayoutReq { 121 | pub fn new(span: [Span; 2]) -> Self { 122 | Self { span: span.into() } 123 | } 124 | 125 | pub fn any() -> Self { 126 | Self::new([Span::min(0.0), Span::min(0.0)]) 127 | } 128 | 129 | pub fn padded(self, padding: f32) -> Self { 130 | Self { 131 | span: self.span.map(|e| Span { 132 | min: e.min + padding * 2.0, 133 | max: e.max.map(|max| max + padding * 2.0), 134 | }), 135 | } 136 | } 137 | 138 | pub fn width(&self) -> Span { 139 | self.span.w 140 | } 141 | 142 | pub fn height(&self) -> Span { 143 | self.span.h 144 | } 145 | } 146 | 147 | impl std::ops::Index for LayoutReq { 148 | type Output = Span; 149 | 150 | fn index(&self, index: usize) -> &Self::Output { 151 | &self.span[index] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod backend; 2 | pub mod canvas; 3 | pub mod draw; 4 | pub mod element; 5 | pub mod event; 6 | pub mod layout; 7 | pub mod widget; 8 | pub mod state; 9 | 10 | pub use self::{ 11 | backend::Window, 12 | element::Element, 13 | widget::{Widget, StateWidget}, 14 | state::State, 15 | }; 16 | 17 | pub struct Response { 18 | redraw: bool, 19 | } 20 | 21 | impl Response { 22 | pub fn redraw(&mut self) { 23 | self.redraw = true; 24 | } 25 | } 26 | 27 | pub enum MouseButton { 28 | Left, 29 | Middle, 30 | Right, 31 | } 32 | 33 | pub enum Event { 34 | CursorMove([f32; 2]), 35 | Click([f32; 2], MouseButton), 36 | } 37 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | pub enum State<'a, D, S> { 2 | Bind(Box FnMut(&'b mut D) -> &'b mut S + 'a>), 3 | Generate(S, Box S + 'a>), 4 | Inner(S), 5 | } 6 | 7 | impl<'a, D, S> State<'a, D, S> { 8 | pub fn get_mut<'b, 'c: 'b>(&'c mut self, data: &'b mut D) -> &'b mut S { 9 | match self { 10 | State::Bind(f) => f(data), 11 | State::Generate(s, f) => { 12 | *s = f(data); 13 | s 14 | }, 15 | State::Inner(s) => s, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/widget/button.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Color, 3 | canvas::Canvas, 4 | layout::{Bounds, LayoutReq}, 5 | event::{Handle, Handler, Click, EventCtx}, 6 | Widget, StateWidget, State, Event, Element, Response, 7 | }; 8 | 9 | pub struct Button<'a, D> { 10 | is_hover: bool, 11 | is_pressed: bool, 12 | inner: Option>, 13 | on_click: Handle<'a, D, Click, Self>, 14 | } 15 | 16 | impl<'a, D> Button<'a, D> { 17 | pub fn containing(mut self, inner: impl Widget<'a, D> + 'a) -> Self { 18 | self.inner = Some(inner.finish()); 19 | self 20 | } 21 | } 22 | 23 | impl<'a, D> StateWidget<'a, D, ()> for Button<'a, D> { 24 | fn from_state(_: State) -> Self { 25 | Self { 26 | is_hover: false, 27 | is_pressed: false, 28 | inner: None, 29 | on_click: None, 30 | } 31 | } 32 | } 33 | 34 | impl<'a, D> Widget<'a, D> for Button<'a, D> { 35 | fn handle( 36 | &mut self, 37 | data: &mut D, 38 | event: &Event, 39 | bounds: Bounds, 40 | resp: &mut Response, 41 | ) -> bool { 42 | if self.inner 43 | .as_mut() 44 | .map(|x| x.handle(data, event, resp)) 45 | .unwrap_or(false) 46 | { 47 | true 48 | } else { 49 | if let Event::Click(pos, _) = event { 50 | if bounds.contains(*pos) { 51 | self.on_click.as_mut().map(|f| f(EventCtx { 52 | widget: std::marker::PhantomData,//self, 53 | data, 54 | })); 55 | resp.redraw(); 56 | true 57 | } else { 58 | false 59 | } 60 | } else if let Event::CursorMove(pos) = event { 61 | let old_hover = self.is_hover; 62 | self.is_hover = bounds.contains(*pos); 63 | if old_hover != self.is_hover { 64 | resp.redraw(); 65 | } 66 | false 67 | } else { 68 | false 69 | } 70 | } 71 | } 72 | 73 | fn get_layout_req(&mut self) -> LayoutReq { 74 | self.inner 75 | .as_mut() 76 | .map(|i| i.get_layout_req()) 77 | .unwrap_or(LayoutReq::any()) 78 | } 79 | 80 | fn fit_bounds(&mut self, bounds: Bounds) { 81 | self.inner.as_mut().map(|i| i.fit_bounds(bounds)); 82 | } 83 | 84 | fn draw( 85 | &mut self, 86 | data: &mut D, 87 | bounds: Bounds, 88 | canvas: &mut Canvas, 89 | ) { 90 | canvas.bounded(bounds).fill(if self.is_pressed { 91 | Color::RED 92 | } else if self.is_hover { 93 | Color::new(0xD0, 0xD0, 0xD0, 0xFF) 94 | } else { 95 | Color::WHITE 96 | }); 97 | self.inner 98 | .as_mut() 99 | .map(|x| x.draw(data, canvas)); 100 | } 101 | } 102 | 103 | impl<'a, D> Handler<'a, D, Click> for Button<'a, D> { 104 | fn attach(&mut self, f: impl FnMut(EventCtx) + 'a) { 105 | self.on_click = Some(Box::new(f)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/widget/label.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Color, 3 | canvas::Canvas, 4 | layout::{Bounds, LayoutReq, Span}, 5 | Widget, StateWidget, Event, Response, State, 6 | }; 7 | 8 | pub struct Label<'a, D> { 9 | state: State<'a, D, String>, 10 | } 11 | 12 | impl<'a, D> StateWidget<'a, D, String> for Label<'a, D> { 13 | fn from_state(state: State<'a, D, String>) -> Self { 14 | Self { state } 15 | } 16 | } 17 | 18 | impl<'a, D> Widget<'a, D> for Label<'a, D> { 19 | fn get_layout_req(&mut self) -> LayoutReq { 20 | LayoutReq::new([ 21 | Span::exactly(96.0), 22 | Span::exactly(32.0), 23 | ]) 24 | } 25 | 26 | fn draw( 27 | &mut self, 28 | data: &mut D, 29 | bounds: Bounds, 30 | canvas: &mut Canvas, 31 | ) { 32 | canvas.bounded(bounds).draw_text([0.0; 2], self.state.get_mut(data).clone(), 20.0, Color::BLACK); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/widget/list.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Color, 3 | canvas::Canvas, 4 | layout::{Bounds, Direction, LayoutReq, Span}, 5 | event::{Handle, Handler, Click, EventCtx}, 6 | Widget, State, Event, Element, Response, 7 | }; 8 | 9 | pub struct List<'a, D> { 10 | dir: Direction, 11 | children: Vec>, 12 | } 13 | 14 | impl<'a, D> List<'a, D> { 15 | pub fn horizontal() -> Self { Self::new(Direction::Horizontal) } 16 | pub fn vertical() -> Self { Self::new(Direction::Vertical) } 17 | 18 | pub fn new(dir: Direction) -> Self { 19 | Self { 20 | dir, 21 | children: Vec::new() 22 | } 23 | } 24 | 25 | pub fn push(mut self, child: impl Widget<'a, D> + 'a) -> Self { 26 | self.children.push(child.finish()); 27 | self 28 | } 29 | 30 | fn child_bounds(&self, bounds: Bounds) -> impl Fn(usize) -> Bounds { 31 | let child_count = self.children.len(); 32 | let dir = self.dir; 33 | move |i| if dir == Direction::Horizontal { 34 | bounds.window( 35 | [i as f32 * bounds.size()[0] / child_count as f32, 0.0], 36 | [bounds.size()[0] / child_count as f32, bounds.size()[1]] 37 | ) 38 | } else { 39 | bounds.window( 40 | [0.0, i as f32 * bounds.size()[1] / child_count as f32], 41 | [bounds.size()[0], bounds.size()[1] / child_count as f32] 42 | ) 43 | } 44 | } 45 | } 46 | 47 | impl<'a, D> Widget<'a, D> for List<'a, D> { 48 | fn children(&mut self) -> Box> + '_> { 49 | Box::new(self.children.iter_mut()) 50 | } 51 | 52 | fn handle( 53 | &mut self, 54 | data: &mut D, 55 | event: &Event, 56 | bounds: Bounds, 57 | resp: &mut Response, 58 | ) -> bool { 59 | self.children 60 | .iter_mut() 61 | .enumerate() 62 | .any(|(i, child)| { 63 | child.handle(data, event, resp) 64 | }) 65 | } 66 | 67 | fn get_layout_req(&mut self) -> LayoutReq { 68 | let dir = self.dir; 69 | self.children.iter_mut().fold( 70 | LayoutReq::new([Span::zero(); 2]), 71 | |l, c| { 72 | let c_l = c.get_layout_req(); 73 | LayoutReq::new(if dir == Direction::Horizontal {[ 74 | l.width() + c_l.width(), 75 | l.height().max(c_l.height()), 76 | ]} else {[ 77 | l.width().max(c_l.width()), 78 | l.height() + c_l.height(), 79 | ]}) 80 | }, 81 | ) 82 | } 83 | 84 | fn fit_bounds(&mut self, bounds: Bounds) { 85 | let dir = self.dir; 86 | 87 | let req_space = self.children.iter().map(|child| child.last_layout_req()[dir as usize].min).sum::(); 88 | let spare_space = (bounds.size()[dir as usize] - req_space).max(0.0); 89 | let min_factor = (bounds.size()[dir as usize] / req_space).min(1.0); 90 | 91 | let child_count = self.children.len(); 92 | 93 | let mut offset = 0.0; 94 | self.children 95 | .iter_mut() 96 | .enumerate() 97 | .for_each(|(i, child)| { 98 | let sz = child.last_layout_req()[dir as usize].min * min_factor + spare_space / child_count as f32; 99 | child.fit_bounds(bounds.window( 100 | if dir == Direction::Horizontal { [offset, 0.0] } else { [0.0, offset] }, 101 | if dir == Direction::Horizontal { 102 | [sz, bounds.size()[1]] 103 | } else { 104 | [bounds.size()[0], sz] 105 | }, 106 | )); 107 | offset += sz; 108 | }); 109 | } 110 | 111 | fn draw( 112 | &mut self, 113 | data: &mut D, 114 | bounds: Bounds, 115 | canvas: &mut Canvas, 116 | ) { 117 | self.children 118 | .iter_mut() 119 | .enumerate() 120 | .for_each(|(i, child)| { 121 | child.draw(data, canvas) 122 | }); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod button; 2 | pub mod label; 3 | pub mod list; 4 | pub mod padded; 5 | pub mod toggle; 6 | 7 | pub use self::{ 8 | button::Button, 9 | label::Label, 10 | list::List, 11 | padded::Padded, 12 | toggle::Toggle, 13 | }; 14 | 15 | use crate::{ 16 | event::{EventCtx, Handler}, 17 | element::Element, 18 | layout::{Bounds, LayoutReq}, 19 | canvas::Canvas, 20 | Event, Response, State, 21 | }; 22 | 23 | pub trait Widget<'a, D> { 24 | fn with_state(state: impl Into) -> Self 25 | where Self: Sized + StateWidget<'a, D, S> 26 | { 27 | Self::from_state(State::Inner(state.into())) 28 | } 29 | 30 | fn gen_state(f: impl FnMut(&mut D) -> S + 'a) -> Self 31 | where 32 | Self: Sized + StateWidget<'a, D, S>, 33 | S: Default, 34 | { 35 | Self::from_state(State::Generate(S::default(), Box::new(f))) 36 | } 37 | 38 | fn default_state() -> Self 39 | where 40 | Self: Sized + StateWidget<'a, D, S>, 41 | S: Default, 42 | { 43 | Self::from_state(State::Inner(S::default())) 44 | } 45 | 46 | fn bind_state(f: impl for<'b> FnMut(&'b mut D) -> &'b mut S + 'a) -> Self 47 | where Self: Sized + StateWidget<'a, D, S>, 48 | { 49 | Self::from_state(State::Bind(Box::new(f))) 50 | } 51 | 52 | fn on(mut self, event: E, f: impl FnMut(EventCtx) + 'a) -> Self 53 | where Self: Sized + Handler<'a, D, E> 54 | { 55 | self.attach(f); 56 | self 57 | } 58 | 59 | fn padded(self, padding: f32) -> Padded<'a, D, Self> where Self: Sized { 60 | Padded::new(self, padding) 61 | } 62 | 63 | fn finish(self) -> Element<'a, D> where Self: Sized + 'a { 64 | Element::from_widget(self) 65 | } 66 | 67 | fn get_layout_req(&mut self) -> LayoutReq; 68 | 69 | fn fit_bounds(&mut self, bounds: Bounds) {} 70 | 71 | fn children(&mut self) -> Box> + '_> { 72 | Box::new(std::iter::empty()) 73 | } 74 | 75 | fn handle( 76 | &mut self, 77 | data: &mut D, 78 | event: &Event, 79 | bounds: Bounds, 80 | resp: &mut Response, 81 | ) -> bool { false } 82 | 83 | fn draw( 84 | &mut self, 85 | data: &mut D, 86 | bounds: Bounds, 87 | canvas: &mut Canvas, 88 | ) {} 89 | } 90 | 91 | pub trait StateWidget<'a, D, S>: Widget<'a, D> { 92 | fn from_state(state: State<'a, D, S>) -> Self; 93 | } 94 | -------------------------------------------------------------------------------- /src/widget/padded.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Color, 3 | canvas::Canvas, 4 | layout::{Bounds, LayoutReq}, 5 | event::{Handle, Handler, Click, EventCtx}, 6 | Widget, State, Event, Element, Response, 7 | }; 8 | use std::marker::PhantomData; 9 | 10 | pub struct Padded<'a, D, W: Widget<'a, D>> { 11 | padding: f32, 12 | inner: W, 13 | phantom: PhantomData<&'a D>, 14 | } 15 | 16 | impl<'a, D, W: Widget<'a, D>> Padded<'a, D, W> { 17 | pub fn new(inner: W, padding: f32) -> Self { 18 | Self { 19 | padding, 20 | inner, 21 | phantom: PhantomData, 22 | } 23 | } 24 | } 25 | 26 | impl<'a, D, W: Widget<'a, D>> Widget<'a, D> for Padded<'a, D, W> { 27 | fn handle( 28 | &mut self, 29 | data: &mut D, 30 | event: &Event, 31 | bounds: Bounds, 32 | resp: &mut Response, 33 | ) -> bool { 34 | self.inner.handle(data, event, bounds.padded_window(self.padding), resp) 35 | } 36 | 37 | fn get_layout_req(&mut self) -> LayoutReq { 38 | self.inner 39 | .get_layout_req() 40 | .padded(self.padding) 41 | } 42 | 43 | fn fit_bounds(&mut self, bounds: Bounds) { 44 | self.inner.fit_bounds(bounds.padded_window(self.padding)) 45 | } 46 | 47 | fn draw( 48 | &mut self, 49 | data: &mut D, 50 | bounds: Bounds, 51 | canvas: &mut Canvas, 52 | ) { 53 | self.inner.draw(data, bounds.padded_window(self.padding), canvas) 54 | } 55 | } 56 | 57 | impl<'a, D, E, W: Widget<'a, D> + Handler<'a, D, E>> Handler<'a, D, E> for Padded<'a, D, W> { 58 | fn attach(&mut self, mut f: impl FnMut(EventCtx) + 'a) { 59 | self.inner.attach(move |ctx| f(ctx.map())); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/widget/toggle.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | draw::Color, 3 | canvas::Canvas, 4 | layout::{Bounds, LayoutReq}, 5 | event::{Handle, Handler, Click, EventCtx}, 6 | Widget, StateWidget, State, Event, Element, Response, 7 | }; 8 | 9 | pub struct Toggle<'a, D> { 10 | is_hover: bool, 11 | is_pressed: bool, 12 | state: State<'a, D, bool>, 13 | inner: Option>, 14 | on_click: Handle<'a, D, Click, Self>, 15 | } 16 | 17 | impl<'a, D> Toggle<'a, D> { 18 | pub fn containing(mut self, inner: impl Widget<'a, D> + 'a) -> Self { 19 | self.inner = Some(inner.finish()); 20 | self 21 | } 22 | } 23 | 24 | impl<'a, D> StateWidget<'a, D, bool> for Toggle<'a, D> { 25 | fn from_state(state: State<'a, D, bool>) -> Self { 26 | Self { 27 | is_hover: false, 28 | is_pressed: false, 29 | inner: None, 30 | state, 31 | on_click: None, 32 | } 33 | } 34 | } 35 | 36 | impl<'a, D> Widget<'a, D> for Toggle<'a, D> { 37 | fn children(&mut self) -> Box> + '_> { 38 | Box::new(self.inner.as_mut().into_iter()) 39 | } 40 | 41 | fn handle( 42 | &mut self, 43 | data: &mut D, 44 | event: &Event, 45 | bounds: Bounds, 46 | resp: &mut Response, 47 | ) -> bool { 48 | if self.inner 49 | .as_mut() 50 | .map(|x| x.handle(data, event, resp)) 51 | .unwrap_or(false) 52 | { 53 | true 54 | } else { 55 | if let Event::Click(pos, _) = event { 56 | if bounds.contains(*pos) { 57 | let state = self.state.get_mut(data); 58 | *state ^= true; 59 | self.on_click.as_mut().map(|f| f(EventCtx { 60 | widget: std::marker::PhantomData,//self, 61 | data, 62 | })); 63 | resp.redraw(); 64 | true 65 | } else { 66 | false 67 | } 68 | } else if let Event::CursorMove(pos) = event { 69 | let old_hover = self.is_hover; 70 | self.is_hover = bounds.contains(*pos); 71 | if old_hover != self.is_hover { 72 | resp.redraw(); 73 | } 74 | false 75 | } else { 76 | false 77 | } 78 | } 79 | } 80 | 81 | fn get_layout_req(&mut self) -> LayoutReq { 82 | self.inner 83 | .as_mut() 84 | .map(|i| i.get_layout_req()) 85 | .unwrap_or(LayoutReq::any()) 86 | } 87 | 88 | fn fit_bounds(&mut self, bounds: Bounds) { 89 | self.inner.as_mut().map(|i| i.fit_bounds(bounds)); 90 | } 91 | 92 | fn draw( 93 | &mut self, 94 | data: &mut D, 95 | bounds: Bounds, 96 | canvas: &mut Canvas, 97 | ) { 98 | canvas.bounded(bounds).fill(if self.is_pressed { 99 | Color::RED 100 | } else if self.is_hover { 101 | Color::YELLOW 102 | } else if *self.state.get_mut(data) { 103 | Color::BLUE 104 | } else { 105 | Color::WHITE 106 | }); 107 | self.inner 108 | .as_mut() 109 | .map(|x| x.draw(data, canvas)); 110 | } 111 | } 112 | 113 | impl<'a, D> Handler<'a, D, Click> for Toggle<'a, D> { 114 | fn attach(&mut self, f: impl FnMut(EventCtx) + 'a) { 115 | self.on_click = Some(Box::new(f)); 116 | } 117 | } 118 | --------------------------------------------------------------------------------