├── examples ├── alchemy │ ├── items.ron │ ├── air.png │ ├── earth.png │ ├── fire.png │ ├── steam.png │ ├── water.png │ ├── pressure.png │ ├── recipes.ron │ ├── alchemy.pwss │ └── main.rs ├── bar.png ├── button.9.png ├── input.9.png ├── window.9.png ├── dropdown.9.png ├── button.hover.9.png ├── dropdown.open.9.png ├── input.focused.9.png ├── button.pressed.9.png ├── dropdown.hover.9.png ├── download.pwss ├── tour │ ├── dummy_window.rs │ ├── login_window.rs │ └── main.rs ├── counter.rs └── download.rs ├── .rustfmt.toml ├── .gitignore ├── src ├── style │ ├── default_font.png │ ├── default_font.ttf │ └── tokenize.rs ├── backend │ ├── mod.rs │ ├── wgpu.wgsl │ └── winit.rs ├── prelude.rs ├── graphics.rs ├── widget │ ├── dummy.rs │ ├── spacer.rs │ ├── image.rs │ ├── text.rs │ ├── frame.rs │ ├── progress.rs │ ├── toggle.rs │ ├── button.rs │ ├── row.rs │ ├── column.rs │ ├── slider.rs │ ├── panel.rs │ ├── window.rs │ └── layers.rs ├── macros.rs ├── node │ ├── mod.rs │ └── widget_node.rs ├── event.rs ├── atlas.rs ├── tracker.rs ├── component.rs ├── sandbox.rs ├── cache.rs ├── layout.rs └── bitset.rs ├── Cargo.toml ├── README.md ├── CHANGELOG.md ├── style.md └── declarative-syntax.md /examples/alchemy/items.ron: -------------------------------------------------------------------------------- 1 | [ 2 | "air", 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | max_width = 120 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | .DS_Store -------------------------------------------------------------------------------- /examples/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/bar.png -------------------------------------------------------------------------------- /examples/button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/button.9.png -------------------------------------------------------------------------------- /examples/input.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/input.9.png -------------------------------------------------------------------------------- /examples/window.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/window.9.png -------------------------------------------------------------------------------- /examples/dropdown.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/dropdown.9.png -------------------------------------------------------------------------------- /examples/alchemy/air.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/air.png -------------------------------------------------------------------------------- /examples/alchemy/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/earth.png -------------------------------------------------------------------------------- /examples/alchemy/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/fire.png -------------------------------------------------------------------------------- /examples/alchemy/steam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/steam.png -------------------------------------------------------------------------------- /examples/alchemy/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/water.png -------------------------------------------------------------------------------- /src/style/default_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/src/style/default_font.png -------------------------------------------------------------------------------- /src/style/default_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/src/style/default_font.ttf -------------------------------------------------------------------------------- /examples/button.hover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/button.hover.9.png -------------------------------------------------------------------------------- /examples/dropdown.open.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/dropdown.open.9.png -------------------------------------------------------------------------------- /examples/input.focused.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/input.focused.9.png -------------------------------------------------------------------------------- /examples/alchemy/pressure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/alchemy/pressure.png -------------------------------------------------------------------------------- /examples/button.pressed.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/button.pressed.9.png -------------------------------------------------------------------------------- /examples/dropdown.hover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kurble/pixel-widgets/HEAD/examples/dropdown.hover.9.png -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | /// wgpu-rs based renderer 2 | #[cfg(feature = "wgpu")] 3 | pub mod wgpu; 4 | /// winit event conversion 5 | #[cfg(feature = "winit")] 6 | pub mod winit; 7 | -------------------------------------------------------------------------------- /examples/alchemy/recipes.ron: -------------------------------------------------------------------------------- 1 | #![enable(implicit_some)] 2 | [ 3 | (image: "air.png", name: "Air", unlocked: true), 4 | (image: "earth.png", name: "Earth", unlocked: true), 5 | (image: "water.png", name: "Water", unlocked: true), 6 | (image: "fire.png", name: "Fire", unlocked: true), 7 | (image: "pressure.png", name: "Pressure", unlocked: false, recipe: ("Air", "Air")), 8 | (image: "steam.png", name: "Steam", unlocked: false, recipe: ("Fire", "Water")), 9 | ] -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "winit")] 2 | #[cfg(feature = "wgpu")] 3 | pub use crate::sandbox::Sandbox; 4 | pub use crate::{ 5 | component::{Component, ComponentExt}, 6 | draw::Color, 7 | layout::{Align, Direction, Rectangle, Size}, 8 | node::component_node::{DetectMut, Runtime}, 9 | node::*, 10 | style::{ 11 | builder::{RuleBuilder, StyleBuilder}, 12 | Style, 13 | }, 14 | view, 15 | widget::{prelude::*, Context}, 16 | Ui, 17 | }; 18 | -------------------------------------------------------------------------------- /examples/alchemy/alchemy.pwss: -------------------------------------------------------------------------------- 1 | space { 2 | width: fill(1); 3 | height: fill(1); 4 | background: #111; 5 | } 6 | 7 | * .game { 8 | width: fill(1); 9 | height: fill(1); 10 | background: #000; 11 | } 12 | 13 | * .game > * { 14 | margin: 16; 15 | } 16 | 17 | * .game > layers { 18 | width: fill(1); 19 | height: fill(1); 20 | } 21 | 22 | drop:hover { 23 | background: #222; 24 | } 25 | 26 | image { 27 | width: 64; 28 | height: 64; 29 | margin: 16; 30 | } -------------------------------------------------------------------------------- /examples/download.pwss: -------------------------------------------------------------------------------- 1 | column { 2 | width: fill(1); 3 | height: fill(1); 4 | padding: 2; 5 | align-horizontal: center; 6 | } 7 | 8 | input { 9 | width: fill(1); 10 | background: #111; 11 | color: #b0b; 12 | padding: 3; 13 | } 14 | 15 | button { 16 | padding: 5 20; 17 | margin: 5 0; 18 | background: #404; 19 | } 20 | 21 | button:hover { 22 | background: #808; 23 | } 24 | 25 | progress { 26 | width: 310; 27 | height: 40; 28 | padding: 5; 29 | margin: 5; 30 | background: #111; 31 | clip-bar: true; 32 | } 33 | 34 | progress > bar { 35 | background: "examples/bar.png"; 36 | clip-bar: true; 37 | } -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use anyhow::*; 4 | 5 | use crate::cache::Cache; 6 | use crate::draw::{ImageData, Patch}; 7 | 8 | /// Cloneable image loader 9 | pub struct Graphics { 10 | pub(crate) cache: Arc>, 11 | } 12 | 13 | impl Graphics { 14 | /// Loads an image 15 | pub fn load_image>(&self, bytes: B) -> Result { 16 | let image = image::load_from_memory(bytes.as_ref())?; 17 | let image = self.cache.lock().unwrap().load_image(image.into_rgba8()); 18 | Ok(image) 19 | } 20 | 21 | /// Loads a 9 patch. 22 | pub fn load_patch>(&self, bytes: B) -> Result { 23 | let image = image::load_from_memory(bytes.as_ref())?; 24 | let image = self.cache.lock().unwrap().load_patch(image.into_rgba8()); 25 | Ok(image) 26 | } 27 | } 28 | 29 | impl Clone for Graphics { 30 | fn clone(&self) -> Self { 31 | Self { 32 | cache: self.cache.clone(), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pixel-widgets" 3 | version = "0.10.0" 4 | authors = ["Bram Buurlage "] 5 | edition = "2021" 6 | readme = "README.md" 7 | description = "Component based UI library for graphical rust applications" 8 | license = "MIT" 9 | repository = "https://github.com/Kurble/pixel-widgets" 10 | documentation = "https://docs.rs/pixel-widgets" 11 | keywords = ["gui", "ui", "wgpu", "interface", "widgets"] 12 | categories = ["gui"] 13 | resolver = "2" 14 | 15 | [features] 16 | default = ["clipboard", "winit", "wgpu"] 17 | 18 | [dependencies] 19 | image = "0.23" 20 | smallvec = "1" 21 | zerocopy = "0.3" 22 | futures = "0.3" 23 | wgpu = { version = "0.12", optional = true } 24 | winit = { version = "0.26", optional = true } 25 | clipboard = { version = "0.5", optional = true } 26 | anyhow = "1" 27 | owning_ref = "0.4" 28 | serde = { version = "1", features = ["derive"] } 29 | serde_json = "1" 30 | 31 | [dev-dependencies] 32 | tokio = { version = "0.2.22", features = ["full"] } 33 | reqwest = "0.10.7" 34 | ron = "0.6.0" 35 | 36 | [package.metadata.docs.rs] 37 | # NOTE: clipboard feature is causing build failures 38 | no-default-features = true 39 | features = ["wgpu", "winit"] 40 | -------------------------------------------------------------------------------- /src/widget/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::draw::Primitive; 2 | use crate::layout::{Rectangle, Size}; 3 | use crate::node::{GenericNode, IntoNode, Node}; 4 | use crate::style::Stylesheet; 5 | use crate::widget::Widget; 6 | 7 | /// Dummy widget that has a custom widget name 8 | pub struct Dummy { 9 | widget: &'static str, 10 | } 11 | 12 | impl Dummy { 13 | /// Construct a new `Dummy` with a widget name 14 | pub fn new(widget: &'static str) -> Self { 15 | Self { widget } 16 | } 17 | 18 | /// Sets the widget name for this dummy 19 | pub fn widget(mut self, widget: &'static str) -> Self { 20 | self.widget = widget; 21 | self 22 | } 23 | } 24 | 25 | impl Default for Dummy { 26 | fn default() -> Self { 27 | Dummy { widget: "" } 28 | } 29 | } 30 | 31 | impl<'a, T: 'a> Widget<'a, T> for Dummy { 32 | type State = (); 33 | 34 | fn mount(&self) {} 35 | 36 | fn widget(&self) -> &'static str { 37 | self.widget 38 | } 39 | 40 | fn len(&self) -> usize { 41 | 0 42 | } 43 | 44 | fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {} 45 | 46 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 47 | (style.width, style.height) 48 | } 49 | 50 | fn draw(&mut self, _: &mut (), layout: Rectangle, _: Rectangle, style: &Stylesheet) -> Vec> { 51 | style.background.render(layout).into_iter().collect() 52 | } 53 | } 54 | 55 | impl<'a, T: 'a> IntoNode<'a, T> for Dummy { 56 | fn into_node(self) -> Node<'a, T> { 57 | Node::from_widget(self) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/widget/spacer.rs: -------------------------------------------------------------------------------- 1 | use crate::draw::*; 2 | use crate::layout::{Rectangle, Size}; 3 | use crate::node::{IntoNode, Node}; 4 | use crate::style::Stylesheet; 5 | use crate::widget::*; 6 | 7 | /// Empty widget. Default size is (fill(1), fill(1)). 8 | #[derive(Default)] 9 | pub struct Spacer; 10 | 11 | impl<'a, T> Widget<'a, T> for Spacer { 12 | type State = (); 13 | 14 | fn mount(&self) {} 15 | 16 | fn widget(&self) -> &'static str { 17 | "spacer" 18 | } 19 | 20 | fn len(&self) -> usize { 21 | 0 22 | } 23 | 24 | fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {} 25 | 26 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 27 | style.background.resolve_size( 28 | (style.width, style.height), 29 | (Size::Exact(0.0), Size::Exact(0.0)), 30 | style.padding, 31 | ) 32 | } 33 | 34 | fn hit( 35 | &self, 36 | _state: &Self::State, 37 | layout: Rectangle, 38 | clip: Rectangle, 39 | style: &Stylesheet, 40 | x: f32, 41 | y: f32, 42 | _recursive: bool, 43 | ) -> bool { 44 | layout.point_inside(x, y) && clip.point_inside(x, y) && style.background.is_solid() 45 | } 46 | 47 | fn draw(&mut self, _: &mut (), layout: Rectangle, _clip: Rectangle, style: &Stylesheet) -> Vec> { 48 | style.background.render(layout).into_iter().collect() 49 | } 50 | } 51 | 52 | impl<'a, T: 'a> IntoNode<'a, T> for Spacer { 53 | fn into_node(self) -> Node<'a, T> { 54 | Node::from_widget(self) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/tour/dummy_window.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Default)] 4 | pub struct DummyWindow; 5 | 6 | impl Component for DummyWindow { 7 | type State = (); 8 | type Message = Message; 9 | type Output = Message; 10 | 11 | fn mount(&self, _: &mut Runtime) {} 12 | 13 | fn view<'a>(&'a self, _: &'a Self::State) -> Node<'a, Self::Message> { 14 | let options = [ 15 | "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", 16 | ]; 17 | 18 | view! { 19 | Window => { 20 | Row { class: "title" } => { 21 | Text { val: "Dummy window", class: "title" } 22 | Spacer 23 | Spacer { class: "close" } 24 | } 25 | Column => { 26 | Text { val: "Select a planet from the dropdown list: " } 27 | Dropdown { on_select: Message::PlanetSelected } => { 28 | [for &option in options.iter()] 29 | Text { val: option } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | fn style() -> StyleBuilder { 37 | let mut builder = StyleBuilder::default(); 38 | let window_background = builder.load_patch("window.9.png", || { 39 | Ok(ImageReader::open("examples/window.9.png")?.decode()?.into_rgba8()) 40 | }); 41 | builder.rule(RuleBuilder::new("window").background_patch(window_background, Color::white())) 42 | } 43 | 44 | fn update(&self, message: Message, _: DetectMut<()>, _: &mut Runtime, _: &mut Context) { 45 | if let Message::PlanetSelected(planet) = message { 46 | println!("{} selected from the planets", planet); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/counter.rs: -------------------------------------------------------------------------------- 1 | use winit::window::WindowBuilder; 2 | 3 | use pixel_widgets::prelude::*; 4 | 5 | // The main component for our simple application 6 | #[derive(Default)] 7 | struct Counter { 8 | initial_value: i32, 9 | } 10 | 11 | // The message type that will be used in our `Counter` component. 12 | #[derive(Clone)] 13 | enum Message { 14 | UpPressed, 15 | DownPressed, 16 | } 17 | 18 | impl Component for Counter { 19 | type State = i32; 20 | type Message = Message; 21 | type Output = (); 22 | 23 | // Creates the state of our component when it's first constructed. 24 | fn mount(&self, _: &mut Runtime) -> Self::State { 25 | self.initial_value 26 | } 27 | 28 | // Generates the widgets for this component, based on the current state. 29 | fn view(&self, state: &i32) -> Node { 30 | // You can build the view using declarative syntax with the view! macro, 31 | // but you can also construct widgets using normal rust code. 32 | view! { 33 | Column => { 34 | Button { text: "Up", on_clicked: Message::UpPressed }, 35 | Text { val: format!("Count: {}", *state) }, 36 | Button { text: "Down", on_clicked: Message::DownPressed }, 37 | } 38 | } 39 | } 40 | 41 | // Updates the component state based on a message. 42 | fn update(&self, message: Message, mut state: DetectMut, _: &mut Runtime, _: &mut Context<()>) { 43 | match message { 44 | Message::UpPressed => *state += 1, 45 | Message::DownPressed => *state -= 1, 46 | } 47 | } 48 | } 49 | 50 | #[tokio::main] 51 | async fn main() { 52 | Sandbox::new( 53 | Counter { initial_value: 15 }, 54 | StyleBuilder::default(), 55 | WindowBuilder::new() 56 | .with_title("Counter") 57 | .with_inner_size(winit::dpi::LogicalSize::new(240, 240)), 58 | ) 59 | .await 60 | .unwrap() 61 | .run() 62 | .await; 63 | } 64 | -------------------------------------------------------------------------------- /src/widget/image.rs: -------------------------------------------------------------------------------- 1 | pub use crate::draw::ImageData; 2 | use crate::draw::Primitive; 3 | use crate::layout::{Rectangle, Size}; 4 | use crate::node::{GenericNode, IntoNode, Node}; 5 | use crate::style::Stylesheet; 6 | use crate::widget::Widget; 7 | use std::marker::PhantomData; 8 | 9 | /// A widget that display an image. 10 | pub struct Image<'a>(*const ImageData, PhantomData<&'a ()>); 11 | 12 | impl<'a> Image<'a> { 13 | /// Sets the image to be displayed. 14 | pub fn image(mut self, image: &'a ImageData) -> Self { 15 | self.0 = image as _; 16 | self 17 | } 18 | 19 | fn content(&self) -> &ImageData { 20 | unsafe { self.0.as_ref().expect("image of `Image` must be set") } 21 | } 22 | } 23 | 24 | impl<'a> Default for Image<'a> { 25 | fn default() -> Self { 26 | Self(std::ptr::null(), PhantomData) 27 | } 28 | } 29 | 30 | unsafe impl<'a> Send for Image<'a> {} 31 | 32 | impl<'a, T: 'a> Widget<'a, T> for Image<'a> { 33 | type State = (); 34 | 35 | fn mount(&self) {} 36 | 37 | fn widget(&self) -> &'static str { 38 | "image" 39 | } 40 | 41 | fn len(&self) -> usize { 42 | 0 43 | } 44 | 45 | fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {} 46 | 47 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 48 | let width = match style.width { 49 | Size::Shrink => Size::Exact(self.content().size.width()), 50 | other => other, 51 | }; 52 | let height = match style.height { 53 | Size::Shrink => Size::Exact(self.content().size.height()), 54 | other => other, 55 | }; 56 | (width, height) 57 | } 58 | 59 | fn draw(&mut self, _: &mut (), layout: Rectangle, _: Rectangle, style: &Stylesheet) -> Vec> { 60 | vec![Primitive::DrawImage(self.content().clone(), layout, style.color)] 61 | } 62 | } 63 | 64 | impl<'a, T: 'a> IntoNode<'a, T> for Image<'a> { 65 | fn into_node(self) -> Node<'a, T> { 66 | Node::from_widget(self) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/backend/wgpu.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | [[location(0)]] uv: vec2; 3 | [[location(1)]] color: vec4; 4 | [[location(2)]] mode: vec4; 5 | [[builtin(position)]] pos: vec4; 6 | }; 7 | 8 | [[stage(vertex)]] 9 | fn vs_main( 10 | [[location(0)]] a_pos: vec2, 11 | [[location(1)]] a_uv: vec2, 12 | [[location(2)]] a_color: vec4, 13 | [[location(3)]] a_mode: vec4, 14 | ) -> VertexOutput { 15 | var out: VertexOutput; 16 | out.uv = a_uv; 17 | out.color = a_color; 18 | out.mode = a_mode; 19 | out.pos = vec4(a_pos.x, -a_pos.y, 0.0, 1.0); 20 | return out; 21 | } 22 | 23 | [[group(0), binding(0)]] 24 | var u_color_texture: texture_2d; 25 | [[group(0), binding(1)]] 26 | var u_sampler: sampler; 27 | [[group(0), binding(2)]] 28 | var u_linear_sampler: sampler; 29 | 30 | [[stage(fragment)]] 31 | fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { 32 | var tex: vec4 = textureSample(u_color_texture, u_sampler, in.uv); 33 | var font: vec4 = textureSample(u_color_texture, u_linear_sampler, in.uv); 34 | switch (u32(in.mode.x)) { 35 | case 1: { 36 | return in.color; 37 | } 38 | case 2: { 39 | let border = in.mode.z; 40 | 41 | let sd = max(min(font.r, font.g), min(max(font.r, font.g), font.b)); 42 | 43 | let outside_distance = clamp(in.mode.y * (sd - 0.5 + border) + 0.5, 0.0, 1.0); 44 | let inside_distance = clamp(in.mode.y * (sd - 0.5) + 0.5, 0.0, 1.0); 45 | 46 | if (border > 0.0) { 47 | return mix( 48 | vec4(0.0, 0.0, 0.0, outside_distance), 49 | vec4(in.color), 50 | inside_distance 51 | ); 52 | } else { 53 | return vec4(in.color.rgb, in.color.a * inside_distance); 54 | } 55 | } 56 | default: { 57 | return in.color * tex; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/tour/login_window.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Default)] 4 | pub struct LoginWindow; 5 | 6 | pub enum LoginWindowState { 7 | Prompt { name: String, password: String }, 8 | Busy, 9 | } 10 | 11 | impl Component for LoginWindow { 12 | type State = LoginWindowState; 13 | type Message = Message; 14 | type Output = Message; 15 | 16 | fn mount(&self, _: &mut Runtime) -> LoginWindowState { 17 | LoginWindowState::Prompt { 18 | name: "example".to_string(), 19 | password: "password".to_string(), 20 | } 21 | } 22 | 23 | fn view<'a>(&'a self, state: &'a LoginWindowState) -> Node<'a, Message> { 24 | view! { 25 | Window => { 26 | Row { class: "title" } => { 27 | Text { val: "Login window", class: "title" } 28 | Spacer 29 | Spacer { class: "close" } 30 | } 31 | 32 | [match state] 33 | [case LoginWindowState::Prompt { name, password }] 34 | Column => { 35 | Input { 36 | placeholder: "username", 37 | val: name.as_str(), 38 | on_change: Message::NameChanged, 39 | trigger_key: Key::Enter 40 | } 41 | Input { 42 | placeholder: "password", 43 | val: password.as_str(), 44 | on_change: Message::PasswordChanged, 45 | password: true 46 | } 47 | Button { text: "Login", on_clicked: Message::LoginPressed } 48 | } 49 | [case LoginWindowState::Busy] 50 | Column => { 51 | Text { val: "logging in!" } 52 | } 53 | } 54 | } 55 | } 56 | 57 | fn style() -> StyleBuilder { 58 | StyleBuilder::default().rule(RuleBuilder::new("window").background_color(Color::rgb(0.3, 0.3, 0.5))) 59 | } 60 | 61 | fn update( 62 | &self, 63 | message: Message, 64 | mut state: DetectMut, 65 | _: &mut Runtime, 66 | _: &mut Context, 67 | ) { 68 | match message { 69 | Message::NameChanged(new_name) => { 70 | if let LoginWindowState::Prompt { name, .. } = &mut *state { 71 | *name = new_name; 72 | } 73 | } 74 | Message::PasswordChanged(new_password) => { 75 | if let LoginWindowState::Prompt { password, .. } = &mut *state { 76 | *password = new_password; 77 | } 78 | } 79 | Message::LoginPressed => *state = LoginWindowState::Busy, 80 | _ => (), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/widget/text.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::draw::Primitive; 4 | use crate::event::Event; 5 | use crate::layout::{Rectangle, Size}; 6 | use crate::node::{IntoNode, Node}; 7 | use crate::style::Stylesheet; 8 | use crate::text; 9 | use crate::widget::*; 10 | 11 | /// Widget that renders a paragraph of text. 12 | #[derive(Default)] 13 | pub struct Text { 14 | text: String, 15 | } 16 | 17 | impl Text { 18 | /// Constructs a new `Text` 19 | pub fn new>(text: S) -> Self { 20 | Self { text: text.into() } 21 | } 22 | 23 | /// Sets the text value. 24 | pub fn val(mut self, text: impl Into) -> Self { 25 | self.text = text.into(); 26 | self 27 | } 28 | } 29 | 30 | impl<'a, T> Widget<'a, T> for Text { 31 | type State = (); 32 | 33 | fn mount(&self) {} 34 | 35 | fn widget(&self) -> &'static str { 36 | "text" 37 | } 38 | 39 | fn len(&self) -> usize { 40 | 0 41 | } 42 | 43 | fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {} 44 | 45 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 46 | let width = style.width; 47 | let height = style.height; 48 | let text = text::Text { 49 | text: Cow::Borrowed(self.text.as_str()), 50 | font: style.font.clone(), 51 | size: style.text_size, 52 | border: style.text_border, 53 | wrap: style.text_wrap, 54 | color: style.color, 55 | }; 56 | let content = match (width, height) { 57 | (Size::Shrink, Size::Shrink) => { 58 | let measured = text.measure(None); 59 | (Size::Exact(measured.width()), Size::Exact(measured.height())) 60 | } 61 | (Size::Shrink, height) => { 62 | let measured = text.measure(None); 63 | (Size::Exact(measured.width()), height) 64 | } 65 | (Size::Exact(size), Size::Shrink) => { 66 | let measured = text.measure(Some(Rectangle::from_wh(size, std::f32::INFINITY))); 67 | (Size::Exact(size), Size::Exact(measured.height())) 68 | } 69 | (width, height) => (width, height), 70 | }; 71 | style 72 | .background 73 | .resolve_size((style.width, style.height), content, style.padding) 74 | } 75 | 76 | fn event(&mut self, _: &mut (), _: Rectangle, _: Rectangle, _: &Stylesheet, _: Event, _: &mut Context) {} 77 | 78 | fn draw(&mut self, _: &mut (), layout: Rectangle, _: Rectangle, style: &Stylesheet) -> Vec> { 79 | let mut result = Vec::new(); 80 | result.extend(style.background.render(layout)); 81 | result.push(Primitive::DrawText( 82 | text::Text { 83 | text: Cow::Owned(self.text.clone()), 84 | font: style.font.clone(), 85 | size: style.text_size, 86 | border: style.text_border, 87 | wrap: style.text_wrap, 88 | color: style.color, 89 | }, 90 | style.background.content_rect(layout, style.padding), 91 | )); 92 | result 93 | } 94 | } 95 | 96 | impl<'a, T: 'a> IntoNode<'a, T> for Text { 97 | fn into_node(self) -> Node<'a, T> { 98 | Node::from_widget(self) 99 | } 100 | } 101 | 102 | impl<'a, T: 'a, S: 'a + Into> IntoNode<'a, T> for S { 103 | fn into_node(self) -> Node<'a, T> { 104 | Node::from_widget(Text::new(self)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/download.rs: -------------------------------------------------------------------------------- 1 | use futures::SinkExt; 2 | use winit::window::WindowBuilder; 3 | 4 | use pixel_widgets::node::Node; 5 | use pixel_widgets::prelude::*; 6 | 7 | #[derive(Default)] 8 | struct Download; 9 | 10 | struct DownloadState { 11 | pub url: String, 12 | pub progress: usize, 13 | pub size: usize, 14 | } 15 | 16 | #[derive(Clone)] 17 | enum Message { 18 | UrlChanged(String), 19 | DownloadPressed, 20 | DownloadFinished, 21 | ProgressUpdated(usize, usize), 22 | } 23 | 24 | impl Component for Download { 25 | type Message = Message; 26 | type State = DownloadState; 27 | type Output = (); 28 | 29 | fn mount(&self, _: &mut Runtime) -> DownloadState { 30 | DownloadState { 31 | url: "http://speedtest.ftp.otenet.gr/files/test10Mb.db".into(), 32 | progress: 0, 33 | size: 0, 34 | } 35 | } 36 | 37 | fn view<'a>(&'a self, state: &'a Self::State) -> Node<'a, Self::Message> { 38 | view! { 39 | Column => { 40 | Input { 41 | placeholder: "download link", 42 | val: state.url.as_str(), 43 | on_change: Message::UrlChanged, 44 | }, 45 | Button { text: "Download", on_clicked: Message::DownloadPressed }, 46 | Text { val: format!("Downloaded: {} / {} bytes", state.progress, state.size) }, 47 | Progress { val: state.progress as f32 / state.size as f32 }, 48 | } 49 | } 50 | } 51 | 52 | fn update( 53 | &self, 54 | message: Message, 55 | mut state: DetectMut, 56 | runtime: &mut Runtime, 57 | _: &mut Context<()>, 58 | ) { 59 | match message { 60 | Message::UrlChanged(url) => { 61 | state.url = url; 62 | } 63 | Message::DownloadPressed => { 64 | let (mut tx, rx) = futures::channel::mpsc::unbounded(); 65 | let url = state.url.clone(); 66 | runtime.stream(rx); 67 | tokio::spawn(async move { 68 | tx.send(Message::ProgressUpdated(0, 1)).await.unwrap(); 69 | 70 | let mut response = reqwest::get(reqwest::Url::parse(url.as_str()).unwrap()).await.unwrap(); 71 | let mut progress = 0; 72 | let length = response.content_length().unwrap_or(0) as usize; 73 | 74 | tx.send(Message::ProgressUpdated(0, length)).await.unwrap(); 75 | while let Ok(Some(bytes)) = response.chunk().await { 76 | progress += bytes.len(); 77 | tx.send(Message::ProgressUpdated(progress, length)).await.unwrap(); 78 | } 79 | 80 | tx.send(Message::DownloadFinished).await.unwrap(); 81 | }); 82 | } 83 | Message::DownloadFinished => (), 84 | Message::ProgressUpdated(downloaded, size) => { 85 | state.progress = downloaded; 86 | state.size = size; 87 | } 88 | } 89 | } 90 | } 91 | 92 | #[tokio::main] 93 | async fn main() { 94 | let mut sandbox = Sandbox::new( 95 | Download, 96 | StyleBuilder::from_file("examples/download.pwss").unwrap(), 97 | WindowBuilder::new() 98 | .with_title("Downloader") 99 | .with_inner_size(winit::dpi::LogicalSize::new(320, 240)), 100 | ) 101 | .await 102 | .unwrap(); 103 | 104 | tokio::spawn(sandbox.task()); 105 | 106 | sandbox.run().await; 107 | } 108 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[doc = include_str!("../declarative-syntax.md")] 2 | #[macro_export] 3 | macro_rules! view { 4 | { $w1:ident $({$($m1:ident: $v1:expr),* $(,)?})? $(=>$c1:tt)? $(,)? } => { 5 | Option::unwrap(view!{ inner $w1 $({$($m1: $v1),*})? $(=>$c1)? }) 6 | }; 7 | 8 | { 9 | inner $widget:ident 10 | $({$($modifier:ident: $value:expr),*})? 11 | $(=>{$( 12 | $([match $e:expr][case $p:pat])? 13 | $([for $x:pat in $i:expr])? 14 | $([if $(let $y:pat =)? $yc:expr])? 15 | $w1:ident $({$($m1:ident: $v1:expr),*$(,)?})? $(=>$c1:tt)? $(,)? 16 | $([else if $(let $z:pat =)? $zc:expr] $w2:ident $({$($m2:ident: $v2:expr),*$(,)?})? $(=>$c2:tt)? $(,)?)* 17 | $([else] $w3:ident $({$($m3:ident: $v3:expr),*$(,)?})? $(=>$c3:tt)? $(,)?)? 18 | $([case $q:pat] $w4:ident $({$($m4:ident: $v4:expr),*$(,)?})? $(=>$c4:tt)? $(,)?)* 19 | )+})? 20 | } => { 21 | Some($widget::default() 22 | $($(.extend(view!{ 23 | inner 24 | $([match $e][case $p])? 25 | $([for $x in $i])? 26 | $([if $(let $y =)? $yc])? 27 | $w1 $({$($m1: $v1),*})? $(=>$c1)? 28 | $([else if $(let $z =)? $zc] $w2 $({$($m2:$v2),*})? $(=>$c2)?)* 29 | $([else] $w3 $({$($m3:$v3),*})? $(=>$c3)?)? 30 | $([case $q] $w4 $({$($m4:$v4),*})? $(=>$c4)?)* 31 | }))+)? 32 | $($(.$modifier($value))*)? 33 | .into_node() 34 | ) 35 | }; 36 | 37 | { 38 | inner 39 | [for $x:pat in $i:expr] 40 | $widget:ident 41 | $({$($modifier:ident: $value:expr),*})? 42 | $(=>$content:tt)? 43 | } => { 44 | $i.into_iter().flat_map(|$x| view!{ inner $widget $({$($modifier: $value),*})? $(=>$content)? }) 45 | }; 46 | { 47 | inner 48 | [if $(let $x:pat =)? $xc:expr] 49 | $w1:ident 50 | $({$($m1:ident: $v1:expr),*})? 51 | $(=>$c1:tt)? 52 | $([else if $(let $y:pat =)? $yc:expr] 53 | $w2:ident 54 | $({($m2:ident: $v2:expr),*})? 55 | $(=>$c2:tt)?)* 56 | } => { 57 | if $(let $x =)? $xc { 58 | view!{ inner $w1 $({$($m1: $v1),*})? $(=>$c1)?} 59 | } 60 | $(else if $(let $y =)? $yc { 61 | view!{ inner $w2 $({$($m2: $v2),*})? $(=>$c2)?} 62 | })* 63 | else { 64 | None 65 | } 66 | }; 67 | { 68 | inner 69 | [if $(let $x:pat =)? $xc:expr] 70 | $w1:ident 71 | $({$($m1:ident: $v1:expr),*})? 72 | $(=>$c1:tt)? 73 | $([else if $(let $y:pat =)? $yc:expr] 74 | $w2:ident 75 | $({$($m2:ident: $v2:expr),*})? 76 | $(=>$c2:tt)?)* 77 | [else] $w3:ident 78 | $({$($m3:ident: $v3:expr),*})? 79 | $(=>$c3:tt)? 80 | } => { 81 | if $(let $x =)? $xc { 82 | view!{ inner $w1 $({$($m1: $v1),*})? $(=>$c1)? } 83 | } 84 | $(else if $(let $y =)? $yc { 85 | view!{ inner $w2 $({$($m2: $v2),*})? $(=>$c2)? } 86 | })* 87 | else { 88 | view!{ inner $w3 $({$($m3: $v3),*})? $(=>$c3)? } 89 | } 90 | }; 91 | { 92 | inner 93 | [match $e:expr] $([case $p:pat] $w:ident $({$($m:ident: $v:expr),*})? $(=>$c:tt)?)* 94 | } => { 95 | match $e { 96 | $($p => view!{ inner $w $({$($m: $v),*})? $(=>$c)? },)* 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/widget/frame.rs: -------------------------------------------------------------------------------- 1 | use crate::draw::Primitive; 2 | use crate::layout::{Rectangle, Size}; 3 | use crate::node::{IntoNode, Node}; 4 | use crate::style::Stylesheet; 5 | use crate::widget::*; 6 | 7 | /// A widget that wraps around a content widget 8 | pub struct Frame<'a, T> { 9 | content: Option>, 10 | } 11 | 12 | impl<'a, T: 'a> Frame<'a, T> { 13 | /// Construct a new `Frame` with content 14 | pub fn new(content: impl IntoNode<'a, T>) -> Self { 15 | Self { 16 | content: Some(content.into_node()), 17 | } 18 | } 19 | 20 | /// Sets the content widget from the first element of an iterator. 21 | pub fn extend, N: IntoNode<'a, T>>(mut self, iter: I) -> Self { 22 | if self.content.is_none() { 23 | self.content = iter.into_iter().next().map(IntoNode::into_node); 24 | } 25 | self 26 | } 27 | 28 | fn content(&self) -> &Node<'a, T> { 29 | self.content.as_ref().expect("content of `Frame` must be set") 30 | } 31 | 32 | fn content_mut(&mut self) -> &mut Node<'a, T> { 33 | self.content.as_mut().expect("content of `Frame` must be set") 34 | } 35 | } 36 | 37 | impl<'a, T: 'a> Default for Frame<'a, T> { 38 | fn default() -> Self { 39 | Self { content: None } 40 | } 41 | } 42 | 43 | impl<'a, T: 'a> Widget<'a, T> for Frame<'a, T> { 44 | type State = (); 45 | 46 | fn mount(&self) {} 47 | 48 | fn widget(&self) -> &'static str { 49 | "frame" 50 | } 51 | 52 | fn len(&self) -> usize { 53 | 1 54 | } 55 | 56 | fn visit_children(&mut self, visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) { 57 | visitor(&mut **self.content_mut()); 58 | } 59 | 60 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 61 | style 62 | .background 63 | .resolve_size((style.width, style.height), self.content().size(), style.padding) 64 | } 65 | 66 | fn hit( 67 | &self, 68 | _state: &Self::State, 69 | layout: Rectangle, 70 | clip: Rectangle, 71 | style: &Stylesheet, 72 | x: f32, 73 | y: f32, 74 | recursive: bool, 75 | ) -> bool { 76 | if layout.point_inside(x, y) && clip.point_inside(x, y) { 77 | if recursive && !style.background.is_solid() { 78 | self.content().hit( 79 | style.background.content_rect(layout, style.padding), 80 | clip, 81 | x, 82 | y, 83 | recursive, 84 | ) 85 | } else { 86 | true 87 | } 88 | } else { 89 | false 90 | } 91 | } 92 | 93 | fn event( 94 | &mut self, 95 | _: &mut (), 96 | layout: Rectangle, 97 | clip: Rectangle, 98 | style: &Stylesheet, 99 | event: Event, 100 | context: &mut Context, 101 | ) { 102 | self.content_mut().event( 103 | style.background.content_rect(layout, style.padding), 104 | clip, 105 | event, 106 | context, 107 | ); 108 | } 109 | 110 | fn draw(&mut self, _: &mut (), layout: Rectangle, clip: Rectangle, style: &Stylesheet) -> Vec> { 111 | let content_rect = style.background.content_rect(layout, style.padding); 112 | 113 | style 114 | .background 115 | .render(layout) 116 | .into_iter() 117 | .chain(self.content_mut().draw(content_rect, clip).into_iter()) 118 | .collect() 119 | } 120 | } 121 | 122 | impl<'a, T: 'a> IntoNode<'a, T> for Frame<'a, T> { 123 | fn into_node(self) -> Node<'a, T> { 124 | Node::from_widget(self) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/node/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::DefaultHasher; 2 | use std::hash::{Hash, Hasher}; 3 | use std::ops::{Deref, DerefMut}; 4 | 5 | use crate::draw::Primitive; 6 | use crate::event::Event; 7 | use crate::layout::{Rectangle, Size}; 8 | use crate::style::tree::Query; 9 | use crate::tracker::ManagedStateTracker; 10 | use crate::widget::{Context, Widget}; 11 | use crate::Component; 12 | 13 | pub(crate) mod component_node; 14 | pub(crate) mod widget_node; 15 | 16 | /// A node in a user interface element tree. 17 | pub struct Node<'a, Message>(Box + 'a>); 18 | 19 | #[doc(hidden)] 20 | pub trait GenericNode<'a, Message>: Send { 21 | fn get_key(&self) -> u64; 22 | 23 | fn set_key(&mut self, key: u64); 24 | 25 | fn set_class(&mut self, class: &'a str); 26 | 27 | fn acquire_state(&mut self, tracker: &mut ManagedStateTracker<'a>); 28 | 29 | fn size(&self) -> (Size, Size); 30 | 31 | fn hit(&self, layout: Rectangle, clip: Rectangle, x: f32, y: f32, recursive: bool) -> bool; 32 | 33 | fn focused(&self) -> bool; 34 | 35 | fn draw(&mut self, layout: Rectangle, clip: Rectangle) -> Vec>; 36 | 37 | fn style(&mut self, query: &mut Query, position: (usize, usize)); 38 | 39 | fn add_matches(&mut self, query: &mut Query); 40 | 41 | fn remove_matches(&mut self, query: &mut Query); 42 | 43 | fn event(&mut self, layout: Rectangle, clip: Rectangle, event: Event, context: &mut Context); 44 | 45 | fn acquire_waker(&mut self, waker: &std::task::Waker); 46 | 47 | fn poll(&mut self, context: &mut Context, task_context: &mut std::task::Context); 48 | } 49 | 50 | /// Convert widget to a [`Node`](struct.Node.html). 51 | /// All widgets should implement this trait. 52 | /// It is also implemented by [`Node`](struct.Node.html) itself, which simply returns self. 53 | pub trait IntoNode<'a, Message: 'a>: 'a + Sized { 54 | /// Perform the conversion. 55 | fn into_node(self) -> Node<'a, Message>; 56 | 57 | /// Convenience function that converts to a node and then adds a style class to the resulting [`Node`](struct.Node.html). 58 | fn class(self, class: &'a str) -> Node<'a, Message> { 59 | let mut node = self.into_node(); 60 | node.set_class(class); 61 | node 62 | } 63 | 64 | /// Convenience function that converts to a node and then sets a custom id to the resulting [`Node`](struct.Node.html). 65 | fn key(self, key: K) -> Node<'a, Message> { 66 | let mut hasher = DefaultHasher::new(); 67 | key.hash(&mut hasher); 68 | let mut node = self.into_node(); 69 | node.set_key(hasher.finish()); 70 | node 71 | } 72 | } 73 | 74 | impl<'a, Message: 'a> Node<'a, Message> { 75 | /// Create a new [`Node`](struct.Node.html) from a [`Widget`](../widget/trait.Widget.html). 76 | pub fn from_widget>(widget: W) -> Self { 77 | Self(Box::new(widget_node::WidgetNode::new(widget)) as Box<_>) 78 | } 79 | 80 | /// Create a new [`Node`](struct.Node.html) from a [`Component`](../component/trait.Component.html). 81 | pub fn from_component>(component: C) -> Self { 82 | Self(Box::new(component_node::ComponentNode::new(component)) as Box<_>) 83 | } 84 | } 85 | 86 | impl<'a, Message> Deref for Node<'a, Message> { 87 | type Target = dyn GenericNode<'a, Message> + 'a; 88 | 89 | fn deref(&self) -> &Self::Target { 90 | &*self.0 91 | } 92 | } 93 | 94 | impl<'a, Message> DerefMut for Node<'a, Message> { 95 | fn deref_mut(&mut self) -> &mut Self::Target { 96 | &mut *self.0 97 | } 98 | } 99 | 100 | impl<'a, Message: 'a> IntoNode<'a, Message> for Node<'a, Message> { 101 | fn into_node(self) -> Node<'a, Message> { 102 | self 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pixel-widgets 2 | 3 | [![Documentation](https://docs.rs/pixel-widgets/badge.svg)](https://docs.rs/pixel-widgets) 4 | [![Crates.io](https://img.shields.io/crates/v/pixel-widgets.svg)](https://crates.io/crates/pixel-widgets) 5 | ![License](https://img.shields.io/crates/l/pixel-widgets.svg) 6 | 7 | pixel-widgets is a component based user interface library focused on integratability in graphical applications. 8 | 9 | # Features 10 | 11 | - Very compact and easy API 12 | - API agnostic rendering 13 | - [`Component`](component/trait.Component.html) based workflow 14 | - CSS like [styling](style/index.html) 15 | - Many built in [widgets](widget/index.html) 16 | - [wgpu](https://github.com/gfx-rs/wgpu) based renderer included 17 | 18 | # Overview 19 | 20 | User interfaces in pixel-widgets are composed of [`Component`](trait.Component.html)s. These components manage their own state, and generate ui elements when that state is mutated. Each component implements some methods: 21 | 22 | - [`view`](trait.Component.html#tymethod.view) - this method renders the ui elements for the current component state. When the state is updated, the view will be rendered again. 23 | method: 24 | - [`update`](trait.Component.html#tymethod.update) - ui elements generate messages that will be passed to the update method. In here, a component will update it's internal state based on these messages. 25 | 26 | # Quick start 27 | 28 | This example shows how to define a component and run it in the included sandbox. More work is required if you want to use pixel-widgets in your own game engine. 29 | 30 | ```rust 31 | use pixel_widgets::prelude::*; 32 | 33 | // The main component for our simple application 34 | struct Counter { 35 | initial_value: i32, 36 | } 37 | ``` 38 | 39 | Then, we have to define a message type. The message type should be able to tell us what happend in the ui. 40 | 41 | ```rust 42 | // The message type that will be used in our `Counter` component. 43 | #[derive(Clone)] 44 | enum Message { 45 | UpPressed, 46 | DownPressed, 47 | } 48 | ``` 49 | 50 | And finally, we must implement [`Component`](component/trait.Component.html) 51 | 52 | ```rust no_run 53 | use pixel_widgets::prelude::*; 54 | 55 | // The main component for our simple application 56 | #[derive(Default)] 57 | struct Counter { 58 | initial_value: i32, 59 | } 60 | 61 | // The message type that will be used in our `Counter` component. 62 | #[derive(Clone)] 63 | enum Message { 64 | UpPressed, 65 | DownPressed, 66 | } 67 | 68 | impl Component for Counter { 69 | type State = i32; 70 | type Message = Message; 71 | type Output = (); 72 | 73 | // Creates the state of our component when it's first constructed. 74 | fn mount(&self, _: &mut Runtime) -> Self::State { 75 | self.initial_value 76 | } 77 | 78 | // Generates the widgets for this component, based on the current state. 79 | fn view<'a>(&'a self, state: &'a i32) -> Node<'a, Message> { 80 | // You can build the view using declarative syntax with the view! macro, 81 | // but you can also construct widgets using normal rust code. 82 | view! { 83 | Column => { 84 | Button { text: "Up", on_clicked: Message::UpPressed } 85 | Text { val: format!("Count: {}", *state) } 86 | Button { text: "Down", on_clicked: Message::DownPressed } 87 | } 88 | } 89 | } 90 | 91 | // Updates the component state based on a message. 92 | fn update(&self, message: Message, mut state: DetectMut, _: &mut Runtime, _: &mut Context<()>) { 93 | match message { 94 | Message::UpPressed => *state += 1, 95 | Message::DownPressed => *state -= 1, 96 | } 97 | } 98 | } 99 | 100 | #[tokio::main] 101 | async fn main() { 102 | use winit::window::WindowBuilder; 103 | 104 | Sandbox::new( 105 | Counter { initial_value: 15 }, 106 | StyleBuilder::default(), 107 | WindowBuilder::new() 108 | .with_title("Counter") 109 | .with_inner_size(winit::dpi::LogicalSize::new(240, 240)) 110 | ) 111 | .await 112 | .expect("failed to load style") 113 | .run() 114 | .await; 115 | } 116 | ``` 117 | 118 | # Examples 119 | 120 | If you want more examples, check out the [examples directory](https://github.com/Kurble/pixel-widgets/tree/master/examples) in the git repository. 121 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | /// A key 2 | #[allow(missing_docs)] 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | pub enum Key { 5 | LeftMouseButton, 6 | MiddleMouseButton, 7 | RightMouseButton, 8 | 9 | Key1, 10 | Key2, 11 | Key3, 12 | Key4, 13 | Key5, 14 | Key6, 15 | Key7, 16 | Key8, 17 | Key9, 18 | Key0, 19 | 20 | F1, 21 | F2, 22 | F3, 23 | F4, 24 | F5, 25 | F6, 26 | F7, 27 | F8, 28 | F9, 29 | F10, 30 | F11, 31 | F12, 32 | 33 | A, 34 | B, 35 | C, 36 | D, 37 | E, 38 | F, 39 | G, 40 | H, 41 | I, 42 | J, 43 | K, 44 | L, 45 | M, 46 | N, 47 | O, 48 | P, 49 | Q, 50 | R, 51 | S, 52 | T, 53 | U, 54 | V, 55 | W, 56 | X, 57 | Y, 58 | Z, 59 | 60 | Tab, 61 | Shift, 62 | Ctrl, 63 | Alt, 64 | Space, 65 | Enter, 66 | Backspace, 67 | Escape, 68 | Home, 69 | End, 70 | Minus, 71 | Plus, 72 | BracketOpen, 73 | BracketClose, 74 | Comma, 75 | Period, 76 | Semicolon, 77 | Quote, 78 | Tilde, 79 | Backslash, 80 | Slash, 81 | 82 | Left, 83 | Right, 84 | Up, 85 | Down, 86 | } 87 | 88 | /// A set of modifiers 89 | #[derive(Clone, Copy, Debug)] 90 | pub struct Modifiers { 91 | /// `true` if the control key is pressed, `false otherwise. 92 | pub ctrl: bool, 93 | /// `true` if the alt key is pressed, `false otherwise. 94 | pub alt: bool, 95 | /// `true` if the shift key is pressed, `false otherwise. 96 | pub shift: bool, 97 | /// `true` if the windows/super/command key is pressed, `false otherwise. 98 | pub logo: bool, 99 | /// `true` if the primary key combination key is presesed, `false` otherwise. 100 | /// This is command on macos, control on other OS'es. 101 | pub command: bool, 102 | } 103 | 104 | #[allow(missing_docs)] 105 | impl Modifiers { 106 | pub fn none() -> Modifiers { 107 | Modifiers { 108 | ctrl: false, 109 | alt: false, 110 | shift: false, 111 | logo: false, 112 | command: false, 113 | } 114 | } 115 | 116 | pub fn ctrl() -> Modifiers { 117 | Modifiers { 118 | ctrl: true, 119 | alt: false, 120 | shift: false, 121 | logo: false, 122 | #[cfg(target_os = "macos")] 123 | command: false, 124 | #[cfg(not(target_os = "macos"))] 125 | command: true, 126 | } 127 | } 128 | 129 | pub fn alt() -> Modifiers { 130 | Modifiers { 131 | ctrl: false, 132 | alt: true, 133 | shift: false, 134 | logo: false, 135 | command: false, 136 | } 137 | } 138 | 139 | pub fn shift() -> Modifiers { 140 | Modifiers { 141 | ctrl: false, 142 | alt: false, 143 | shift: true, 144 | logo: false, 145 | command: false, 146 | } 147 | } 148 | 149 | pub fn logo() -> Modifiers { 150 | Modifiers { 151 | ctrl: false, 152 | alt: false, 153 | shift: false, 154 | logo: true, 155 | #[cfg(target_os = "macos")] 156 | command: true, 157 | #[cfg(not(target_os = "macos"))] 158 | command: false, 159 | } 160 | } 161 | } 162 | 163 | /// A user input event. 164 | #[derive(Clone, Copy, Debug)] 165 | pub enum Event { 166 | /// A button on some input device was pressed. 167 | Press(Key), 168 | /// A button on some input device was released. 169 | Release(Key), 170 | /// Modifiers were changed. 171 | Modifiers(Modifiers), 172 | /// The window was resized to the given dimensions. 173 | Resize(f32, f32), 174 | /// Some motion input was received (e.g. moving mouse or joystick axis). 175 | Motion(f32, f32), 176 | /// The mouse cursor was moved to a location. 177 | Cursor(f32, f32), 178 | /// The mouse wheel or touchpad scroll gesture sent us some scroll event. 179 | Scroll(f32, f32), 180 | /// Text input was received, usually via the keyboard. 181 | Text(char), 182 | /// The window was focused or lost focus. 183 | Focus(bool), 184 | /// The application exited it's main event loop 185 | Exit, 186 | /// The ui was redrawn, maybe you want to do it again? 187 | Animate, 188 | } 189 | -------------------------------------------------------------------------------- /src/atlas.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Weak; 2 | 3 | pub enum Atlas { 4 | Split(Area, Box<[Atlas; 4]>), 5 | Vacant(Area), 6 | Occupied(Area, T), 7 | } 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub struct Area { 11 | pub left: usize, 12 | pub top: usize, 13 | pub right: usize, 14 | pub bottom: usize, 15 | } 16 | 17 | impl Atlas { 18 | pub fn new(size: usize) -> Self { 19 | Atlas::Vacant(Area { 20 | left: 0, 21 | top: 0, 22 | right: size, 23 | bottom: size, 24 | }) 25 | } 26 | 27 | pub fn size(&self) -> usize { 28 | match self { 29 | Atlas::Split(area, _) | Atlas::Vacant(area) | Atlas::Occupied(area, _) => area.right - area.left, 30 | } 31 | } 32 | 33 | pub fn area(&self) -> Area { 34 | match self { 35 | Atlas::Split(area, _) | Atlas::Vacant(area) | Atlas::Occupied(area, _) => area.clone(), 36 | } 37 | } 38 | 39 | pub fn insert(&mut self, mut val: T, size: usize) -> Result { 40 | let size = size.next_power_of_two(); 41 | if size > self.size() { 42 | return Err(val); 43 | } 44 | 45 | match self { 46 | &mut Atlas::Split(_, ref mut children) => { 47 | if size <= children[0].size() { 48 | for child in children.iter_mut() { 49 | val = match child.insert(val, size) { 50 | Ok(area) => return Ok(area), 51 | Err(val) => val, 52 | }; 53 | } 54 | } 55 | Err(val) 56 | } 57 | 58 | &mut Atlas::Occupied(_, _) => Err(val), 59 | 60 | vacant => { 61 | let area = vacant.area(); 62 | if size < area.right - area.left { 63 | *vacant = Atlas::Split( 64 | area.clone(), 65 | Box::new([ 66 | Atlas::Vacant(Area { 67 | left: area.left, 68 | top: area.top, 69 | right: (area.left + area.right) / 2, 70 | bottom: (area.top + area.bottom) / 2, 71 | }), 72 | Atlas::Vacant(Area { 73 | left: (area.left + area.right) / 2, 74 | top: area.top, 75 | right: area.right, 76 | bottom: (area.top + area.bottom) / 2, 77 | }), 78 | Atlas::Vacant(Area { 79 | left: area.left, 80 | top: (area.top + area.bottom) / 2, 81 | right: (area.left + area.right) / 2, 82 | bottom: area.bottom, 83 | }), 84 | Atlas::Vacant(Area { 85 | left: (area.left + area.right) / 2, 86 | top: (area.top + area.bottom) / 2, 87 | right: area.right, 88 | bottom: area.bottom, 89 | }), 90 | ]), 91 | ); 92 | 93 | vacant.insert(val, size) 94 | } else { 95 | *vacant = Atlas::Occupied(area.clone(), val); 96 | Ok(area) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | impl Atlas> { 104 | pub fn remove_expired(&mut self) -> bool { 105 | let (area, empty) = match self { 106 | Atlas::Split(area, children) => ( 107 | area.clone(), 108 | children 109 | .iter_mut() 110 | .fold(true, |empty, child| child.remove_expired() && empty), 111 | ), 112 | Atlas::Vacant(area) => (area.clone(), true), 113 | Atlas::Occupied(area, content) => { 114 | if content.strong_count() == 0 { 115 | (area.clone(), true) 116 | } else { 117 | (area.clone(), false) 118 | } 119 | } 120 | }; 121 | 122 | if empty { 123 | *self = Atlas::Vacant(area); 124 | true 125 | } else { 126 | false 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/tracker.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | /// An [`Widget`](../widget/trait.Widget.html) state tracker. 4 | pub(crate) struct ManagedState { 5 | state: Vec, 6 | } 7 | 8 | enum Tracked { 9 | Begin { id: u64, state: Box }, 10 | End, 11 | } 12 | 13 | #[doc(hidden)] 14 | pub struct ManagedStateTracker<'a> { 15 | tracker: &'a mut ManagedState, 16 | index: usize, 17 | } 18 | 19 | impl ManagedState { 20 | /// Retrieve a `ManagedStateTracker` that can be used to build a ui. 21 | /// Normally you will call this function at the start of your 22 | /// [`view`](../trait.Component.html#tymethod.view) implementation. 23 | pub fn tracker(&mut self) -> ManagedStateTracker { 24 | ManagedStateTracker { 25 | tracker: self, 26 | index: 0, 27 | } 28 | } 29 | } 30 | 31 | impl Default for ManagedState { 32 | fn default() -> Self { 33 | Self { state: Vec::new() } 34 | } 35 | } 36 | 37 | impl Tracked { 38 | unsafe fn unchecked_mut_ref<'a, T: Any + Send + Sync>(&mut self) -> &'a mut T { 39 | match self { 40 | Tracked::Begin { state, .. } => { 41 | let state = state 42 | .downcast_mut::() 43 | .expect("widgets with the same id must always be of the same type"); 44 | 45 | (state as *mut T).as_mut().unwrap() 46 | } 47 | _ => unreachable!(), 48 | } 49 | } 50 | } 51 | 52 | impl<'a> ManagedStateTracker<'a> { 53 | /// Get a state object for the given id. If such an object doesn't exist yet, it is constructed using the closure. 54 | /// The span of the widget that requests this state object should be closed using [`end`](#method.end). 55 | pub(crate) fn begin<'i, T, F>(&mut self, id: u64, default: F) -> &'i mut T 56 | where 57 | T: Any + Send + Sync, 58 | F: FnOnce() -> T, 59 | { 60 | let search_start = self.index; 61 | let mut level = 0; 62 | 63 | while self.index < self.tracker.state.len() { 64 | match &self.tracker.state[self.index] { 65 | Tracked::End if level > 0 => level -= 1, 66 | Tracked::End => { 67 | // not found, revert to start of local scope 68 | self.index = search_start; 69 | break; 70 | } 71 | &Tracked::Begin { id: tid, state: _ } if level == 0 && tid == id => { 72 | self.tracker.state.splice(search_start..self.index, None); 73 | unsafe { 74 | let i = search_start; 75 | self.index = search_start + 1; 76 | return self.tracker.state[i].unchecked_mut_ref(); 77 | } 78 | } 79 | &Tracked::Begin { .. } => level += 1, 80 | } 81 | self.index += 1; 82 | } 83 | 84 | let i = self.index; 85 | let state = Box::new(default()) as Box; 86 | self.tracker.state.insert(i, Tracked::Begin { id, state }); 87 | self.tracker.state.insert(i + 1, Tracked::End); 88 | self.index += 1; 89 | unsafe { self.tracker.state[i].unchecked_mut_ref() } 90 | } 91 | 92 | /// Ends the span of a widget. 93 | /// Should be called after all of it's children have been handled. 94 | pub(crate) fn end(&mut self) { 95 | let search_start = self.index; 96 | let mut level = 0; 97 | 98 | while self.index < self.tracker.state.len() { 99 | match &self.tracker.state[self.index] { 100 | Tracked::Begin { .. } => { 101 | self.index += 1; 102 | level += 1; 103 | } 104 | Tracked::End if level > 0 => { 105 | self.index += 1; 106 | level -= 1; 107 | } 108 | Tracked::End => { 109 | // found it! remove any widget states that were not matched. 110 | self.tracker.state.splice(search_start..self.index, None); 111 | self.index = search_start + 1; 112 | return; 113 | } 114 | } 115 | } 116 | 117 | unreachable!("did not find `End` at the end."); 118 | } 119 | } 120 | 121 | impl<'a> Drop for ManagedStateTracker<'a> { 122 | fn drop(&mut self) { 123 | while self.index < self.tracker.state.len() { 124 | self.tracker.state.pop(); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/widget/progress.rs: -------------------------------------------------------------------------------- 1 | use crate::draw::Primitive; 2 | use crate::event::Event; 3 | use crate::layout::{Direction, Rectangle, Size}; 4 | use crate::node::{GenericNode, IntoNode, Node}; 5 | use crate::style::Stylesheet; 6 | use crate::widget::{dummy::Dummy, Context, Widget}; 7 | 8 | /// A progress bar that fill up according to some progress 9 | /// The bar part of the progress bar can be styled by selecting the child widget `bar` of the `progress` widget. 10 | /// Progress accepts the `clip-bar` flag for it's style. When the `clip-bar` flag is set, the bar is always rendered 11 | /// at full size and then clipped according to the progress. When `clip-bar` is not set, the bar itself is rendered 12 | /// with a size that matches the progress. 13 | pub struct Progress<'a, T> { 14 | progress: ProgressValue<'a>, 15 | fill: Node<'a, T>, 16 | } 17 | 18 | enum ProgressValue<'a> { 19 | Static(f32), 20 | Dynamic(Box f32>), 21 | } 22 | 23 | impl<'a, T: 'a> Progress<'a, T> { 24 | /// Construct a new `Progress` with a progress value in the range [0.0, 1.0] 25 | pub fn new(progress: f32) -> Self { 26 | Self { 27 | progress: ProgressValue::Static(progress), 28 | fill: Dummy::new("bar").into_node(), 29 | } 30 | } 31 | 32 | /// Sets the progress value, which should be in the range [0.0, 1.0] 33 | pub fn val(mut self, val: f32) -> Self { 34 | self.progress = ProgressValue::Static(val); 35 | self 36 | } 37 | 38 | /// Sets the progress value to be calculated from a function. 39 | /// The function will be called every time the progress is drawn. 40 | /// The returned progress must be in the range [0.0, 1.0] 41 | pub fn val_with(mut self, val: impl 'a + Send + FnMut() -> f32) -> Self { 42 | self.progress = ProgressValue::Dynamic(Box::new(val)); 43 | self 44 | } 45 | } 46 | 47 | impl<'a, T: 'a> Default for Progress<'a, T> { 48 | fn default() -> Self { 49 | Self { 50 | progress: ProgressValue::Static(0.0), 51 | fill: Dummy::new("bar").into_node(), 52 | } 53 | } 54 | } 55 | 56 | impl<'a, T: 'a> Widget<'a, T> for Progress<'a, T> { 57 | type State = (); 58 | 59 | fn mount(&self) {} 60 | 61 | fn widget(&self) -> &'static str { 62 | "progress" 63 | } 64 | 65 | fn len(&self) -> usize { 66 | 1 67 | } 68 | 69 | fn visit_children(&mut self, visitor: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) { 70 | visitor(&mut *self.fill); 71 | } 72 | 73 | fn size(&self, _: &(), style: &Stylesheet) -> (Size, Size) { 74 | (style.width, style.height) 75 | } 76 | 77 | fn hit(&self, _: &Self::State, _: Rectangle, _: Rectangle, _: &Stylesheet, _: f32, _: f32, _: bool) -> bool { 78 | true 79 | } 80 | 81 | fn event(&mut self, _: &mut (), _: Rectangle, _: Rectangle, _: &Stylesheet, _: Event, context: &mut Context) { 82 | if let ProgressValue::Dynamic(_) = self.progress { 83 | context.redraw(); 84 | } 85 | } 86 | 87 | fn draw(&mut self, _: &mut (), layout: Rectangle, clip: Rectangle, style: &Stylesheet) -> Vec> { 88 | let progress = match &mut self.progress { 89 | &mut ProgressValue::Static(value) => value, 90 | ProgressValue::Dynamic(dynamic) => dynamic(), 91 | }; 92 | 93 | let mut result = Vec::new(); 94 | result.extend(style.background.render(layout)); 95 | let fill = layout.after_padding(style.padding); 96 | let fill = match style.direction { 97 | Direction::LeftToRight => Rectangle { 98 | right: fill.left + fill.width() * progress, 99 | ..fill 100 | }, 101 | Direction::RightToLeft => Rectangle { 102 | left: fill.right - fill.width() * progress, 103 | ..fill 104 | }, 105 | Direction::TopToBottom => Rectangle { 106 | bottom: fill.top + fill.height() * progress, 107 | ..fill 108 | }, 109 | Direction::BottomToTop => Rectangle { 110 | top: fill.bottom - fill.height() * progress, 111 | ..fill 112 | }, 113 | }; 114 | 115 | if progress > 0.0 { 116 | if style.contains("clip-bar") { 117 | if let Some(clip) = clip.intersect(&fill) { 118 | result.push(Primitive::PushClip(clip)); 119 | result.extend(self.fill.draw(layout.after_padding(style.padding), clip)); 120 | result.push(Primitive::PopClip); 121 | } 122 | } else { 123 | result.extend(self.fill.draw(fill, clip)); 124 | } 125 | } 126 | 127 | result 128 | } 129 | } 130 | 131 | impl<'a, T: 'a> IntoNode<'a, T> for Progress<'a, T> { 132 | fn into_node(self) -> Node<'a, T> { 133 | Node::from_widget(self) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/component.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::collections::hash_map::DefaultHasher; 3 | use std::hash::{Hash, Hasher}; 4 | 5 | use crate::node::component_node::{DetectMut, Runtime}; 6 | use crate::node::Node; 7 | use crate::style::builder::StyleBuilder; 8 | use crate::widget::Context; 9 | 10 | /// A re-usable component for defining a fragment of a user interface. 11 | /// Components are the main building block for user interfaces in pixel-widgets. 12 | /// 13 | /// The examples in this repository all implement some kind of `Component`, 14 | /// check them out if you just want to read some code. 15 | pub trait Component: Sized { 16 | /// Mutable state associated with this `Component`. 17 | type State: 'static + Any + Send + Sync; 18 | 19 | /// The message type this `Component` will receive from it's view. 20 | type Message: 'static; 21 | 22 | /// The message type this `Component` submits to its parent. 23 | type Output: 'static; 24 | 25 | /// Create a new `State` for the `Component`. 26 | /// This will be called only once when the `Component` is first created. 27 | fn mount(&self, runtime: &mut Runtime) -> Self::State; 28 | 29 | /// Generate the view for the `Component`. 30 | /// This will be called just in time before ui rendering. 31 | /// When the `Component` is updated, 32 | /// the view will be invalidated and the runtime will have to call this function again. 33 | fn view<'a>(&'a self, state: &'a Self::State) -> Node<'a, Self::Message>; 34 | 35 | /// Update the `Component` state in response to the `message`. 36 | /// Asynchronous operations can be submitted to the `context`, 37 | /// which will result in more `update` calls in the future. 38 | /// Messages for the parent `Component` or root can also be submitted through the `context`. 39 | fn update( 40 | &self, 41 | _message: Self::Message, 42 | _state: DetectMut, 43 | _runtime: &mut Runtime, 44 | _context: &mut Context, 45 | ) { 46 | } 47 | 48 | /// Returns a `StyleBuilder` with styling information scoped to this component. 49 | /// This method will be called when you call 50 | /// [`StyleBuilder::component()`](../style/builder/struct.StyleBuilder.html#method.component) 51 | /// when building your style. 52 | fn style() -> StyleBuilder { 53 | StyleBuilder::default() 54 | } 55 | 56 | /// Returns the scope for the styling information of this component returned by [`style()`](#method.style) 57 | fn style_scope() -> &'static str { 58 | std::any::type_name::() 59 | } 60 | 61 | /// Converts the component into a `Node`. This is used by the library to 62 | /// instantiate the component in a user interface. 63 | fn into_node<'a>(self) -> Node<'a, Self::Output> 64 | where 65 | Self: 'a + Sized, 66 | { 67 | Node::from_component(self) 68 | } 69 | 70 | /// Converts the component into a `Node` and sets a style class to it. 71 | fn class<'a>(self, class: &'a str) -> Node<'a, Self::Output> 72 | where 73 | Self: 'a + Sized, 74 | { 75 | let mut node = self.into_node(); 76 | node.set_class(class); 77 | node 78 | } 79 | 80 | /// Converts the component into a `Node` and sets a custom key to it. 81 | fn key<'a, K>(self, key: K) -> Node<'a, Self::Output> 82 | where 83 | Self: 'a + Sized, 84 | K: Hash, 85 | { 86 | let mut hasher = DefaultHasher::new(); 87 | key.hash(&mut hasher); 88 | let mut node = self.into_node(); 89 | node.set_key(hasher.finish()); 90 | node 91 | } 92 | } 93 | 94 | /// Utility methods for components 95 | pub trait ComponentExt: Component + Sized { 96 | /// Maps the output message of a component to a different type. 97 | fn map_message T>(self, map_fn: F) -> MapComponent { 98 | MapComponent { 99 | component: self, 100 | map_fn, 101 | } 102 | } 103 | } 104 | 105 | impl ComponentExt for T {} 106 | 107 | /// The value returned by [`ComponentExt::map_message`](trait.ComponentExt.html#method.map_message). 108 | pub struct MapComponent T> { 109 | component: C, 110 | map_fn: F, 111 | } 112 | 113 | impl T> Component for MapComponent { 114 | type State = C::State; 115 | 116 | type Message = C::Message; 117 | 118 | type Output = T; 119 | 120 | fn mount(&self, runtime: &mut Runtime) -> Self::State { 121 | self.component.mount(runtime) 122 | } 123 | 124 | fn view<'a>(&'a self, state: &'a Self::State) -> Node<'a, Self::Message> { 125 | self.component.view(state) 126 | } 127 | 128 | fn update( 129 | &self, 130 | message: C::Message, 131 | state: DetectMut, 132 | runtime: &mut Runtime, 133 | context: &mut Context, 134 | ) { 135 | let mut sub_context = context.sub_context(); 136 | self.component.update(message, state, runtime, &mut sub_context); 137 | if sub_context.redraw_requested() { 138 | context.redraw(); 139 | } 140 | context.extend(sub_context.into_iter().map(|m| (self.map_fn)(m))); 141 | } 142 | 143 | fn style() -> StyleBuilder { 144 | C::style() 145 | } 146 | 147 | fn style_scope() -> &'static str { 148 | C::style_scope() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/style/tokenize.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | 3 | const URL_CHARACTERS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;%="; 4 | const NUMBER_CHARACTERS: &str = "0123456789."; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct TokenPos { 8 | pub line: usize, 9 | pub col_start: usize, 10 | pub col_end: usize, 11 | } 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub enum TokenValue { 15 | Iden(String), 16 | Color(String), 17 | Path(String), 18 | Number(String), 19 | ParenOpen, 20 | ParenClose, 21 | BraceOpen, 22 | BraceClose, 23 | Colon, 24 | Semi, 25 | Dot, 26 | Comma, 27 | Gt, 28 | Plus, 29 | Tilde, 30 | Star, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct Token(pub TokenValue, pub TokenPos); 35 | 36 | pub fn tokenize(text: String) -> Result, Error> { 37 | let mut line = 1; 38 | let mut col = 0; 39 | 40 | let mut current: Option = None; 41 | let mut tokens = Vec::new(); 42 | 43 | for chr in text.chars() { 44 | col += 1; 45 | 46 | let extended = current 47 | .as_mut() 48 | .map(|current| current.extend(chr)) 49 | .unwrap_or(ExtendResult::NotAccepted); 50 | match extended { 51 | ExtendResult::Accepted => (), 52 | ExtendResult::Finished => { 53 | tokens.extend(current.take()); 54 | } 55 | ExtendResult::NotAccepted => { 56 | tokens.extend(current.take()); 57 | let pos = TokenPos { 58 | col_start: col, 59 | col_end: col, 60 | line, 61 | }; 62 | 63 | current = match chr { 64 | '\n' => { 65 | line += 1; 66 | col = 0; 67 | None 68 | } 69 | chr if chr.is_whitespace() => None, 70 | chr if chr.is_alphabetic() || chr == '_' || chr == '-' => { 71 | Some(Token(TokenValue::Iden(chr.to_string()), pos)) 72 | } 73 | chr if chr.is_numeric() => Some(Token(TokenValue::Number(chr.to_string()), pos)), 74 | '#' => Some(Token(TokenValue::Color(String::new()), pos)), 75 | '"' => Some(Token(TokenValue::Path(String::new()), pos)), 76 | '(' => Some(Token(TokenValue::ParenOpen, pos)), 77 | ')' => Some(Token(TokenValue::ParenClose, pos)), 78 | '{' => Some(Token(TokenValue::BraceOpen, pos)), 79 | '}' => Some(Token(TokenValue::BraceClose, pos)), 80 | ':' => Some(Token(TokenValue::Colon, pos)), 81 | ';' => Some(Token(TokenValue::Semi, pos)), 82 | '.' => Some(Token(TokenValue::Dot, pos)), 83 | ',' => Some(Token(TokenValue::Comma, pos)), 84 | '>' => Some(Token(TokenValue::Gt, pos)), 85 | '+' => Some(Token(TokenValue::Plus, pos)), 86 | '~' => Some(Token(TokenValue::Tilde, pos)), 87 | '*' => Some(Token(TokenValue::Star, pos)), 88 | chr => { 89 | return Err(Error::Syntax(format!("Unexpected character '{}'", chr), pos)); 90 | } 91 | }; 92 | } 93 | } 94 | } 95 | 96 | tokens.extend(current); 97 | 98 | Ok(tokens) 99 | } 100 | 101 | enum ExtendResult { 102 | Accepted, 103 | Finished, 104 | NotAccepted, 105 | } 106 | 107 | impl Token { 108 | fn extend(&mut self, ch: char) -> ExtendResult { 109 | match self { 110 | Token(TokenValue::Iden(ref mut s), ref mut pos) => { 111 | if ch.is_alphanumeric() || ch == '_' || ch == '-' { 112 | pos.col_end += 1; 113 | s.push(ch); 114 | ExtendResult::Accepted 115 | } else { 116 | ExtendResult::NotAccepted 117 | } 118 | } 119 | Token(TokenValue::Color(ref mut p), ref mut pos) => { 120 | if ch.is_ascii_hexdigit() { 121 | pos.col_end += 1; 122 | p.push(ch); 123 | ExtendResult::Accepted 124 | } else { 125 | ExtendResult::NotAccepted 126 | } 127 | } 128 | Token(TokenValue::Path(ref mut p), ref mut pos) => { 129 | if URL_CHARACTERS.chars().any(|c| c == ch) { 130 | pos.col_end += 1; 131 | p.push(ch); 132 | ExtendResult::Accepted 133 | } else if ch == '"' { 134 | ExtendResult::Finished 135 | } else { 136 | ExtendResult::NotAccepted 137 | } 138 | } 139 | Token(TokenValue::Number(ref mut n), ref mut pos) => { 140 | if NUMBER_CHARACTERS.chars().any(|c| c == ch) { 141 | pos.col_end += 1; 142 | n.push(ch); 143 | ExtendResult::Accepted 144 | } else { 145 | ExtendResult::NotAccepted 146 | } 147 | } 148 | _ => ExtendResult::NotAccepted, 149 | } 150 | } 151 | } 152 | 153 | impl std::fmt::Display for TokenPos { 154 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 155 | write!(fmt, "col {}:{} of line {}", self.col_start, self.col_end, self.line) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v0.10.0 4 | 5 | - Added match functionality to the `view!` macro. 6 | - Fixed a bug where the state of the root component was not initialized if update() is called before the first view() call. 7 | - Removed `Unpin` requirement from component `Context::wait` and `Context::stream`. 8 | - Removed `Vec` return values from `Ui`, replaced it with a single `output()` call that returns an iterator. 9 | - Fixed async bugs. As a side effect you will now have to call task() and spawn the resulting future once before starting your ui, and never poll anything again. 10 | 11 | ### v0.9.1 12 | 13 | - `Component` should not have to be `Default`, it's builder should be. The `Default` requirement of `Component` has been removed. 14 | 15 | ### v0.9.0 16 | 17 | - Moved to a completely new `Component` trait that succeeds the `Model` and `UpdateModel` trait, that allows for component based ui development. 18 | - Added a declarative syntax macro for defining views. 19 | - Refactored most widgets to be compatible with declarative syntax. 20 | - Fixed some issues with style specifity not being handled correctly. 21 | - Added a code based style builder that exists along side the file based one. 22 | - Styles are now defined globally for the whole `Ui`. 23 | - Upgraded wgpu backend to version 0.11 24 | 25 | ### v0.8.0 26 | 27 | - Moved `Model::update` to a separate trait `UpdateModel`. 28 | This allows the `UpdateModel::update` to receive a custom state that can be modified during the update. 29 | 30 | ### v0.7.0 31 | 32 | - Upgraded winit backend to version 0.25 33 | - Moved some state management responsibility to the caller: 34 | - Modified `Input` constructor to take a `AsRef` value. It's state no longer has a value of its own. 35 | - Modified `Slider` constructor to take a `f32` value. It's state no longer has a value of its own. 36 | - Added `Dropdown::default_selection`. 37 | 38 | ### v0.6.1 39 | 40 | - Add `Slider` widget. 41 | 42 | ### v0.6.0 43 | 44 | - Upgraded wgpu backend to version 0.8 45 | - Refactored `Vertex::mode` from `u32` to `f32` for simpler branchless shading 46 | - Performing scissor rect validation in `u32` instead of `f32`, 47 | to prevent incorrectly valid scissor rects with a size of 0. 48 | 49 | ### v0.5.10 50 | 51 | - Added widget::input::State::is_focused 52 | - Added Input::with_on_submit 53 | - Added Input::with_trigger_key 54 | 55 | ### v0.5.9 56 | 57 | - Added some more keys to the winit utils 58 | 59 | ### v0.5.8 60 | 61 | - Fixed panic bug with stylesheets that have more than 64 rules 62 | 63 | ### v0.5.7 64 | 65 | - Fixed panic bug with stylesheets that have more than 32 rules 66 | 67 | ### v0.5.6 68 | 69 | - Bump winit to 0.24 70 | 71 | ### v0.5.4 72 | 73 | - Fixed bug where some async tasks were resumed after being finished 74 | 75 | ### v0.5.3 76 | 77 | - Fixed bug where ui wouldn't be redrawn even if needed 78 | 79 | ### v0.5.2 80 | 81 | - Added input::State::get_value and updated the set_value return value. 82 | 83 | ### v0.5.1 84 | 85 | - Fixed a bug with inserting textures in the atlas 86 | 87 | ### v0.5.0 88 | 89 | - More `Loader` flexibility. 90 | - `Style` is responsible for textures instead of `Ui` 91 | 92 | ### v0.4.3 93 | 94 | - Fixed `ManagedState` not working anymore 95 | 96 | ### v0.4.2 (yanked) 97 | 98 | - Made all widgets and `Ui` `Send` compatible 99 | 100 | ### v0.4.1 101 | 102 | - Fixed compilation errors after dependencies that were allowed to updated were updated. 103 | 104 | ### v0.4.0 105 | 106 | - `Model::update` now returns a `Vec>`, which can be used to send async messages. 107 | - `Ui::command` added, which can be used to send an async message externally 108 | - Download example added 109 | - `Ui::reload_stylesheet` added. 110 | - Loader system has been refactored 111 | - Margins added to stylesheet system. Margins automatically handled for all widgets. 112 | - Added `widget::input::State::set_value` 113 | - Removed scrollbars from stylesheet in favor of the new `Dummy` widget. 114 | - Added `Progress` widget. 115 | - Added support for flags to stylesheets. 116 | - Added `Menu` widget. 117 | - Added `on_right_click` callback to `Node`. 118 | - Modified `Widget::state` to return a `SmallVec` of states, to support multiple states at once, 119 | like a `Toggle` than be `checked` and `hover` at the same time. 120 | - Added `Drag` and `Drop` widget. 121 | - The `Layers` widget now propagates events to all layers, except for `Event::Cursor`. 122 | 123 | ### v0.3.0 124 | 125 | - Added `len()` to `Widget`. 126 | - New style system 127 | - Changed pwss syntax to resemble css more. 128 | - Removed some backgrounds from [`Stylesheet`](src/stylesheet/mod.rs). 129 | The styling system is now responsible for specifying these using selectors like `:hover`. 130 | - Added `:nth-first-child(n)`, `:nth-last-child(n)`, `:nth-first-child-mod(n, d)`, 131 | `:nth-last-child-mod(n, d)` selectors. All support numbers, `odd` and `even`. 132 | - Added `:first-child`, `:last-child` and `:only-child` selectors. 133 | - Added `:not()` selector. 134 | - Added a `:` selector that checks the result of the new method `Widget::state()`. 135 | Useful for states such as `hover`, `pressed` or `open`. 136 | - Added `+ `, `> ` and `~ ` selectors. 137 | - The any (`*`) selector can now be used in any place where `` is expected. 138 | 139 | ### v0.2.0 140 | 141 | - Added a sandbox module so you don't need to initialize a window yourself 142 | - Fixed bugs in the [`ManagedStateTracker`](src/tracker.rs) 143 | - Added padding behaviour to `Button`, `Column`, `Dropdown`, `Row`, `Scroll`, `Text` and `Window` widgets 144 | - Added a system for widgets to take exclusive focus 145 | - Added [`Dropdown`](src/widget/dropdown.rs) widget 146 | - Fixed build errors when turning off the features 147 | 148 | ### v0.1.1 149 | 150 | - Fixed docs.rs. build 151 | -------------------------------------------------------------------------------- /src/widget/toggle.rs: -------------------------------------------------------------------------------- 1 | use std::mem::replace; 2 | 3 | use smallvec::smallvec; 4 | 5 | use crate::draw::*; 6 | use crate::event::{Event, Key}; 7 | use crate::layout::{Rectangle, Size}; 8 | use crate::node::{GenericNode, IntoNode, Node}; 9 | use crate::style::{StyleState, Stylesheet}; 10 | use crate::widget::{Context, StateVec, Widget}; 11 | 12 | /// State for [`Toggle`](struct.Toggle.html) 13 | #[allow(missing_docs)] 14 | pub enum State { 15 | Idle, 16 | Hover, 17 | Pressed, 18 | Disabled, 19 | } 20 | 21 | /// A clickable button that toggles some `bool`. 22 | pub struct Toggle T> { 23 | checked: bool, 24 | on_toggle: F, 25 | } 26 | 27 | impl<'a, T: 'a, F: 'a + Fn(bool) -> T> Toggle { 28 | /// Constructs a new `Toggle` 29 | pub fn new + 'a>(checked: bool, on_toggle: F) -> Self { 30 | Self { checked, on_toggle } 31 | } 32 | 33 | /// Sets the current toggle state of the `Toggle`. 34 | pub fn val(mut self, checked: bool) -> Self { 35 | self.checked = checked; 36 | self 37 | } 38 | 39 | /// Sets the on_toggle callback for this `Toggle`, which is called when the toggle state changes. 40 | pub fn on_toggle T>(self, on_toggle: N) -> Toggle { 41 | Toggle { 42 | checked: self.checked, 43 | on_toggle, 44 | } 45 | } 46 | } 47 | 48 | impl<'a, T: 'a> Default for Toggle T> { 49 | fn default() -> Self { 50 | Self { 51 | checked: false, 52 | on_toggle: |_| panic!("on_toggle of `Toggle` must be set"), 53 | } 54 | } 55 | } 56 | 57 | impl<'a, T, F: Send + Fn(bool) -> T> Widget<'a, T> for Toggle { 58 | type State = State; 59 | 60 | fn mount(&self) -> Self::State { 61 | State::Idle 62 | } 63 | 64 | fn widget(&self) -> &'static str { 65 | "toggle" 66 | } 67 | 68 | fn state(&self, state: &State) -> StateVec { 69 | let mut state = match state { 70 | State::Idle => StateVec::new(), 71 | State::Hover => smallvec![StyleState::Hover], 72 | State::Pressed => smallvec![StyleState::Pressed], 73 | State::Disabled => smallvec![StyleState::Disabled], 74 | }; 75 | 76 | if self.checked { 77 | state.push(StyleState::Checked); 78 | } 79 | 80 | state 81 | } 82 | 83 | fn len(&self) -> usize { 84 | 0 85 | } 86 | 87 | fn visit_children(&mut self, _: &mut dyn FnMut(&mut dyn GenericNode<'a, T>)) {} 88 | 89 | fn size(&self, _: &State, stylesheet: &Stylesheet) -> (Size, Size) { 90 | match stylesheet.background { 91 | Background::Patch(ref patch, _) => { 92 | let size = patch.minimum_size(); 93 | (Size::Exact(size.0), Size::Exact(size.1)) 94 | } 95 | Background::Image(ref image, _) => (Size::Exact(image.size.width()), Size::Exact(image.size.height())), 96 | _ => (stylesheet.width, stylesheet.height), 97 | } 98 | } 99 | 100 | fn event( 101 | &mut self, 102 | state: &mut State, 103 | layout: Rectangle, 104 | clip: Rectangle, 105 | _: &Stylesheet, 106 | event: Event, 107 | context: &mut Context, 108 | ) { 109 | match event { 110 | Event::Cursor(x, y) => { 111 | *state = match replace(state, State::Idle) { 112 | State::Idle => { 113 | if layout.point_inside(x, y) && clip.point_inside(x, y) { 114 | context.redraw(); 115 | State::Hover 116 | } else { 117 | State::Idle 118 | } 119 | } 120 | State::Hover => { 121 | if layout.point_inside(x, y) && clip.point_inside(x, y) { 122 | State::Hover 123 | } else { 124 | context.redraw(); 125 | State::Idle 126 | } 127 | } 128 | State::Pressed => { 129 | if layout.point_inside(x, y) && clip.point_inside(x, y) { 130 | State::Pressed 131 | } else { 132 | context.redraw(); 133 | State::Idle 134 | } 135 | } 136 | State::Disabled => State::Disabled, 137 | }; 138 | } 139 | 140 | Event::Press(Key::LeftMouseButton) => { 141 | *state = match replace(state, State::Idle) { 142 | State::Hover => { 143 | context.redraw(); 144 | State::Pressed 145 | } 146 | other => other, 147 | }; 148 | } 149 | 150 | Event::Release(Key::LeftMouseButton) => { 151 | *state = match replace(state, State::Idle) { 152 | State::Pressed => { 153 | context.redraw(); 154 | context.push((self.on_toggle)(!self.checked)); 155 | State::Hover 156 | } 157 | other => other, 158 | }; 159 | } 160 | 161 | _ => (), 162 | } 163 | } 164 | 165 | fn draw(&mut self, _: &mut State, layout: Rectangle, _: Rectangle, stylesheet: &Stylesheet) -> Vec> { 166 | stylesheet.background.render(layout).into_iter().collect() 167 | } 168 | } 169 | 170 | impl<'a, T: 'a + Send, F: 'a + Send + Fn(bool) -> T> IntoNode<'a, T> for Toggle { 171 | fn into_node(self) -> Node<'a, T> { 172 | Node::from_widget(self) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /style.md: -------------------------------------------------------------------------------- 1 | Style in pixel-widgets is either defined in *.pwss* (**p**ixel-**w**idgets **s**tyle**s**heets) files or in code. The file variant uses a format that is syntactically similar to CSS. 2 | 3 | # How to use styles 4 | Styles can be loaded or created when building your [`Ui`](../struct.Ui.html), as [`Ui::new`](../struct.Ui.html#method.new) accepts a `TryInto