From 7cea7371150e6de28032827519936008592f112d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 20 Jan 2020 06:27:01 +0100 Subject: Package examples and remove `dev-dependencies` --- Cargo.toml | 29 +- examples/bezier_tool.rs | 366 ----------------- examples/bezier_tool/Cargo.toml | 12 + examples/bezier_tool/src/main.rs | 366 +++++++++++++++++ examples/counter.rs | 56 --- examples/counter/Cargo.toml | 9 + examples/counter/src/main.rs | 56 +++ examples/custom_widget.rs | 145 ------- examples/custom_widget/Cargo.toml | 11 + examples/custom_widget/src/main.rs | 145 +++++++ examples/events.rs | 86 ---- examples/events/Cargo.toml | 10 + examples/events/src/main.rs | 86 ++++ examples/geometry.rs | 210 ---------- examples/geometry/Cargo.toml | 11 + examples/geometry/src/main.rs | 210 ++++++++++ examples/pokedex.rs | 243 ------------ examples/pokedex/Cargo.toml | 14 + examples/pokedex/src/main.rs | 243 ++++++++++++ examples/progress_bar.rs | 47 --- examples/progress_bar/Cargo.toml | 9 + examples/progress_bar/src/main.rs | 47 +++ examples/resources/ferris.png | Bin 33061 -> 0 bytes examples/resources/icons.ttf | Bin 5596 -> 0 bytes examples/resources/tiger.svg | 725 --------------------------------- examples/stopwatch.rs | 204 ---------- examples/stopwatch/Cargo.toml | 12 + examples/stopwatch/src/main.rs | 206 ++++++++++ examples/styling.rs | 514 ------------------------ examples/styling/Cargo.toml | 9 + examples/styling/src/main.rs | 514 ++++++++++++++++++++++++ examples/svg.rs | 54 --- examples/svg/Cargo.toml | 9 + examples/svg/resources/tiger.svg | 725 +++++++++++++++++++++++++++++++++ examples/svg/src/main.rs | 37 ++ examples/todos.rs | 615 ---------------------------- examples/todos/Cargo.toml | 16 + examples/todos/fonts/icons.ttf | Bin 0 -> 5596 bytes examples/todos/src/main.rs | 615 ++++++++++++++++++++++++++++ examples/tour.rs | 794 ------------------------------------- examples/tour/Cargo.toml | 13 + examples/tour/images/ferris.png | Bin 0 -> 33061 bytes examples/tour/src/main.rs | 794 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- 44 files changed, 4193 insertions(+), 4078 deletions(-) delete mode 100644 examples/bezier_tool.rs create mode 100644 examples/bezier_tool/Cargo.toml create mode 100644 examples/bezier_tool/src/main.rs delete mode 100644 examples/counter.rs create mode 100644 examples/counter/Cargo.toml create mode 100644 examples/counter/src/main.rs delete mode 100644 examples/custom_widget.rs create mode 100644 examples/custom_widget/Cargo.toml create mode 100644 examples/custom_widget/src/main.rs delete mode 100644 examples/events.rs create mode 100644 examples/events/Cargo.toml create mode 100644 examples/events/src/main.rs delete mode 100644 examples/geometry.rs create mode 100644 examples/geometry/Cargo.toml create mode 100644 examples/geometry/src/main.rs delete mode 100644 examples/pokedex.rs create mode 100644 examples/pokedex/Cargo.toml create mode 100644 examples/pokedex/src/main.rs delete mode 100644 examples/progress_bar.rs create mode 100644 examples/progress_bar/Cargo.toml create mode 100644 examples/progress_bar/src/main.rs delete mode 100644 examples/resources/ferris.png delete mode 100644 examples/resources/icons.ttf delete mode 100644 examples/resources/tiger.svg delete mode 100644 examples/stopwatch.rs create mode 100644 examples/stopwatch/Cargo.toml create mode 100644 examples/stopwatch/src/main.rs delete mode 100644 examples/styling.rs create mode 100644 examples/styling/Cargo.toml create mode 100644 examples/styling/src/main.rs delete mode 100644 examples/svg.rs create mode 100644 examples/svg/Cargo.toml create mode 100644 examples/svg/resources/tiger.svg create mode 100644 examples/svg/src/main.rs delete mode 100644 examples/todos.rs create mode 100644 examples/todos/Cargo.toml create mode 100644 examples/todos/fonts/icons.ttf create mode 100644 examples/todos/src/main.rs delete mode 100644 examples/tour.rs create mode 100644 examples/tour/Cargo.toml create mode 100644 examples/tour/images/ferris.png create mode 100644 examples/tour/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 87f3000e..28a97af9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,18 @@ members = [ "web", "wgpu", "winit", + "examples/bezier_tool", + "examples/counter", + "examples/custom_widget", + "examples/events", + "examples/geometry", + "examples/pokedex", + "examples/progress_bar", + "examples/stopwatch", + "examples/styling", + "examples/svg", + "examples/todos", + "examples/tour", ] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -37,20 +49,3 @@ iced_wgpu = { version = "0.1.0", path = "wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_web = { version = "0.1.0", path = "web" } - -[dev-dependencies] -iced_native = { version = "0.1", path = "./native" } -iced_wgpu = { version = "0.1", path = "./wgpu" } -iced_futures = { version = "0.1.0-alpha", path = "./futures", features = ["async-std"] } -env_logger = "0.7" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -directories = "2.0" -futures = "0.3" -async-std = { version = "1.3", features = ["unstable"] } -surf = "1.0" -rand = "0.7" -lyon = "0.15" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen = "0.2.51" diff --git a/examples/bezier_tool.rs b/examples/bezier_tool.rs deleted file mode 100644 index 043d265c..00000000 --- a/examples/bezier_tool.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! This example showcases a simple native custom widget that renders arbitrary -//! path with `lyon`. -mod bezier { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - input, layout, Clipboard, Color, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector, - VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - use std::sync::Arc; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box Message>, - } - - #[derive(Debug, Clone, Copy)] - pub struct Curve { - from: Point, - to: Point, - control: Point, - } - - #[derive(Default)] - pub struct State { - pending: Option, - } - - enum Pending { - One { from: Point }, - Two { from: Point, to: Point }, - } - - impl<'a, Message> Bezier<'a, Message> { - pub fn new( - state: &'a mut State, - curves: &'a [Curve], - on_click: F, - ) -> Self - where - F: 'static + Fn(Curve) -> Message, - { - Self { - state, - curves, - on_click: Box::new(on_click), - } - } - } - - impl<'a, Message> Widget for Bezier<'a, Message> { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits - .height(Length::Fill) - .width(Length::Fill) - .resolve(Size::ZERO); - layout::Node::new(size) - } - - fn draw( - &self, - _renderer: &mut Renderer, - defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let mut buffer: VertexBuffers = VertexBuffers::new(); - let mut path_builder = lyon::path::Path::builder(); - - let bounds = layout.bounds(); - - // Draw rectangle border with lyon. - basic_shapes::stroke_rectangle( - &lyon::math::Rect::new( - lyon::math::Point::new(bounds.x + 0.5, bounds.y + 0.5), - lyon::math::Size::new( - bounds.width - 1.0, - bounds.height - 1.0, - ), - ), - &StrokeOptions::default().with_line_width(1.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - }, - ), - ) - .unwrap(); - - for curve in self.curves { - path_builder.move_to(lyon::math::Point::new( - curve.from.x + bounds.x, - curve.from.y + bounds.y, - )); - - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - curve.control.x + bounds.x, - curve.control.y + bounds.y, - ), - lyon::math::Point::new( - curve.to.x + bounds.x, - curve.to.y + bounds.y, - ), - ); - } - - match self.state.pending { - None => {} - Some(Pending::One { from }) => { - path_builder.move_to(lyon::math::Point::new( - from.x + bounds.x, - from.y + bounds.y, - )); - path_builder.line_to(lyon::math::Point::new( - cursor_position.x, - cursor_position.y, - )); - } - Some(Pending::Two { from, to }) => { - path_builder.move_to(lyon::math::Point::new( - from.x + bounds.x, - from.y + bounds.y, - )); - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - cursor_position.x, - cursor_position.y, - ), - lyon::math::Point::new( - to.x + bounds.x, - to.y + bounds.y, - ), - ); - } - } - - let mut tessellator = StrokeTessellator::new(); - - // Draw strokes with lyon. - tessellator - .tessellate( - &path_builder.build(), - &StrokeOptions::default().with_line_width(3.0), - &mut BuffersBuilder::new( - &mut buffer, - |pos: lyon::math::Point, _: StrokeAttributes| { - Vertex2D { - position: pos.to_array(), - color: [0.0, 0.0, 0.0, 1.0], - } - }, - ), - ) - .unwrap(); - - let mesh = Primitive::Mesh2D(Arc::new(Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - })); - - ( - Primitive::Clip { - bounds, - offset: Vector::new(0, 0), - content: Box::new( - if self.curves.is_empty() - && self.state.pending.is_none() - { - let instructions = Primitive::Text { - bounds, - color: Color { - a: defaults.text.color.a * 0.7, - ..defaults.text.color - }, - content: String::from( - "Click to create bezier curves!", - ), - font: Font::Default, - size: 30.0, - horizontal_alignment: - HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - Primitive::Group { - primitives: vec![mesh, instructions], - } - } else { - mesh - }, - ), - }, - MouseCursor::OutOfBounds, - ) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec, - _renderer: &Renderer, - _clipboard: Option<&dyn Clipboard>, - ) { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - match event { - Event::Mouse(input::mouse::Event::Input { - state: input::ButtonState::Pressed, - .. - }) => { - let new_point = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - match self.state.pending { - None => { - self.state.pending = - Some(Pending::One { from: new_point }); - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: new_point, - }); - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - messages.push((self.on_click)(Curve { - from, - to, - control: new_point, - })); - } - } - } - _ => {} - } - } - } - } - - impl<'a, Message> Into> for Bezier<'a, Message> - where - Message: 'static, - { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use bezier::Bezier; -use iced::{ - button, Align, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, -}; - -pub fn main() { - Example::run(Settings::default()) -} - -#[derive(Default)] -struct Example { - bezier: bezier::State, - curves: Vec, - button_state: button::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - AddCurve(bezier::Curve), - Clear, -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Example::default() - } - - fn title(&self) -> String { - String::from("Bezier tool - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::AddCurve(curve) => { - self.curves.push(curve); - } - Message::Clear => { - self.bezier = bezier::State::default(); - self.curves.clear(); - } - } - } - - fn view(&mut self) -> Element { - let content = Column::new() - .padding(20) - .spacing(20) - .align_items(Align::Center) - .push( - Text::new("Bezier tool example") - .width(Length::Shrink) - .size(50), - ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) - .push( - Button::new(&mut self.button_state, Text::new("Clear")) - .padding(8) - .on_press(Message::Clear), - ); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml new file mode 100644 index 00000000..b13a0aa5 --- /dev/null +++ b/examples/bezier_tool/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bezier_tool" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } +lyon = "0.15" diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs new file mode 100644 index 00000000..043d265c --- /dev/null +++ b/examples/bezier_tool/src/main.rs @@ -0,0 +1,366 @@ +//! This example showcases a simple native custom widget that renders arbitrary +//! path with `lyon`. +mod bezier { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + input, layout, Clipboard, Color, Element, Event, Font, Hasher, + HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector, + VerticalAlignment, Widget, + }; + use iced_wgpu::{ + triangle::{Mesh2D, Vertex2D}, + Defaults, Primitive, Renderer, + }; + use lyon::tessellation::{ + basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, + StrokeTessellator, VertexBuffers, + }; + use std::sync::Arc; + + pub struct Bezier<'a, Message> { + state: &'a mut State, + curves: &'a [Curve], + // [from, to, ctrl] + on_click: Box Message>, + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + #[derive(Default)] + pub struct State { + pending: Option, + } + + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl<'a, Message> Bezier<'a, Message> { + pub fn new( + state: &'a mut State, + curves: &'a [Curve], + on_click: F, + ) -> Self + where + F: 'static + Fn(Curve) -> Message, + { + Self { + state, + curves, + on_click: Box::new(on_click), + } + } + } + + impl<'a, Message> Widget for Bezier<'a, Message> { + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Fill + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits + .height(Length::Fill) + .width(Length::Fill) + .resolve(Size::ZERO); + layout::Node::new(size) + } + + fn draw( + &self, + _renderer: &mut Renderer, + defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let mut buffer: VertexBuffers = VertexBuffers::new(); + let mut path_builder = lyon::path::Path::builder(); + + let bounds = layout.bounds(); + + // Draw rectangle border with lyon. + basic_shapes::stroke_rectangle( + &lyon::math::Rect::new( + lyon::math::Point::new(bounds.x + 0.5, bounds.y + 0.5), + lyon::math::Size::new( + bounds.width - 1.0, + bounds.height - 1.0, + ), + ), + &StrokeOptions::default().with_line_width(1.0), + &mut BuffersBuilder::new( + &mut buffer, + |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D { + position: pos.to_array(), + color: [0.0, 0.0, 0.0, 1.0], + }, + ), + ) + .unwrap(); + + for curve in self.curves { + path_builder.move_to(lyon::math::Point::new( + curve.from.x + bounds.x, + curve.from.y + bounds.y, + )); + + path_builder.quadratic_bezier_to( + lyon::math::Point::new( + curve.control.x + bounds.x, + curve.control.y + bounds.y, + ), + lyon::math::Point::new( + curve.to.x + bounds.x, + curve.to.y + bounds.y, + ), + ); + } + + match self.state.pending { + None => {} + Some(Pending::One { from }) => { + path_builder.move_to(lyon::math::Point::new( + from.x + bounds.x, + from.y + bounds.y, + )); + path_builder.line_to(lyon::math::Point::new( + cursor_position.x, + cursor_position.y, + )); + } + Some(Pending::Two { from, to }) => { + path_builder.move_to(lyon::math::Point::new( + from.x + bounds.x, + from.y + bounds.y, + )); + path_builder.quadratic_bezier_to( + lyon::math::Point::new( + cursor_position.x, + cursor_position.y, + ), + lyon::math::Point::new( + to.x + bounds.x, + to.y + bounds.y, + ), + ); + } + } + + let mut tessellator = StrokeTessellator::new(); + + // Draw strokes with lyon. + tessellator + .tessellate( + &path_builder.build(), + &StrokeOptions::default().with_line_width(3.0), + &mut BuffersBuilder::new( + &mut buffer, + |pos: lyon::math::Point, _: StrokeAttributes| { + Vertex2D { + position: pos.to_array(), + color: [0.0, 0.0, 0.0, 1.0], + } + }, + ), + ) + .unwrap(); + + let mesh = Primitive::Mesh2D(Arc::new(Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + })); + + ( + Primitive::Clip { + bounds, + offset: Vector::new(0, 0), + content: Box::new( + if self.curves.is_empty() + && self.state.pending.is_none() + { + let instructions = Primitive::Text { + bounds, + color: Color { + a: defaults.text.color.a * 0.7, + ..defaults.text.color + }, + content: String::from( + "Click to create bezier curves!", + ), + font: Font::Default, + size: 30.0, + horizontal_alignment: + HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + }; + + Primitive::Group { + primitives: vec![mesh, instructions], + } + } else { + mesh + }, + ), + }, + MouseCursor::OutOfBounds, + ) + } + + fn hash_layout(&self, _state: &mut Hasher) {} + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + match event { + Event::Mouse(input::mouse::Event::Input { + state: input::ButtonState::Pressed, + .. + }) => { + let new_point = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + match self.state.pending { + None => { + self.state.pending = + Some(Pending::One { from: new_point }); + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: new_point, + }); + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + messages.push((self.on_click)(Curve { + from, + to, + control: new_point, + })); + } + } + } + _ => {} + } + } + } + } + + impl<'a, Message> Into> for Bezier<'a, Message> + where + Message: 'static, + { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use bezier::Bezier; +use iced::{ + button, Align, Button, Column, Container, Element, Length, Sandbox, + Settings, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + bezier: bezier::State, + curves: Vec, + button_state: button::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + AddCurve(bezier::Curve), + Clear, +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example::default() + } + + fn title(&self) -> String { + String::from("Bezier tool - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::AddCurve(curve) => { + self.curves.push(curve); + } + Message::Clear => { + self.bezier = bezier::State::default(); + self.curves.clear(); + } + } + } + + fn view(&mut self) -> Element { + let content = Column::new() + .padding(20) + .spacing(20) + .align_items(Align::Center) + .push( + Text::new("Bezier tool example") + .width(Length::Shrink) + .size(50), + ) + .push(Bezier::new( + &mut self.bezier, + self.curves.as_slice(), + Message::AddCurve, + )) + .push( + Button::new(&mut self.button_state, Text::new("Clear")) + .padding(8) + .on_press(Message::Clear), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/counter.rs b/examples/counter.rs deleted file mode 100644 index b85db70d..00000000 --- a/examples/counter.rs +++ /dev/null @@ -1,56 +0,0 @@ -use iced::{button, Button, Column, Element, Sandbox, Settings, Text}; - -pub fn main() { - Counter::run(Settings::default()) -} - -#[derive(Default)] -struct Counter { - value: i32, - increment_button: button::State, - decrement_button: button::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - IncrementPressed, - DecrementPressed, -} - -impl Sandbox for Counter { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("A simple counter") - } - - fn update(&mut self, message: Message) { - match message { - Message::IncrementPressed => { - self.value += 1; - } - Message::DecrementPressed => { - self.value -= 1; - } - } - } - - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .push( - Button::new(&mut self.increment_button, Text::new("Increment")) - .on_press(Message::IncrementPressed), - ) - .push(Text::new(self.value.to_string()).size(50)) - .push( - Button::new(&mut self.decrement_button, Text::new("Decrement")) - .on_press(Message::DecrementPressed), - ) - .into() - } -} diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml new file mode 100644 index 00000000..a763cd78 --- /dev/null +++ b/examples/counter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "counter" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs new file mode 100644 index 00000000..b85db70d --- /dev/null +++ b/examples/counter/src/main.rs @@ -0,0 +1,56 @@ +use iced::{button, Button, Column, Element, Sandbox, Settings, Text}; + +pub fn main() { + Counter::run(Settings::default()) +} + +#[derive(Default)] +struct Counter { + value: i32, + increment_button: button::State, + decrement_button: button::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + IncrementPressed, + DecrementPressed, +} + +impl Sandbox for Counter { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("A simple counter") + } + + fn update(&mut self, message: Message) { + match message { + Message::IncrementPressed => { + self.value += 1; + } + Message::DecrementPressed => { + self.value -= 1; + } + } + } + + fn view(&mut self) -> Element { + Column::new() + .padding(20) + .push( + Button::new(&mut self.increment_button, Text::new("Increment")) + .on_press(Message::IncrementPressed), + ) + .push(Text::new(self.value.to_string()).size(50)) + .push( + Button::new(&mut self.decrement_button, Text::new("Decrement")) + .on_press(Message::DecrementPressed), + ) + .into() + } +} diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs deleted file mode 100644 index 0a570745..00000000 --- a/examples/custom_widget.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! This example showcases a simple native custom widget that draws a circle. -mod circle { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, Background, Color, Element, Hasher, Layout, Length, - MouseCursor, Point, Size, Widget, - }; - use iced_wgpu::{Defaults, Primitive, Renderer}; - - pub struct Circle { - radius: u16, - } - - impl Circle { - pub fn new(radius: u16) -> Self { - Self { radius } - } - } - - impl Widget for Circle { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - layout::Node::new(Size::new( - f32::from(self.radius) * 2.0, - f32::from(self.radius) * 2.0, - )) - } - - fn hash_layout(&self, state: &mut Hasher) { - use std::hash::Hash; - - self.radius.hash(state); - } - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, MouseCursor) { - ( - Primitive::Quad { - bounds: layout.bounds(), - background: Background::Color(Color::BLACK), - border_radius: self.radius, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - MouseCursor::OutOfBounds, - ) - } - } - - impl<'a, Message> Into> for Circle { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use circle::Circle; -use iced::{ - slider, Align, Column, Container, Element, Length, Sandbox, Settings, - Slider, Text, -}; - -pub fn main() { - Example::run(Settings::default()) -} - -struct Example { - radius: u16, - slider: slider::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - RadiusChanged(f32), -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Example { - radius: 50, - slider: slider::State::new(), - } - } - - fn title(&self) -> String { - String::from("Custom widget - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::RadiusChanged(radius) => { - self.radius = radius.round() as u16; - } - } - } - - fn view(&mut self) -> Element { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Align::Center) - .push(Circle::new(self.radius)) - .push(Text::new(format!("Radius: {}", self.radius.to_string()))) - .push(Slider::new( - &mut self.slider, - 1.0..=100.0, - f32::from(self.radius), - Message::RadiusChanged, - )); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml new file mode 100644 index 00000000..30747dc0 --- /dev/null +++ b/examples/custom_widget/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "custom_widget" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs new file mode 100644 index 00000000..0a570745 --- /dev/null +++ b/examples/custom_widget/src/main.rs @@ -0,0 +1,145 @@ +//! This example showcases a simple native custom widget that draws a circle. +mod circle { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + layout, Background, Color, Element, Hasher, Layout, Length, + MouseCursor, Point, Size, Widget, + }; + use iced_wgpu::{Defaults, Primitive, Renderer}; + + pub struct Circle { + radius: u16, + } + + impl Circle { + pub fn new(radius: u16) -> Self { + Self { radius } + } + } + + impl Widget for Circle { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(Size::new( + f32::from(self.radius) * 2.0, + f32::from(self.radius) * 2.0, + )) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.radius.hash(state); + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + ( + Primitive::Quad { + bounds: layout.bounds(), + background: Background::Color(Color::BLACK), + border_radius: self.radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + MouseCursor::OutOfBounds, + ) + } + } + + impl<'a, Message> Into> for Circle { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use circle::Circle; +use iced::{ + slider, Align, Column, Container, Element, Length, Sandbox, Settings, + Slider, Text, +}; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + radius: u16, + slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + RadiusChanged(f32), +} + +impl Sandbox for Example { + type Message = Message; + + fn new() -> Self { + Example { + radius: 50, + slider: slider::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom widget - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::RadiusChanged(radius) => { + self.radius = radius.round() as u16; + } + } + } + + fn view(&mut self) -> Element { + let content = Column::new() + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Align::Center) + .push(Circle::new(self.radius)) + .push(Text::new(format!("Radius: {}", self.radius.to_string()))) + .push(Slider::new( + &mut self.slider, + 1.0..=100.0, + f32::from(self.radius), + Message::RadiusChanged, + )); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/events.rs b/examples/events.rs deleted file mode 100644 index 0c9dca05..00000000 --- a/examples/events.rs +++ /dev/null @@ -1,86 +0,0 @@ -use iced::{ - executor, Align, Application, Checkbox, Column, Command, Container, - Element, Length, Settings, Subscription, Text, -}; - -pub fn main() { - Events::run(Settings::default()) -} - -#[derive(Debug, Default)] -struct Events { - last: Vec, - enabled: bool, -} - -#[derive(Debug, Clone)] -enum Message { - EventOccurred(iced_native::Event), - Toggled(bool), -} - -impl Application for Events { - type Executor = executor::Default; - type Message = Message; - - fn new() -> (Events, Command) { - (Events::default(), Command::none()) - } - - fn title(&self) -> String { - String::from("Events - Iced") - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::EventOccurred(event) => { - self.last.push(event); - - if self.last.len() > 5 { - let _ = self.last.remove(0); - } - } - Message::Toggled(enabled) => { - self.enabled = enabled; - } - }; - - Command::none() - } - - fn subscription(&self) -> Subscription { - if self.enabled { - iced_native::subscription::events().map(Message::EventOccurred) - } else { - Subscription::none() - } - } - - fn view(&mut self) -> Element { - let events = self.last.iter().fold( - Column::new().spacing(10), - |column, event| { - column.push(Text::new(format!("{:?}", event)).size(40)) - }, - ); - - let toggle = Checkbox::new( - self.enabled, - "Listen to runtime events", - Message::Toggled, - ); - - let content = Column::new() - .align_items(Align::Center) - .spacing(20) - .push(events) - .push(toggle); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml new file mode 100644 index 00000000..f883075f --- /dev/null +++ b/examples/events/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "events" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs new file mode 100644 index 00000000..0c9dca05 --- /dev/null +++ b/examples/events/src/main.rs @@ -0,0 +1,86 @@ +use iced::{ + executor, Align, Application, Checkbox, Column, Command, Container, + Element, Length, Settings, Subscription, Text, +}; + +pub fn main() { + Events::run(Settings::default()) +} + +#[derive(Debug, Default)] +struct Events { + last: Vec, + enabled: bool, +} + +#[derive(Debug, Clone)] +enum Message { + EventOccurred(iced_native::Event), + Toggled(bool), +} + +impl Application for Events { + type Executor = executor::Default; + type Message = Message; + + fn new() -> (Events, Command) { + (Events::default(), Command::none()) + } + + fn title(&self) -> String { + String::from("Events - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::EventOccurred(event) => { + self.last.push(event); + + if self.last.len() > 5 { + let _ = self.last.remove(0); + } + } + Message::Toggled(enabled) => { + self.enabled = enabled; + } + }; + + Command::none() + } + + fn subscription(&self) -> Subscription { + if self.enabled { + iced_native::subscription::events().map(Message::EventOccurred) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let events = self.last.iter().fold( + Column::new().spacing(10), + |column, event| { + column.push(Text::new(format!("{:?}", event)).size(40)) + }, + ); + + let toggle = Checkbox::new( + self.enabled, + "Listen to runtime events", + Message::Toggled, + ); + + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .push(events) + .push(toggle); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/geometry.rs b/examples/geometry.rs deleted file mode 100644 index 9d5fd611..00000000 --- a/examples/geometry.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! This example showcases a simple native custom widget that renders using -//! arbitrary low-level geometry. -mod rainbow { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, - Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - - pub struct Rainbow; - - impl Rainbow { - pub fn new() -> Self { - Self - } - } - - impl Widget for Rainbow { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let size = limits.width(Length::Fill).resolve(Size::ZERO); - - layout::Node::new(Size::new(size.width, size.width)) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn draw( - &self, - _renderer: &mut Renderer, - _defaults: &Defaults, - layout: Layout<'_>, - cursor_position: Point, - ) -> (Primitive, MouseCursor) { - let b = layout.bounds(); - - // R O Y G B I V - let color_r = [1.0, 0.0, 0.0, 1.0]; - let color_o = [1.0, 0.5, 0.0, 1.0]; - let color_y = [1.0, 1.0, 0.0, 1.0]; - let color_g = [0.0, 1.0, 0.0, 1.0]; - let color_gb = [0.0, 1.0, 0.5, 1.0]; - let color_b = [0.0, 0.2, 1.0, 1.0]; - let color_i = [0.5, 0.0, 1.0, 1.0]; - let color_v = [0.75, 0.0, 0.5, 1.0]; - - let posn_center = { - if b.contains(cursor_position) { - [cursor_position.x, cursor_position.y] - } else { - [b.x + (b.width / 2.0), b.y + (b.height / 2.0)] - } - }; - - let posn_tl = [b.x, b.y]; - let posn_t = [b.x + (b.width / 2.0), b.y]; - let posn_tr = [b.x + b.width, b.y]; - let posn_r = [b.x + b.width, b.y + (b.height / 2.0)]; - let posn_br = [b.x + b.width, b.y + b.height]; - let posn_b = [b.x + (b.width / 2.0), b.y + b.height]; - let posn_bl = [b.x, b.y + b.height]; - let posn_l = [b.x, b.y + (b.height / 2.0)]; - - ( - Primitive::Mesh2D(std::sync::Arc::new(Mesh2D { - vertices: vec![ - Vertex2D { - position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], - }, - Vertex2D { - position: posn_tl, - color: color_r, - }, - Vertex2D { - position: posn_t, - color: color_o, - }, - Vertex2D { - position: posn_tr, - color: color_y, - }, - Vertex2D { - position: posn_r, - color: color_g, - }, - Vertex2D { - position: posn_br, - color: color_gb, - }, - Vertex2D { - position: posn_b, - color: color_b, - }, - Vertex2D { - position: posn_bl, - color: color_i, - }, - Vertex2D { - position: posn_l, - color: color_v, - }, - ], - indices: vec![ - 0, 1, 2, // TL - 0, 2, 3, // T - 0, 3, 4, // TR - 0, 4, 5, // R - 0, 5, 6, // BR - 0, 6, 7, // B - 0, 7, 8, // BL - 0, 8, 1, // L - ], - })), - MouseCursor::OutOfBounds, - ) - } - } - - impl<'a, Message> Into> for Rainbow { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use iced::{ - scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable, - Settings, Text, -}; -use rainbow::Rainbow; - -pub fn main() { - Example::run(Settings::default()) -} - -struct Example { - scroll: scrollable::State, -} - -impl Sandbox for Example { - type Message = (); - - fn new() -> Self { - Example { - scroll: scrollable::State::new(), - } - } - - fn title(&self) -> String { - String::from("Custom 2D geometry - Iced") - } - - fn update(&mut self, _: ()) {} - - fn view(&mut self) -> Element<()> { - let content = Column::new() - .padding(20) - .spacing(20) - .max_width(500) - .align_items(Align::Start) - .push(Rainbow::new()) - .push(Text::new( - "In this example we draw a custom widget Rainbow, using \ - the Mesh2D primitive. This primitive supplies a list of \ - triangles, expressed as vertices and indices.", - )) - .push(Text::new( - "Move your cursor over it, and see the center vertex \ - follow you!", - )) - .push(Text::new( - "Every Vertex2D defines its own color. You could use the \ - Mesh2D primitive to render virtually any two-dimensional \ - geometry for your widget.", - )); - - let scrollable = Scrollable::new(&mut self.scroll) - .push(Container::new(content).width(Length::Fill).center_x()); - - Container::new(scrollable) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() - } -} diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml new file mode 100644 index 00000000..9df52454 --- /dev/null +++ b/examples/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "geometry" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_wgpu = { path = "../../wgpu" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs new file mode 100644 index 00000000..9d5fd611 --- /dev/null +++ b/examples/geometry/src/main.rs @@ -0,0 +1,210 @@ +//! This example showcases a simple native custom widget that renders using +//! arbitrary low-level geometry. +mod rainbow { + // For now, to implement a custom native widget you will need to add + // `iced_native` and `iced_wgpu` to your dependencies. + // + // Then, you simply need to define your widget type and implement the + // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. + // + // Of course, you can choose to make the implementation renderer-agnostic, + // if you wish to, by creating your own `Renderer` trait, which could be + // implemented by `iced_wgpu` and other renderers. + use iced_native::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, + Widget, + }; + use iced_wgpu::{ + triangle::{Mesh2D, Vertex2D}, + Defaults, Primitive, Renderer, + }; + + pub struct Rainbow; + + impl Rainbow { + pub fn new() -> Self { + Self + } + } + + impl Widget for Rainbow { + fn width(&self) -> Length { + Length::Fill + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let size = limits.width(Length::Fill).resolve(Size::ZERO); + + layout::Node::new(Size::new(size.width, size.width)) + } + + fn hash_layout(&self, _state: &mut Hasher) {} + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let b = layout.bounds(); + + // R O Y G B I V + let color_r = [1.0, 0.0, 0.0, 1.0]; + let color_o = [1.0, 0.5, 0.0, 1.0]; + let color_y = [1.0, 1.0, 0.0, 1.0]; + let color_g = [0.0, 1.0, 0.0, 1.0]; + let color_gb = [0.0, 1.0, 0.5, 1.0]; + let color_b = [0.0, 0.2, 1.0, 1.0]; + let color_i = [0.5, 0.0, 1.0, 1.0]; + let color_v = [0.75, 0.0, 0.5, 1.0]; + + let posn_center = { + if b.contains(cursor_position) { + [cursor_position.x, cursor_position.y] + } else { + [b.x + (b.width / 2.0), b.y + (b.height / 2.0)] + } + }; + + let posn_tl = [b.x, b.y]; + let posn_t = [b.x + (b.width / 2.0), b.y]; + let posn_tr = [b.x + b.width, b.y]; + let posn_r = [b.x + b.width, b.y + (b.height / 2.0)]; + let posn_br = [b.x + b.width, b.y + b.height]; + let posn_b = [b.x + (b.width / 2.0), b.y + b.height]; + let posn_bl = [b.x, b.y + b.height]; + let posn_l = [b.x, b.y + (b.height / 2.0)]; + + ( + Primitive::Mesh2D(std::sync::Arc::new(Mesh2D { + vertices: vec![ + Vertex2D { + position: posn_center, + color: [1.0, 1.0, 1.0, 1.0], + }, + Vertex2D { + position: posn_tl, + color: color_r, + }, + Vertex2D { + position: posn_t, + color: color_o, + }, + Vertex2D { + position: posn_tr, + color: color_y, + }, + Vertex2D { + position: posn_r, + color: color_g, + }, + Vertex2D { + position: posn_br, + color: color_gb, + }, + Vertex2D { + position: posn_b, + color: color_b, + }, + Vertex2D { + position: posn_bl, + color: color_i, + }, + Vertex2D { + position: posn_l, + color: color_v, + }, + ], + indices: vec![ + 0, 1, 2, // TL + 0, 2, 3, // T + 0, 3, 4, // TR + 0, 4, 5, // R + 0, 5, 6, // BR + 0, 6, 7, // B + 0, 7, 8, // BL + 0, 8, 1, // L + ], + })), + MouseCursor::OutOfBounds, + ) + } + } + + impl<'a, Message> Into> for Rainbow { + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } + } +} + +use iced::{ + scrollable, Align, Column, Container, Element, Length, Sandbox, Scrollable, + Settings, Text, +}; +use rainbow::Rainbow; + +pub fn main() { + Example::run(Settings::default()) +} + +struct Example { + scroll: scrollable::State, +} + +impl Sandbox for Example { + type Message = (); + + fn new() -> Self { + Example { + scroll: scrollable::State::new(), + } + } + + fn title(&self) -> String { + String::from("Custom 2D geometry - Iced") + } + + fn update(&mut self, _: ()) {} + + fn view(&mut self) -> Element<()> { + let content = Column::new() + .padding(20) + .spacing(20) + .max_width(500) + .align_items(Align::Start) + .push(Rainbow::new()) + .push(Text::new( + "In this example we draw a custom widget Rainbow, using \ + the Mesh2D primitive. This primitive supplies a list of \ + triangles, expressed as vertices and indices.", + )) + .push(Text::new( + "Move your cursor over it, and see the center vertex \ + follow you!", + )) + .push(Text::new( + "Every Vertex2D defines its own color. You could use the \ + Mesh2D primitive to render virtually any two-dimensional \ + geometry for your widget.", + )); + + let scrollable = Scrollable::new(&mut self.scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() + } +} diff --git a/examples/pokedex.rs b/examples/pokedex.rs deleted file mode 100644 index 505dbf19..00000000 --- a/examples/pokedex.rs +++ /dev/null @@ -1,243 +0,0 @@ -use iced::{ - button, image, Align, Application, Button, Column, Command, Container, - Element, Image, Length, Row, Settings, Text, -}; - -pub fn main() { - Pokedex::run(Settings::default()) -} - -#[derive(Debug)] -enum Pokedex { - Loading, - Loaded { - pokemon: Pokemon, - search: button::State, - }, - Errored { - error: Error, - try_again: button::State, - }, -} - -#[derive(Debug, Clone)] -enum Message { - PokemonFound(Result), - Search, -} - -impl Application for Pokedex { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Pokedex, Command) { - ( - Pokedex::Loading, - Command::perform(Pokemon::search(), Message::PokemonFound), - ) - } - - fn title(&self) -> String { - let subtitle = match self { - Pokedex::Loading => "Loading", - Pokedex::Loaded { pokemon, .. } => &pokemon.name, - Pokedex::Errored { .. } => "Whoops!", - }; - - format!("{} - Pokédex", subtitle) - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::PokemonFound(Ok(pokemon)) => { - *self = Pokedex::Loaded { - pokemon, - search: button::State::new(), - }; - - Command::none() - } - Message::PokemonFound(Err(error)) => { - *self = Pokedex::Errored { - error, - try_again: button::State::new(), - }; - - Command::none() - } - Message::Search => match self { - Pokedex::Loading => Command::none(), - _ => { - *self = Pokedex::Loading; - - Command::perform(Pokemon::search(), Message::PokemonFound) - } - }, - } - } - - fn view(&mut self) -> Element { - let content = match self { - Pokedex::Loading => Column::new() - .push(Text::new("Searching for Pokémon...").size(40)), - Pokedex::Loaded { pokemon, search } => Column::new() - .max_width(500) - .spacing(20) - .align_items(Align::End) - .push(pokemon.view()) - .push( - button(search, "Keep searching!").on_press(Message::Search), - ), - Pokedex::Errored { try_again, .. } => Column::new() - .spacing(20) - .align_items(Align::End) - .push(Text::new("Whoops! Something went wrong...").size(40)) - .push(button(try_again, "Try again").on_press(Message::Search)), - }; - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} - -#[derive(Debug, Clone)] -struct Pokemon { - number: u16, - name: String, - description: String, - image: image::Handle, -} - -impl Pokemon { - const TOTAL: u16 = 807; - - fn view(&self) -> Element { - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(Image::new(self.image.clone())) - .push( - Column::new() - .spacing(20) - .push( - Row::new() - .align_items(Align::Center) - .spacing(20) - .push( - Text::new(&self.name) - .size(30) - .width(Length::Fill), - ) - .push( - Text::new(format!("#{}", self.number)) - .size(20) - .color([0.5, 0.5, 0.5]), - ), - ) - .push(Text::new(&self.description)), - ) - .into() - } - - async fn search() -> Result { - use rand::Rng; - use serde::Deserialize; - - #[derive(Debug, Deserialize)] - struct Entry { - id: u32, - name: String, - flavor_text_entries: Vec, - } - - #[derive(Debug, Deserialize)] - struct FlavorText { - flavor_text: String, - language: Language, - } - - #[derive(Debug, Deserialize)] - struct Language { - name: String, - } - - let id = { - let mut rng = rand::thread_rng(); - - rng.gen_range(0, Pokemon::TOTAL) - }; - - let url = format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); - let sprite = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); - - let (entry, sprite): (Entry, _) = futures::future::try_join( - surf::get(&url).recv_json(), - surf::get(&sprite).recv_bytes(), - ) - .await?; - - let description = entry - .flavor_text_entries - .iter() - .filter(|text| text.language.name == "en") - .next() - .ok_or(Error::LanguageError)?; - - Ok(Pokemon { - number: id, - name: entry.name.to_uppercase(), - description: description - .flavor_text - .chars() - .map(|c| if c.is_control() { ' ' } else { c }) - .collect(), - image: image::Handle::from_memory(sprite), - }) - } -} - -#[derive(Debug, Clone)] -enum Error { - APIError, - LanguageError, -} - -impl From for Error { - fn from(exception: surf::Exception) -> Error { - dbg!(&exception); - - Error::APIError - } -} - -fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { - Button::new(state, Text::new(text)) - .padding(10) - .style(style::Button::Primary) -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml new file mode 100644 index 00000000..2972590f --- /dev/null +++ b/examples/pokedex/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pokedex" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_futures = { path = "../../futures", features = ["async-std"] } +surf = "1.0" +rand = "0.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs new file mode 100644 index 00000000..283437b2 --- /dev/null +++ b/examples/pokedex/src/main.rs @@ -0,0 +1,243 @@ +use iced::{ + button, futures, image, Align, Application, Button, Column, Command, + Container, Element, Image, Length, Row, Settings, Text, +}; + +pub fn main() { + Pokedex::run(Settings::default()) +} + +#[derive(Debug)] +enum Pokedex { + Loading, + Loaded { + pokemon: Pokemon, + search: button::State, + }, + Errored { + error: Error, + try_again: button::State, + }, +} + +#[derive(Debug, Clone)] +enum Message { + PokemonFound(Result), + Search, +} + +impl Application for Pokedex { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Pokedex, Command) { + ( + Pokedex::Loading, + Command::perform(Pokemon::search(), Message::PokemonFound), + ) + } + + fn title(&self) -> String { + let subtitle = match self { + Pokedex::Loading => "Loading", + Pokedex::Loaded { pokemon, .. } => &pokemon.name, + Pokedex::Errored { .. } => "Whoops!", + }; + + format!("{} - Pokédex", subtitle) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::PokemonFound(Ok(pokemon)) => { + *self = Pokedex::Loaded { + pokemon, + search: button::State::new(), + }; + + Command::none() + } + Message::PokemonFound(Err(error)) => { + *self = Pokedex::Errored { + error, + try_again: button::State::new(), + }; + + Command::none() + } + Message::Search => match self { + Pokedex::Loading => Command::none(), + _ => { + *self = Pokedex::Loading; + + Command::perform(Pokemon::search(), Message::PokemonFound) + } + }, + } + } + + fn view(&mut self) -> Element { + let content = match self { + Pokedex::Loading => Column::new() + .push(Text::new("Searching for Pokémon...").size(40)), + Pokedex::Loaded { pokemon, search } => Column::new() + .max_width(500) + .spacing(20) + .align_items(Align::End) + .push(pokemon.view()) + .push( + button(search, "Keep searching!").on_press(Message::Search), + ), + Pokedex::Errored { try_again, .. } => Column::new() + .spacing(20) + .align_items(Align::End) + .push(Text::new("Whoops! Something went wrong...").size(40)) + .push(button(try_again, "Try again").on_press(Message::Search)), + }; + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +struct Pokemon { + number: u16, + name: String, + description: String, + image: image::Handle, +} + +impl Pokemon { + const TOTAL: u16 = 807; + + fn view(&self) -> Element { + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(Image::new(self.image.clone())) + .push( + Column::new() + .spacing(20) + .push( + Row::new() + .align_items(Align::Center) + .spacing(20) + .push( + Text::new(&self.name) + .size(30) + .width(Length::Fill), + ) + .push( + Text::new(format!("#{}", self.number)) + .size(20) + .color([0.5, 0.5, 0.5]), + ), + ) + .push(Text::new(&self.description)), + ) + .into() + } + + async fn search() -> Result { + use rand::Rng; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct Entry { + id: u32, + name: String, + flavor_text_entries: Vec, + } + + #[derive(Debug, Deserialize)] + struct FlavorText { + flavor_text: String, + language: Language, + } + + #[derive(Debug, Deserialize)] + struct Language { + name: String, + } + + let id = { + let mut rng = rand::thread_rng(); + + rng.gen_range(0, Pokemon::TOTAL) + }; + + let url = format!("https://pokeapi.co/api/v2/pokemon-species/{}", id); + let sprite = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id); + + let (entry, sprite): (Entry, _) = futures::future::try_join( + surf::get(&url).recv_json(), + surf::get(&sprite).recv_bytes(), + ) + .await?; + + let description = entry + .flavor_text_entries + .iter() + .filter(|text| text.language.name == "en") + .next() + .ok_or(Error::LanguageError)?; + + Ok(Pokemon { + number: id, + name: entry.name.to_uppercase(), + description: description + .flavor_text + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect(), + image: image::Handle::from_memory(sprite), + }) + } +} + +#[derive(Debug, Clone)] +enum Error { + APIError, + LanguageError, +} + +impl From for Error { + fn from(exception: surf::Exception) -> Error { + dbg!(&exception); + + Error::APIError + } +} + +fn button<'a>(state: &'a mut button::State, text: &str) -> Button<'a, Message> { + Button::new(state, Text::new(text)) + .padding(10) + .style(style::Button::Primary) +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/progress_bar.rs b/examples/progress_bar.rs deleted file mode 100644 index 43b09928..00000000 --- a/examples/progress_bar.rs +++ /dev/null @@ -1,47 +0,0 @@ -use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; - -pub fn main() { - Progress::run(Settings::default()) -} - -#[derive(Default)] -struct Progress { - value: f32, - progress_bar_slider: slider::State, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - SliderChanged(f32), -} - -impl Sandbox for Progress { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("A simple Progressbar") - } - - fn update(&mut self, message: Message) { - match message { - Message::SliderChanged(x) => self.value = x, - } - } - - fn view(&mut self) -> Element { - Column::new() - .padding(20) - .push(ProgressBar::new(0.0..=100.0, self.value)) - .push(Slider::new( - &mut self.progress_bar_slider, - 0.0..=100.0, - self.value, - Message::SliderChanged, - )) - .into() - } -} diff --git a/examples/progress_bar/Cargo.toml b/examples/progress_bar/Cargo.toml new file mode 100644 index 00000000..4eccbf14 --- /dev/null +++ b/examples/progress_bar/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "progress_bar" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs new file mode 100644 index 00000000..43b09928 --- /dev/null +++ b/examples/progress_bar/src/main.rs @@ -0,0 +1,47 @@ +use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider}; + +pub fn main() { + Progress::run(Settings::default()) +} + +#[derive(Default)] +struct Progress { + value: f32, + progress_bar_slider: slider::State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SliderChanged(f32), +} + +impl Sandbox for Progress { + type Message = Message; + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("A simple Progressbar") + } + + fn update(&mut self, message: Message) { + match message { + Message::SliderChanged(x) => self.value = x, + } + } + + fn view(&mut self) -> Element { + Column::new() + .padding(20) + .push(ProgressBar::new(0.0..=100.0, self.value)) + .push(Slider::new( + &mut self.progress_bar_slider, + 0.0..=100.0, + self.value, + Message::SliderChanged, + )) + .into() + } +} diff --git a/examples/resources/ferris.png b/examples/resources/ferris.png deleted file mode 100644 index ebce1a14..00000000 Binary files a/examples/resources/ferris.png and /dev/null differ diff --git a/examples/resources/icons.ttf b/examples/resources/icons.ttf deleted file mode 100644 index 4498299d..00000000 Binary files a/examples/resources/icons.ttf and /dev/null differ diff --git a/examples/resources/tiger.svg b/examples/resources/tiger.svg deleted file mode 100644 index 679edec2..00000000 --- a/examples/resources/tiger.svg +++ /dev/null @@ -1,725 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs deleted file mode 100644 index 6e357039..00000000 --- a/examples/stopwatch.rs +++ /dev/null @@ -1,204 +0,0 @@ -use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, -}; -use std::time::{Duration, Instant}; - -pub fn main() { - Stopwatch::run(Settings::default()) -} - -struct Stopwatch { - duration: Duration, - state: State, - toggle: button::State, - reset: button::State, -} - -enum State { - Idle, - Ticking { last_tick: Instant }, -} - -#[derive(Debug, Clone)] -enum Message { - Toggle, - Reset, - Tick(Instant), -} - -impl Application for Stopwatch { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Stopwatch, Command) { - ( - Stopwatch { - duration: Duration::default(), - state: State::Idle, - toggle: button::State::new(), - reset: button::State::new(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Stopwatch - Iced") - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::Toggle => match self.state { - State::Idle => { - self.state = State::Ticking { - last_tick: Instant::now(), - }; - } - State::Ticking { .. } => { - self.state = State::Idle; - } - }, - Message::Tick(now) => match &mut self.state { - State::Ticking { last_tick } => { - self.duration += now - *last_tick; - *last_tick = now; - } - _ => {} - }, - Message::Reset => { - self.duration = Duration::default(); - } - } - - Command::none() - } - - fn subscription(&self) -> Subscription { - match self.state { - State::Idle => Subscription::none(), - State::Ticking { .. } => { - time::every(Duration::from_millis(10)).map(Message::Tick) - } - } - } - - fn view(&mut self) -> Element { - const MINUTE: u64 = 60; - const HOUR: u64 = 60 * MINUTE; - - let seconds = self.duration.as_secs(); - - let duration = Text::new(format!( - "{:0>2}:{:0>2}:{:0>2}.{:0>2}", - seconds / HOUR, - (seconds % HOUR) / MINUTE, - seconds % MINUTE, - self.duration.subsec_millis() / 10, - )) - .size(40); - - let button = |state, label, style| { - Button::new( - state, - Text::new(label) - .horizontal_alignment(HorizontalAlignment::Center), - ) - .min_width(80) - .padding(10) - .style(style) - }; - - let toggle_button = { - let (label, color) = match self.state { - State::Idle => ("Start", style::Button::Primary), - State::Ticking { .. } => ("Stop", style::Button::Destructive), - }; - - button(&mut self.toggle, label, color).on_press(Message::Toggle) - }; - - let reset_button = - button(&mut self.reset, "Reset", style::Button::Secondary) - .on_press(Message::Reset); - - let controls = Row::new() - .spacing(20) - .push(toggle_button) - .push(reset_button); - - let content = Column::new() - .align_items(Align::Center) - .spacing(20) - .push(duration) - .push(controls); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} - -mod time { - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl iced_native::subscription::Recipe for Every - where - H: std::hash::Hasher, - { - type Output = std::time::Instant; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| std::time::Instant::now()) - .boxed() - } - } -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::WHITE, - ..button::Style::default() - } - } - } -} diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml new file mode 100644 index 00000000..1dae3b83 --- /dev/null +++ b/examples/stopwatch/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stopwatch" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +iced_native = { path = "../../native" } +iced_futures = { path = "../../futures", features = ["async-std"] } +async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs new file mode 100644 index 00000000..d84c4817 --- /dev/null +++ b/examples/stopwatch/src/main.rs @@ -0,0 +1,206 @@ +use iced::{ + button, Align, Application, Button, Column, Command, Container, Element, + HorizontalAlignment, Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + Stopwatch::run(Settings::default()) +} + +struct Stopwatch { + duration: Duration, + state: State, + toggle: button::State, + reset: button::State, +} + +enum State { + Idle, + Ticking { last_tick: Instant }, +} + +#[derive(Debug, Clone)] +enum Message { + Toggle, + Reset, + Tick(Instant), +} + +impl Application for Stopwatch { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Stopwatch, Command) { + ( + Stopwatch { + duration: Duration::default(), + state: State::Idle, + toggle: button::State::new(), + reset: button::State::new(), + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Stopwatch - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Toggle => match self.state { + State::Idle => { + self.state = State::Ticking { + last_tick: Instant::now(), + }; + } + State::Ticking { .. } => { + self.state = State::Idle; + } + }, + Message::Tick(now) => match &mut self.state { + State::Ticking { last_tick } => { + self.duration += now - *last_tick; + *last_tick = now; + } + _ => {} + }, + Message::Reset => { + self.duration = Duration::default(); + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + match self.state { + State::Idle => Subscription::none(), + State::Ticking { .. } => { + time::every(Duration::from_millis(10)).map(Message::Tick) + } + } + } + + fn view(&mut self) -> Element { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + + let seconds = self.duration.as_secs(); + + let duration = Text::new(format!( + "{:0>2}:{:0>2}:{:0>2}.{:0>2}", + seconds / HOUR, + (seconds % HOUR) / MINUTE, + seconds % MINUTE, + self.duration.subsec_millis() / 10, + )) + .size(40); + + let button = |state, label, style| { + Button::new( + state, + Text::new(label) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .min_width(80) + .padding(10) + .style(style) + }; + + let toggle_button = { + let (label, color) = match self.state { + State::Idle => ("Start", style::Button::Primary), + State::Ticking { .. } => ("Stop", style::Button::Destructive), + }; + + button(&mut self.toggle, label, color).on_press(Message::Toggle) + }; + + let reset_button = + button(&mut self.reset, "Reset", style::Button::Secondary) + .on_press(Message::Reset); + + let controls = Row::new() + .spacing(20) + .push(toggle_button) + .push(reset_button); + + let content = Column::new() + .align_items(Align::Center) + .spacing(20) + .push(duration) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} + +mod time { + use iced::futures; + + pub fn every( + duration: std::time::Duration, + ) -> iced::Subscription { + iced::Subscription::from_recipe(Every(duration)) + } + + struct Every(std::time::Duration); + + impl iced_native::subscription::Recipe for Every + where + H: std::hash::Hasher, + { + type Output = std::time::Instant; + + fn hash(&self, state: &mut H) { + use std::hash::Hash; + + std::any::TypeId::of::().hash(state); + self.0.hash(state); + } + + fn stream( + self: Box, + _input: futures::stream::BoxStream<'static, I>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + Secondary, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::WHITE, + ..button::Style::default() + } + } + } +} diff --git a/examples/styling.rs b/examples/styling.rs deleted file mode 100644 index 50095ec7..00000000 --- a/examples/styling.rs +++ /dev/null @@ -1,514 +0,0 @@ -use iced::{ - button, scrollable, slider, text_input, Align, Button, Checkbox, Column, - Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, - Settings, Slider, Space, Text, TextInput, -}; - -pub fn main() { - Styling::run(Settings::default()) -} - -#[derive(Default)] -struct Styling { - theme: style::Theme, - scroll: scrollable::State, - input: text_input::State, - input_value: String, - button: button::State, - slider: slider::State, - slider_value: f32, - toggle_value: bool, -} - -#[derive(Debug, Clone)] -enum Message { - ThemeChanged(style::Theme), - InputChanged(String), - ButtonPressed, - SliderChanged(f32), - CheckboxToggled(bool), -} - -impl Sandbox for Styling { - type Message = Message; - - fn new() -> Self { - Styling::default() - } - - fn title(&self) -> String { - String::from("Styling - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::ThemeChanged(theme) => self.theme = theme, - Message::InputChanged(value) => self.input_value = value, - Message::ButtonPressed => (), - Message::SliderChanged(value) => self.slider_value = value, - Message::CheckboxToggled(value) => self.toggle_value = value, - } - } - - fn view(&mut self) -> Element { - let choose_theme = style::Theme::ALL.iter().fold( - Column::new().spacing(10).push(Text::new("Choose a theme:")), - |column, theme| { - column.push( - Radio::new( - *theme, - &format!("{:?}", theme), - Some(self.theme), - Message::ThemeChanged, - ) - .style(self.theme), - ) - }, - ); - - let text_input = TextInput::new( - &mut self.input, - "Type something...", - &self.input_value, - Message::InputChanged, - ) - .padding(10) - .size(20) - .style(self.theme); - - let button = Button::new(&mut self.button, Text::new("Submit")) - .padding(10) - .on_press(Message::ButtonPressed) - .style(self.theme); - - let slider = Slider::new( - &mut self.slider, - 0.0..=100.0, - self.slider_value, - Message::SliderChanged, - ) - .style(self.theme); - - let progress_bar = - ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); - - let scrollable = Scrollable::new(&mut self.scroll) - .height(Length::Units(100)) - .style(self.theme) - .push(Text::new("Scroll me!")) - .push(Space::with_height(Length::Units(800))) - .push(Text::new("You did it!")); - - let checkbox = Checkbox::new( - self.toggle_value, - "Toggle me!", - Message::CheckboxToggled, - ) - .style(self.theme); - - let content = Column::new() - .spacing(20) - .padding(20) - .max_width(600) - .push(choose_theme) - .push(Row::new().spacing(10).push(text_input).push(button)) - .push(slider) - .push(progress_bar) - .push( - Row::new() - .spacing(10) - .align_items(Align::Center) - .push(scrollable) - .push(checkbox), - ); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .style(self.theme) - .into() - } -} - -mod style { - use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, slider, - text_input, - }; - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - Light, - Dark, - } - - impl Theme { - pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; - } - - impl Default for Theme { - fn default() -> Theme { - Theme::Light - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Container.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Radio.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::TextInput.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => light::Button.into(), - Theme::Dark => dark::Button.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Scrollable.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Slider.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::ProgressBar.into(), - } - } - } - - impl From for Box { - fn from(theme: Theme) -> Self { - match theme { - Theme::Light => Default::default(), - Theme::Dark => dark::Checkbox.into(), - } - } - } - - mod light { - use iced::{button, Background, Color, Vector}; - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.11, 0.42, 0.87, - ))), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } - } - - mod dark { - use iced::{ - button, checkbox, container, progress_bar, radio, scrollable, - slider, text_input, Background, Color, - }; - - const SURFACE: Color = Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ); - - const ACCENT: Color = Color::from_rgb( - 0x6F as f32 / 255.0, - 0xFF as f32 / 255.0, - 0xE9 as f32 / 255.0, - ); - - const ACTIVE: Color = Color::from_rgb( - 0x72 as f32 / 255.0, - 0x89 as f32 / 255.0, - 0xDA as f32 / 255.0, - ); - - const HOVERED: Color = Color::from_rgb( - 0x67 as f32 / 255.0, - 0x7B as f32 / 255.0, - 0xC4 as f32 / 255.0, - ); - - pub struct Container; - - impl container::StyleSheet for Container { - fn style(&self) -> container::Style { - container::Style { - background: Some(Background::Color(Color::from_rgb8( - 0x36, 0x39, 0x3F, - ))), - text_color: Some(Color::WHITE), - ..container::Style::default() - } - } - } - - pub struct Radio; - - impl radio::StyleSheet for Radio { - fn active(&self) -> radio::Style { - radio::Style { - background: Background::Color(SURFACE), - dot_color: ACTIVE, - border_width: 1, - border_color: ACTIVE, - } - } - - fn hovered(&self) -> radio::Style { - radio::Style { - background: Background::Color(Color { a: 0.5, ..SURFACE }), - ..self.active() - } - } - } - - pub struct TextInput; - - impl text_input::StyleSheet for TextInput { - fn active(&self) -> text_input::Style { - text_input::Style { - background: Background::Color(SURFACE), - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - } - } - - fn focused(&self) -> text_input::Style { - text_input::Style { - border_width: 1, - border_color: ACCENT, - ..self.active() - } - } - - fn hovered(&self) -> text_input::Style { - text_input::Style { - border_width: 1, - border_color: Color { a: 0.3, ..ACCENT }, - ..self.focused() - } - } - - fn placeholder_color(&self) -> Color { - Color::from_rgb(0.4, 0.4, 0.4) - } - - fn value_color(&self) -> Color { - Color::WHITE - } - } - - pub struct Button; - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(ACTIVE)), - border_radius: 3, - text_color: Color::WHITE, - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - background: Some(Background::Color(HOVERED)), - text_color: Color::WHITE, - ..self.active() - } - } - - fn pressed(&self) -> button::Style { - button::Style { - border_width: 1, - border_color: Color::WHITE, - ..self.hovered() - } - } - } - - pub struct Scrollable; - - impl scrollable::StyleSheet for Scrollable { - fn active(&self) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: Some(Background::Color(SURFACE)), - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: ACTIVE, - border_radius: 2, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> scrollable::Scrollbar { - let active = self.active(); - - scrollable::Scrollbar { - background: Some(Background::Color(Color { - a: 0.5, - ..SURFACE - })), - scroller: scrollable::Scroller { - color: HOVERED, - ..active.scroller - }, - ..active - } - } - - fn dragging(&self) -> scrollable::Scrollbar { - let hovered = self.hovered(); - - scrollable::Scrollbar { - scroller: scrollable::Scroller { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..hovered.scroller - }, - ..hovered - } - } - } - - pub struct Slider; - - impl slider::StyleSheet for Slider { - fn active(&self) -> slider::Style { - slider::Style { - rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 9 }, - color: ACTIVE, - border_width: 0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: HOVERED, - ..active.handle - }, - ..active - } - } - - fn dragging(&self) -> slider::Style { - let active = self.active(); - - slider::Style { - handle: slider::Handle { - color: Color::from_rgb(0.85, 0.85, 0.85), - ..active.handle - }, - ..active - } - } - } - - pub struct ProgressBar; - - impl progress_bar::StyleSheet for ProgressBar { - fn style(&self) -> progress_bar::Style { - progress_bar::Style { - background: Background::Color(SURFACE), - bar: Background::Color(ACTIVE), - border_radius: 10, - } - } - } - - pub struct Checkbox; - - impl checkbox::StyleSheet for Checkbox { - fn active(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Background::Color(if is_checked { - ACTIVE - } else { - SURFACE - }), - checkmark_color: Color::WHITE, - border_radius: 2, - border_width: 1, - border_color: ACTIVE, - } - } - - fn hovered(&self, is_checked: bool) -> checkbox::Style { - checkbox::Style { - background: Background::Color(Color { - a: 0.8, - ..if is_checked { ACTIVE } else { SURFACE } - }), - ..self.active(is_checked) - } - } - } - } -} diff --git a/examples/styling/Cargo.toml b/examples/styling/Cargo.toml new file mode 100644 index 00000000..eb729f93 --- /dev/null +++ b/examples/styling/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "styling" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs new file mode 100644 index 00000000..50095ec7 --- /dev/null +++ b/examples/styling/src/main.rs @@ -0,0 +1,514 @@ +use iced::{ + button, scrollable, slider, text_input, Align, Button, Checkbox, Column, + Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable, + Settings, Slider, Space, Text, TextInput, +}; + +pub fn main() { + Styling::run(Settings::default()) +} + +#[derive(Default)] +struct Styling { + theme: style::Theme, + scroll: scrollable::State, + input: text_input::State, + input_value: String, + button: button::State, + slider: slider::State, + slider_value: f32, + toggle_value: bool, +} + +#[derive(Debug, Clone)] +enum Message { + ThemeChanged(style::Theme), + InputChanged(String), + ButtonPressed, + SliderChanged(f32), + CheckboxToggled(bool), +} + +impl Sandbox for Styling { + type Message = Message; + + fn new() -> Self { + Styling::default() + } + + fn title(&self) -> String { + String::from("Styling - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::ThemeChanged(theme) => self.theme = theme, + Message::InputChanged(value) => self.input_value = value, + Message::ButtonPressed => (), + Message::SliderChanged(value) => self.slider_value = value, + Message::CheckboxToggled(value) => self.toggle_value = value, + } + } + + fn view(&mut self) -> Element { + let choose_theme = style::Theme::ALL.iter().fold( + Column::new().spacing(10).push(Text::new("Choose a theme:")), + |column, theme| { + column.push( + Radio::new( + *theme, + &format!("{:?}", theme), + Some(self.theme), + Message::ThemeChanged, + ) + .style(self.theme), + ) + }, + ); + + let text_input = TextInput::new( + &mut self.input, + "Type something...", + &self.input_value, + Message::InputChanged, + ) + .padding(10) + .size(20) + .style(self.theme); + + let button = Button::new(&mut self.button, Text::new("Submit")) + .padding(10) + .on_press(Message::ButtonPressed) + .style(self.theme); + + let slider = Slider::new( + &mut self.slider, + 0.0..=100.0, + self.slider_value, + Message::SliderChanged, + ) + .style(self.theme); + + let progress_bar = + ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme); + + let scrollable = Scrollable::new(&mut self.scroll) + .height(Length::Units(100)) + .style(self.theme) + .push(Text::new("Scroll me!")) + .push(Space::with_height(Length::Units(800))) + .push(Text::new("You did it!")); + + let checkbox = Checkbox::new( + self.toggle_value, + "Toggle me!", + Message::CheckboxToggled, + ) + .style(self.theme); + + let content = Column::new() + .spacing(20) + .padding(20) + .max_width(600) + .push(choose_theme) + .push(Row::new().spacing(10).push(text_input).push(button)) + .push(slider) + .push(progress_bar) + .push( + Row::new() + .spacing(10) + .align_items(Align::Center) + .push(scrollable) + .push(checkbox), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .style(self.theme) + .into() + } +} + +mod style { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, slider, + text_input, + }; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Theme { + Light, + Dark, + } + + impl Theme { + pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; + } + + impl Default for Theme { + fn default() -> Theme { + Theme::Light + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Container.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Radio.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::TextInput.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => light::Button.into(), + Theme::Dark => dark::Button.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Scrollable.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Slider.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::ProgressBar.into(), + } + } + } + + impl From for Box { + fn from(theme: Theme) -> Self { + match theme { + Theme::Light => Default::default(), + Theme::Dark => dark::Checkbox.into(), + } + } + } + + mod light { + use iced::{button, Background, Color, Vector}; + + pub struct Button; + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.11, 0.42, 0.87, + ))), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } + } + + mod dark { + use iced::{ + button, checkbox, container, progress_bar, radio, scrollable, + slider, text_input, Background, Color, + }; + + const SURFACE: Color = Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ); + + const ACCENT: Color = Color::from_rgb( + 0x6F as f32 / 255.0, + 0xFF as f32 / 255.0, + 0xE9 as f32 / 255.0, + ); + + const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, + ); + + const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, + ); + + pub struct Container; + + impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } + } + + pub struct Radio; + + impl radio::StyleSheet for Radio { + fn active(&self) -> radio::Style { + radio::Style { + background: Background::Color(SURFACE), + dot_color: ACTIVE, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self) -> radio::Style { + radio::Style { + background: Background::Color(Color { a: 0.5, ..SURFACE }), + ..self.active() + } + } + } + + pub struct TextInput; + + impl text_input::StyleSheet for TextInput { + fn active(&self) -> text_input::Style { + text_input::Style { + background: Background::Color(SURFACE), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + } + } + + fn focused(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: ACCENT, + ..self.active() + } + } + + fn hovered(&self) -> text_input::Style { + text_input::Style { + border_width: 1, + border_color: Color { a: 0.3, ..ACCENT }, + ..self.focused() + } + } + + fn placeholder_color(&self) -> Color { + Color::from_rgb(0.4, 0.4, 0.4) + } + + fn value_color(&self) -> Color { + Color::WHITE + } + } + + pub struct Button; + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } + } + + pub struct Scrollable; + + impl scrollable::StyleSheet for Scrollable { + fn active(&self) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color(SURFACE)), + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: ACTIVE, + border_radius: 2, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> scrollable::Scrollbar { + let active = self.active(); + + scrollable::Scrollbar { + background: Some(Background::Color(Color { + a: 0.5, + ..SURFACE + })), + scroller: scrollable::Scroller { + color: HOVERED, + ..active.scroller + }, + ..active + } + } + + fn dragging(&self) -> scrollable::Scrollbar { + let hovered = self.hovered(); + + scrollable::Scrollbar { + scroller: scrollable::Scroller { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..hovered.scroller + }, + ..hovered + } + } + } + + pub struct Slider; + + impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } + } + + pub struct ProgressBar; + + impl progress_bar::StyleSheet for ProgressBar { + fn style(&self) -> progress_bar::Style { + progress_bar::Style { + background: Background::Color(SURFACE), + bar: Background::Color(ACTIVE), + border_radius: 10, + } + } + } + + pub struct Checkbox; + + impl checkbox::StyleSheet for Checkbox { + fn active(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(if is_checked { + ACTIVE + } else { + SURFACE + }), + checkmark_color: Color::WHITE, + border_radius: 2, + border_width: 1, + border_color: ACTIVE, + } + } + + fn hovered(&self, is_checked: bool) -> checkbox::Style { + checkbox::Style { + background: Background::Color(Color { + a: 0.8, + ..if is_checked { ACTIVE } else { SURFACE } + }), + ..self.active(is_checked) + } + } + } + } +} diff --git a/examples/svg.rs b/examples/svg.rs deleted file mode 100644 index 1895039d..00000000 --- a/examples/svg.rs +++ /dev/null @@ -1,54 +0,0 @@ -use iced::{Container, Element, Length, Sandbox, Settings}; - -pub fn main() { - Tiger::run(Settings::default()) -} - -#[derive(Default)] -struct Tiger; - -impl Sandbox for Tiger { - type Message = (); - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("SVG - Iced") - } - - fn update(&mut self, _message: ()) {} - - fn view(&mut self) -> Element<()> { - #[cfg(feature = "svg")] - let content = { - use iced::{Column, Svg}; - - Column::new().padding(20).push( - Svg::new(format!( - "{}/examples/resources/tiger.svg", - env!("CARGO_MANIFEST_DIR") - )) - .width(Length::Fill) - .height(Length::Fill), - ) - }; - - #[cfg(not(feature = "svg"))] - let content = { - use iced::{HorizontalAlignment, Text}; - - Text::new("You need to enable the `svg` feature!") - .horizontal_alignment(HorizontalAlignment::Center) - .size(30) - }; - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml new file mode 100644 index 00000000..d8f83ac2 --- /dev/null +++ b/examples/svg/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "svg" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["svg"] } diff --git a/examples/svg/resources/tiger.svg b/examples/svg/resources/tiger.svg new file mode 100644 index 00000000..679edec2 --- /dev/null +++ b/examples/svg/resources/tiger.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs new file mode 100644 index 00000000..57358e24 --- /dev/null +++ b/examples/svg/src/main.rs @@ -0,0 +1,37 @@ +use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg}; + +pub fn main() { + Tiger::run(Settings::default()) +} + +#[derive(Default)] +struct Tiger; + +impl Sandbox for Tiger { + type Message = (); + + fn new() -> Self { + Self::default() + } + + fn title(&self) -> String { + String::from("SVG - Iced") + } + + fn update(&mut self, _message: ()) {} + + fn view(&mut self) -> Element<()> { + let content = Column::new().padding(20).push( + Svg::new(format!("{}/tiger.svg", env!("CARGO_MANIFEST_DIR"))) + .width(Length::Fill) + .height(Length::Fill), + ); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/todos.rs b/examples/todos.rs deleted file mode 100644 index 06595a1e..00000000 --- a/examples/todos.rs +++ /dev/null @@ -1,615 +0,0 @@ -use iced::{ - button, scrollable, text_input, Align, Application, Button, Checkbox, - Column, Command, Container, Element, Font, HorizontalAlignment, Length, - Row, Scrollable, Settings, Text, TextInput, -}; -use serde::{Deserialize, Serialize}; - -pub fn main() { - Todos::run(Settings::default()) -} - -#[derive(Debug)] -enum Todos { - Loading, - Loaded(State), -} - -#[derive(Debug, Default)] -struct State { - scroll: scrollable::State, - input: text_input::State, - input_value: String, - filter: Filter, - tasks: Vec, - controls: Controls, - dirty: bool, - saving: bool, -} - -#[derive(Debug, Clone)] -enum Message { - Loaded(Result), - Saved(Result<(), SaveError>), - InputChanged(String), - CreateTask, - FilterChanged(Filter), - TaskMessage(usize, TaskMessage), -} - -impl Application for Todos { - type Executor = iced_futures::executor::AsyncStd; - type Message = Message; - - fn new() -> (Todos, Command) { - ( - Todos::Loading, - Command::perform(SavedState::load(), Message::Loaded), - ) - } - - fn title(&self) -> String { - let dirty = match self { - Todos::Loading => false, - Todos::Loaded(state) => state.dirty, - }; - - format!("Todos{} - Iced", if dirty { "*" } else { "" }) - } - - fn update(&mut self, message: Message) -> Command { - match self { - Todos::Loading => { - match message { - Message::Loaded(Ok(state)) => { - *self = Todos::Loaded(State { - input_value: state.input_value, - filter: state.filter, - tasks: state.tasks, - ..State::default() - }); - } - Message::Loaded(Err(_)) => { - *self = Todos::Loaded(State::default()); - } - _ => {} - } - - Command::none() - } - Todos::Loaded(state) => { - let mut saved = false; - - match message { - Message::InputChanged(value) => { - state.input_value = value; - } - Message::CreateTask => { - if !state.input_value.is_empty() { - state - .tasks - .push(Task::new(state.input_value.clone())); - state.input_value.clear(); - } - } - Message::FilterChanged(filter) => { - state.filter = filter; - } - Message::TaskMessage(i, TaskMessage::Delete) => { - state.tasks.remove(i); - } - Message::TaskMessage(i, task_message) => { - if let Some(task) = state.tasks.get_mut(i) { - task.update(task_message); - } - } - Message::Saved(_) => { - state.saving = false; - saved = true; - } - _ => {} - } - - if !saved { - state.dirty = true; - } - - if state.dirty && !state.saving { - state.dirty = false; - state.saving = true; - - Command::perform( - SavedState { - input_value: state.input_value.clone(), - filter: state.filter, - tasks: state.tasks.clone(), - } - .save(), - Message::Saved, - ) - } else { - Command::none() - } - } - } - } - - fn view(&mut self) -> Element { - match self { - Todos::Loading => loading_message(), - Todos::Loaded(State { - scroll, - input, - input_value, - filter, - tasks, - controls, - .. - }) => { - let title = Text::new("todos") - .width(Length::Fill) - .size(100) - .color([0.5, 0.5, 0.5]) - .horizontal_alignment(HorizontalAlignment::Center); - - let input = TextInput::new( - input, - "What needs to be done?", - input_value, - Message::InputChanged, - ) - .padding(15) - .size(30) - .on_submit(Message::CreateTask); - - let controls = controls.view(&tasks, *filter); - let filtered_tasks = - tasks.iter().filter(|task| filter.matches(task)); - - let tasks: Element<_> = if filtered_tasks.count() > 0 { - tasks - .iter_mut() - .enumerate() - .filter(|(_, task)| filter.matches(task)) - .fold(Column::new().spacing(20), |column, (i, task)| { - column.push(task.view().map(move |message| { - Message::TaskMessage(i, message) - })) - }) - .into() - } else { - empty_message(match filter { - Filter::All => "You have not created a task yet...", - Filter::Active => "All your tasks are done! :D", - Filter::Completed => { - "You have not completed a task yet..." - } - }) - }; - - let content = Column::new() - .max_width(800) - .spacing(20) - .push(title) - .push(input) - .push(controls) - .push(tasks); - - Scrollable::new(scroll) - .padding(40) - .push( - Container::new(content).width(Length::Fill).center_x(), - ) - .into() - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Task { - description: String, - completed: bool, - - #[serde(skip)] - state: TaskState, -} - -#[derive(Debug, Clone)] -pub enum TaskState { - Idle { - edit_button: button::State, - }, - Editing { - text_input: text_input::State, - delete_button: button::State, - }, -} - -impl Default for TaskState { - fn default() -> Self { - TaskState::Idle { - edit_button: button::State::new(), - } - } -} - -#[derive(Debug, Clone)] -pub enum TaskMessage { - Completed(bool), - Edit, - DescriptionEdited(String), - FinishEdition, - Delete, -} - -impl Task { - fn new(description: String) -> Self { - Task { - description, - completed: false, - state: TaskState::Idle { - edit_button: button::State::new(), - }, - } - } - - fn update(&mut self, message: TaskMessage) { - match message { - TaskMessage::Completed(completed) => { - self.completed = completed; - } - TaskMessage::Edit => { - self.state = TaskState::Editing { - text_input: text_input::State::focused(), - delete_button: button::State::new(), - }; - } - TaskMessage::DescriptionEdited(new_description) => { - self.description = new_description; - } - TaskMessage::FinishEdition => { - if !self.description.is_empty() { - self.state = TaskState::Idle { - edit_button: button::State::new(), - } - } - } - TaskMessage::Delete => {} - } - } - - fn view(&mut self) -> Element { - match &mut self.state { - TaskState::Idle { edit_button } => { - let checkbox = Checkbox::new( - self.completed, - &self.description, - TaskMessage::Completed, - ) - .width(Length::Fill); - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(checkbox) - .push( - Button::new(edit_button, edit_icon()) - .on_press(TaskMessage::Edit) - .padding(10) - .style(style::Button::Icon), - ) - .into() - } - TaskState::Editing { - text_input, - delete_button, - } => { - let text_input = TextInput::new( - text_input, - "Describe your task...", - &self.description, - TaskMessage::DescriptionEdited, - ) - .on_submit(TaskMessage::FinishEdition) - .padding(10); - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push(text_input) - .push( - Button::new( - delete_button, - Row::new() - .spacing(10) - .push(delete_icon()) - .push(Text::new("Delete")), - ) - .on_press(TaskMessage::Delete) - .padding(10) - .style(style::Button::Destructive), - ) - .into() - } - } - } -} - -#[derive(Debug, Default, Clone)] -pub struct Controls { - all_button: button::State, - active_button: button::State, - completed_button: button::State, -} - -impl Controls { - fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row { - let Controls { - all_button, - active_button, - completed_button, - } = self; - - let tasks_left = tasks.iter().filter(|task| !task.completed).count(); - - let filter_button = |state, label, filter, current_filter| { - let label = Text::new(label).size(16); - let button = - Button::new(state, label).style(style::Button::Filter { - selected: filter == current_filter, - }); - - button.on_press(Message::FilterChanged(filter)).padding(8) - }; - - Row::new() - .spacing(20) - .align_items(Align::Center) - .push( - Text::new(&format!( - "{} {} left", - tasks_left, - if tasks_left == 1 { "task" } else { "tasks" } - )) - .width(Length::Fill) - .size(16), - ) - .push( - Row::new() - .spacing(10) - .push(filter_button( - all_button, - "All", - Filter::All, - current_filter, - )) - .push(filter_button( - active_button, - "Active", - Filter::Active, - current_filter, - )) - .push(filter_button( - completed_button, - "Completed", - Filter::Completed, - current_filter, - )), - ) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum Filter { - All, - Active, - Completed, -} - -impl Default for Filter { - fn default() -> Self { - Filter::All - } -} - -impl Filter { - fn matches(&self, task: &Task) -> bool { - match self { - Filter::All => true, - Filter::Active => !task.completed, - Filter::Completed => task.completed, - } - } -} - -fn loading_message() -> Element<'static, Message> { - Container::new( - Text::new("Loading...") - .horizontal_alignment(HorizontalAlignment::Center) - .size(50), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_y() - .into() -} - -fn empty_message(message: &str) -> Element<'static, Message> { - Container::new( - Text::new(message) - .size(25) - .horizontal_alignment(HorizontalAlignment::Center) - .color([0.7, 0.7, 0.7]), - ) - .width(Length::Fill) - .height(Length::Units(200)) - .center_y() - .into() -} - -// Fonts -const ICONS: Font = Font::External { - name: "Icons", - bytes: include_bytes!("resources/icons.ttf"), -}; - -fn icon(unicode: char) -> Text { - Text::new(&unicode.to_string()) - .font(ICONS) - .width(Length::Units(20)) - .horizontal_alignment(HorizontalAlignment::Center) - .size(20) -} - -fn edit_icon() -> Text { - icon('\u{F303}') -} - -fn delete_icon() -> Text { - icon('\u{F1F8}') -} - -// Persistence -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SavedState { - input_value: String, - filter: Filter, - tasks: Vec, -} - -#[derive(Debug, Clone)] -enum LoadError { - FileError, - FormatError, -} - -#[derive(Debug, Clone)] -enum SaveError { - DirectoryError, - FileError, - WriteError, - FormatError, -} - -impl SavedState { - fn path() -> std::path::PathBuf { - let mut path = if let Some(project_dirs) = - directories::ProjectDirs::from("rs", "Iced", "Todos") - { - project_dirs.data_dir().into() - } else { - std::env::current_dir().unwrap_or(std::path::PathBuf::new()) - }; - - path.push("todos.json"); - - path - } - - async fn load() -> Result { - use async_std::prelude::*; - - let mut contents = String::new(); - - let mut file = async_std::fs::File::open(Self::path()) - .await - .map_err(|_| LoadError::FileError)?; - - file.read_to_string(&mut contents) - .await - .map_err(|_| LoadError::FileError)?; - - serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) - } - - async fn save(self) -> Result<(), SaveError> { - use async_std::prelude::*; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::FormatError)?; - - let path = Self::path(); - - if let Some(dir) = path.parent() { - async_std::fs::create_dir_all(dir) - .await - .map_err(|_| SaveError::DirectoryError)?; - } - - { - let mut file = async_std::fs::File::create(path) - .await - .map_err(|_| SaveError::FileError)?; - - file.write_all(json.as_bytes()) - .await - .map_err(|_| SaveError::WriteError)?; - } - - // This is a simple way to save at most once every couple seconds - async_std::task::sleep(std::time::Duration::from_secs(2)).await; - - Ok(()) - } -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Filter { selected: bool }, - Icon, - Destructive, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - match self { - Button::Filter { selected } => { - if *selected { - button::Style { - background: Some(Background::Color( - Color::from_rgb(0.2, 0.2, 0.7), - )), - border_radius: 10, - text_color: Color::WHITE, - ..button::Style::default() - } - } else { - button::Style::default() - } - } - Button::Icon => button::Style { - text_color: Color::from_rgb(0.5, 0.5, 0.5), - ..button::Style::default() - }, - Button::Destructive => button::Style { - background: Some(Background::Color(Color::from_rgb( - 0.8, 0.2, 0.2, - ))), - border_radius: 5, - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 1.0), - ..button::Style::default() - }, - } - } - - fn hovered(&self) -> button::Style { - let active = self.active(); - - button::Style { - text_color: match self { - Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), - Button::Filter { selected } if !selected => { - Color::from_rgb(0.2, 0.2, 0.7) - } - _ => active.text_color, - }, - shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), - ..active - } - } - } -} diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml new file mode 100644 index 00000000..53a135e6 --- /dev/null +++ b/examples/todos/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced = { path = "../.." } +iced_futures = { path = "../../futures", features = ["async-std"] } +async-std = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +directories = "2.0" diff --git a/examples/todos/fonts/icons.ttf b/examples/todos/fonts/icons.ttf new file mode 100644 index 00000000..4498299d Binary files /dev/null and b/examples/todos/fonts/icons.ttf differ diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs new file mode 100644 index 00000000..c6ddf2ea --- /dev/null +++ b/examples/todos/src/main.rs @@ -0,0 +1,615 @@ +use iced::{ + button, scrollable, text_input, Align, Application, Button, Checkbox, + Column, Command, Container, Element, Font, HorizontalAlignment, Length, + Row, Scrollable, Settings, Text, TextInput, +}; +use serde::{Deserialize, Serialize}; + +pub fn main() { + Todos::run(Settings::default()) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + scroll: scrollable::State, + input: text_input::State, + input_value: String, + filter: Filter, + tasks: Vec, + controls: Controls, + dirty: bool, + saving: bool, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), +} + +impl Application for Todos { + type Executor = iced_futures::executor::AsyncStd; + type Message = Message; + + fn new() -> (Todos, Command) { + ( + Todos::Loading, + Command::perform(SavedState::load(), Message::Loaded), + ) + } + + fn title(&self) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Command { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + Command::none() + } + Todos::Loaded(state) => { + let mut saved = false; + + match message { + Message::InputChanged(value) => { + state.input_value = value; + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(Task::new(state.input_value.clone())); + state.input_value.clear(); + } + } + Message::FilterChanged(filter) => { + state.filter = filter; + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + task.update(task_message); + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + } + _ => {} + } + + if !saved { + state.dirty = true; + } + + if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Command::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Command::none() + } + } + } + } + + fn view(&mut self) -> Element { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + scroll, + input, + input_value, + filter, + tasks, + controls, + .. + }) => { + let title = Text::new("todos") + .width(Length::Fill) + .size(100) + .color([0.5, 0.5, 0.5]) + .horizontal_alignment(HorizontalAlignment::Center); + + let input = TextInput::new( + input, + "What needs to be done?", + input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreateTask); + + let controls = controls.view(&tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + tasks + .iter_mut() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .fold(Column::new().spacing(20), |column, (i, task)| { + column.push(task.view().map(move |message| { + Message::TaskMessage(i, message) + })) + }) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(title) + .push(input) + .push(controls) + .push(tasks); + + Scrollable::new(scroll) + .padding(40) + .push( + Container::new(content).width(Length::Fill).center_x(), + ) + .into() + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle { + edit_button: button::State, + }, + Editing { + text_input: text_input::State, + delete_button: button::State, + }, +} + +impl Default for TaskState { + fn default() -> Self { + TaskState::Idle { + edit_button: button::State::new(), + } + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl Task { + fn new(description: String) -> Self { + Task { + description, + completed: false, + state: TaskState::Idle { + edit_button: button::State::new(), + }, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing { + text_input: text_input::State::focused(), + delete_button: button::State::new(), + }; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle { + edit_button: button::State::new(), + } + } + } + TaskMessage::Delete => {} + } + } + + fn view(&mut self) -> Element { + match &mut self.state { + TaskState::Idle { edit_button } => { + let checkbox = Checkbox::new( + self.completed, + &self.description, + TaskMessage::Completed, + ) + .width(Length::Fill); + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(checkbox) + .push( + Button::new(edit_button, edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(style::Button::Icon), + ) + .into() + } + TaskState::Editing { + text_input, + delete_button, + } => { + let text_input = TextInput::new( + text_input, + "Describe your task...", + &self.description, + TaskMessage::DescriptionEdited, + ) + .on_submit(TaskMessage::FinishEdition) + .padding(10); + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push(text_input) + .push( + Button::new( + delete_button, + Row::new() + .spacing(10) + .push(delete_icon()) + .push(Text::new("Delete")), + ) + .on_press(TaskMessage::Delete) + .padding(10) + .style(style::Button::Destructive), + ) + .into() + } + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Controls { + all_button: button::State, + active_button: button::State, + completed_button: button::State, +} + +impl Controls { + fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row { + let Controls { + all_button, + active_button, + completed_button, + } = self; + + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |state, label, filter, current_filter| { + let label = Text::new(label).size(16); + let button = + Button::new(state, label).style(style::Button::Filter { + selected: filter == current_filter, + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + Row::new() + .spacing(20) + .align_items(Align::Center) + .push( + Text::new(&format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + ) + .push( + Row::new() + .spacing(10) + .push(filter_button( + all_button, + "All", + Filter::All, + current_filter, + )) + .push(filter_button( + active_button, + "Active", + Filter::Active, + current_filter, + )) + .push(filter_button( + completed_button, + "Completed", + Filter::Completed, + current_filter, + )), + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &Task) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message() -> Element<'static, Message> { + Container::new( + Text::new("Loading...") + .horizontal_alignment(HorizontalAlignment::Center) + .size(50), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_y() + .into() +} + +fn empty_message(message: &str) -> Element<'static, Message> { + Container::new( + Text::new(message) + .size(25) + .horizontal_alignment(HorizontalAlignment::Center) + .color([0.7, 0.7, 0.7]), + ) + .width(Length::Fill) + .height(Length::Units(200)) + .center_y() + .into() +} + +// Fonts +const ICONS: Font = Font::External { + name: "Icons", + bytes: include_bytes!("../fonts/icons.ttf"), +}; + +fn icon(unicode: char) -> Text { + Text::new(&unicode.to_string()) + .font(ICONS) + .width(Length::Units(20)) + .horizontal_alignment(HorizontalAlignment::Center) + .size(20) +} + +fn edit_icon() -> Text { + icon('\u{F303}') +} + +fn delete_icon() -> Text { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec, +} + +#[derive(Debug, Clone)] +enum LoadError { + FileError, + FormatError, +} + +#[derive(Debug, Clone)] +enum SaveError { + DirectoryError, + FileError, + WriteError, + FormatError, +} + +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or(std::path::PathBuf::new()) + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result { + use async_std::prelude::*; + + let mut contents = String::new(); + + let mut file = async_std::fs::File::open(Self::path()) + .await + .map_err(|_| LoadError::FileError)?; + + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::FileError)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) + } + + async fn save(self) -> Result<(), SaveError> { + use async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::FormatError)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::DirectoryError)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::FileError)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::WriteError)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Filter { selected: bool }, + Icon, + Destructive, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + match self { + Button::Filter { selected } => { + if *selected { + button::Style { + background: Some(Background::Color( + Color::from_rgb(0.2, 0.2, 0.7), + )), + border_radius: 10, + text_color: Color::WHITE, + ..button::Style::default() + } + } else { + button::Style::default() + } + } + Button::Icon => button::Style { + text_color: Color::from_rgb(0.5, 0.5, 0.5), + ..button::Style::default() + }, + Button::Destructive => button::Style { + background: Some(Background::Color(Color::from_rgb( + 0.8, 0.2, 0.2, + ))), + border_radius: 5, + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 1.0), + ..button::Style::default() + }, + } + } + + fn hovered(&self) -> button::Style { + let active = self.active(); + + button::Style { + text_color: match self { + Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), + Button::Filter { selected } if !selected => { + Color::from_rgb(0.2, 0.2, 0.7) + } + _ => active.text_color, + }, + shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), + ..active + } + } + } +} diff --git a/examples/tour.rs b/examples/tour.rs deleted file mode 100644 index b0ee4d96..00000000 --- a/examples/tour.rs +++ /dev/null @@ -1,794 +0,0 @@ -use iced::{ - button, scrollable, slider, text_input, Button, Checkbox, Color, Column, - Container, Element, HorizontalAlignment, Image, Length, Radio, Row, - Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, -}; - -pub fn main() { - env_logger::init(); - - Tour::run(Settings::default()) -} - -pub struct Tour { - steps: Steps, - scroll: scrollable::State, - back_button: button::State, - next_button: button::State, - debug: bool, -} - -impl Sandbox for Tour { - type Message = Message; - - fn new() -> Tour { - Tour { - steps: Steps::new(), - scroll: scrollable::State::new(), - back_button: button::State::new(), - next_button: button::State::new(), - debug: false, - } - } - - fn title(&self) -> String { - format!("{} - Iced", self.steps.title()) - } - - fn update(&mut self, event: Message) { - match event { - Message::BackPressed => { - self.steps.go_back(); - } - Message::NextPressed => { - self.steps.advance(); - } - Message::StepMessage(step_msg) => { - self.steps.update(step_msg, &mut self.debug); - } - } - } - - fn view(&mut self) -> Element { - let Tour { - steps, - scroll, - back_button, - next_button, - .. - } = self; - - let mut controls = Row::new(); - - if steps.has_previous() { - controls = controls.push( - button(back_button, "Back") - .on_press(Message::BackPressed) - .style(style::Button::Secondary), - ); - } - - controls = controls.push(Space::with_width(Length::Fill)); - - if steps.can_continue() { - controls = controls.push( - button(next_button, "Next") - .on_press(Message::NextPressed) - .style(style::Button::Primary), - ); - } - - let content: Element<_> = Column::new() - .max_width(540) - .spacing(20) - .padding(20) - .push(steps.view(self.debug).map(Message::StepMessage)) - .push(controls) - .into(); - - let content = if self.debug { - content.explain(Color::BLACK) - } else { - content - }; - - let scrollable = Scrollable::new(scroll) - .push(Container::new(content).width(Length::Fill).center_x()); - - Container::new(scrollable) - .height(Length::Fill) - .center_y() - .into() - } -} - -#[derive(Debug, Clone)] -pub enum Message { - BackPressed, - NextPressed, - StepMessage(StepMessage), -} - -struct Steps { - steps: Vec, - current: usize, -} - -impl Steps { - fn new() -> Steps { - Steps { - steps: vec![ - Step::Welcome, - Step::Slider { - state: slider::State::new(), - value: 50, - }, - Step::RowsAndColumns { - layout: Layout::Row, - spacing_slider: slider::State::new(), - spacing: 20, - }, - Step::Text { - size_slider: slider::State::new(), - size: 30, - color_sliders: [slider::State::new(); 3], - color: Color::BLACK, - }, - Step::Radio { selection: None }, - Step::Image { - width: 300, - slider: slider::State::new(), - }, - Step::Scrollable, - Step::TextInput { - value: String::new(), - is_secure: false, - state: text_input::State::new(), - }, - Step::Debugger, - Step::End, - ], - current: 0, - } - } - - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - self.steps[self.current].update(msg, debug); - } - - fn view(&mut self, debug: bool) -> Element { - self.steps[self.current].view(debug) - } - - fn advance(&mut self) { - if self.can_continue() { - self.current += 1; - } - } - - fn go_back(&mut self) { - if self.has_previous() { - self.current -= 1; - } - } - - fn has_previous(&self) -> bool { - self.current > 0 - } - - fn can_continue(&self) -> bool { - self.current + 1 < self.steps.len() - && self.steps[self.current].can_continue() - } - - fn title(&self) -> &str { - self.steps[self.current].title() - } -} - -enum Step { - Welcome, - Slider { - state: slider::State, - value: u16, - }, - RowsAndColumns { - layout: Layout, - spacing_slider: slider::State, - spacing: u16, - }, - Text { - size_slider: slider::State, - size: u16, - color_sliders: [slider::State; 3], - color: Color, - }, - Radio { - selection: Option, - }, - Image { - width: u16, - slider: slider::State, - }, - Scrollable, - TextInput { - value: String, - is_secure: bool, - state: text_input::State, - }, - Debugger, - End, -} - -#[derive(Debug, Clone)] -pub enum StepMessage { - SliderChanged(f32), - LayoutChanged(Layout), - SpacingChanged(f32), - TextSizeChanged(f32), - TextColorChanged(Color), - LanguageSelected(Language), - ImageWidthChanged(f32), - InputChanged(String), - ToggleSecureInput(bool), - DebugToggled(bool), -} - -impl<'a> Step { - fn update(&mut self, msg: StepMessage, debug: &mut bool) { - match msg { - StepMessage::DebugToggled(value) => { - if let Step::Debugger = self { - *debug = value; - } - } - StepMessage::LanguageSelected(language) => { - if let Step::Radio { selection } = self { - *selection = Some(language); - } - } - StepMessage::SliderChanged(new_value) => { - if let Step::Slider { value, .. } = self { - *value = new_value.round() as u16; - } - } - StepMessage::TextSizeChanged(new_size) => { - if let Step::Text { size, .. } = self { - *size = new_size.round() as u16; - } - } - StepMessage::TextColorChanged(new_color) => { - if let Step::Text { color, .. } = self { - *color = new_color; - } - } - StepMessage::LayoutChanged(new_layout) => { - if let Step::RowsAndColumns { layout, .. } = self { - *layout = new_layout; - } - } - StepMessage::SpacingChanged(new_spacing) => { - if let Step::RowsAndColumns { spacing, .. } = self { - *spacing = new_spacing.round() as u16; - } - } - StepMessage::ImageWidthChanged(new_width) => { - if let Step::Image { width, .. } = self { - *width = new_width.round() as u16; - } - } - StepMessage::InputChanged(new_value) => { - if let Step::TextInput { value, .. } = self { - *value = new_value; - } - } - StepMessage::ToggleSecureInput(toggle) => { - if let Step::TextInput { is_secure, .. } = self { - *is_secure = toggle; - } - } - }; - } - - fn title(&self) -> &str { - match self { - Step::Welcome => "Welcome", - Step::Radio { .. } => "Radio button", - Step::Slider { .. } => "Slider", - Step::Text { .. } => "Text", - Step::Image { .. } => "Image", - Step::RowsAndColumns { .. } => "Rows and columns", - Step::Scrollable => "Scrollable", - Step::TextInput { .. } => "Text input", - Step::Debugger => "Debugger", - Step::End => "End", - } - } - - fn can_continue(&self) -> bool { - match self { - Step::Welcome => true, - Step::Radio { selection } => *selection == Some(Language::Rust), - Step::Slider { .. } => true, - Step::Text { .. } => true, - Step::Image { .. } => true, - Step::RowsAndColumns { .. } => true, - Step::Scrollable => true, - Step::TextInput { value, .. } => !value.is_empty(), - Step::Debugger => true, - Step::End => false, - } - } - - fn view(&mut self, debug: bool) -> Element { - match self { - Step::Welcome => Self::welcome(), - Step::Radio { selection } => Self::radio(*selection), - Step::Slider { state, value } => Self::slider(state, *value), - Step::Text { - size_slider, - size, - color_sliders, - color, - } => Self::text(size_slider, *size, color_sliders, *color), - Step::Image { width, slider } => Self::image(*width, slider), - Step::RowsAndColumns { - layout, - spacing_slider, - spacing, - } => Self::rows_and_columns(*layout, spacing_slider, *spacing), - Step::Scrollable => Self::scrollable(), - Step::TextInput { - value, - is_secure, - state, - } => Self::text_input(value, *is_secure, state), - Step::Debugger => Self::debugger(debug), - Step::End => Self::end(), - } - .into() - } - - fn container(title: &str) -> Column<'a, StepMessage> { - Column::new().spacing(20).push(Text::new(title).size(50)) - } - - fn welcome() -> Column<'a, StepMessage> { - Self::container("Welcome!") - .push(Text::new( - "This is a simple tour meant to showcase a bunch of widgets \ - that can be easily implemented on top of Iced.", - )) - .push(Text::new( - "Iced is a cross-platform GUI library for Rust focused on \ - simplicity and type-safety. It is heavily inspired by Elm.", - )) - .push(Text::new( - "It was originally born as part of Coffee, an opinionated \ - 2D game engine for Rust.", - )) - .push(Text::new( - "On native platforms, Iced provides by default a renderer \ - built on top of wgpu, a graphics library supporting Vulkan, \ - Metal, DX11, and DX12.", - )) - .push(Text::new( - "Additionally, this tour can also run on WebAssembly thanks \ - to dodrio, an experimental VDOM library for Rust.", - )) - .push(Text::new( - "You will need to interact with the UI in order to reach the \ - end!", - )) - } - - fn slider( - state: &'a mut slider::State, - value: u16, - ) -> Column<'a, StepMessage> { - Self::container("Slider") - .push(Text::new( - "A slider allows you to smoothly select a value from a range \ - of values.", - )) - .push(Text::new( - "The following slider lets you choose an integer from \ - 0 to 100:", - )) - .push(Slider::new( - state, - 0.0..=100.0, - value as f32, - StepMessage::SliderChanged, - )) - .push( - Text::new(&value.to_string()) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn rows_and_columns( - layout: Layout, - spacing_slider: &'a mut slider::State, - spacing: u16, - ) -> Column<'a, StepMessage> { - let row_radio = Radio::new( - Layout::Row, - "Row", - Some(layout), - StepMessage::LayoutChanged, - ); - - let column_radio = Radio::new( - Layout::Column, - "Column", - Some(layout), - StepMessage::LayoutChanged, - ); - - let layout_section: Element<_> = match layout { - Layout::Row => Row::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - Layout::Column => Column::new() - .spacing(spacing) - .push(row_radio) - .push(column_radio) - .into(), - }; - - let spacing_section = Column::new() - .spacing(10) - .push(Slider::new( - spacing_slider, - 0.0..=80.0, - spacing as f32, - StepMessage::SpacingChanged, - )) - .push( - Text::new(&format!("{} px", spacing)) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ); - - Self::container("Rows and columns") - .spacing(spacing) - .push(Text::new( - "Iced uses a layout model based on flexbox to position UI \ - elements.", - )) - .push(Text::new( - "Rows and columns can be used to distribute content \ - horizontally or vertically, respectively.", - )) - .push(layout_section) - .push(Text::new( - "You can also easily change the spacing between elements:", - )) - .push(spacing_section) - } - - fn text( - size_slider: &'a mut slider::State, - size: u16, - color_sliders: &'a mut [slider::State; 3], - color: Color, - ) -> Column<'a, StepMessage> { - let size_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("You can change its size:")) - .push( - Text::new(&format!("This text is {} pixels", size)).size(size), - ) - .push(Slider::new( - size_slider, - 10.0..=70.0, - size as f32, - StepMessage::TextSizeChanged, - )); - - let [red, green, blue] = color_sliders; - let color_section = Column::new() - .padding(20) - .spacing(20) - .push(Text::new("And its color:")) - .push(Text::new(&format!("{:?}", color)).color(color)) - .push( - Row::new() - .spacing(10) - .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { - StepMessage::TextColorChanged(Color { r, ..color }) - })) - .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { - StepMessage::TextColorChanged(Color { g, ..color }) - })) - .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { - StepMessage::TextColorChanged(Color { b, ..color }) - })), - ); - - Self::container("Text") - .push(Text::new( - "Text is probably the most essential widget for your UI. \ - It will try to adapt to the dimensions of its container.", - )) - .push(size_section) - .push(color_section) - } - - fn radio(selection: Option) -> Column<'a, StepMessage> { - let question = Column::new() - .padding(20) - .spacing(10) - .push(Text::new("Iced is written in...").size(24)) - .push(Language::all().iter().cloned().fold( - Column::new().padding(10).spacing(20), - |choices, language| { - choices.push(Radio::new( - language, - language.into(), - selection, - StepMessage::LanguageSelected, - )) - }, - )); - - Self::container("Radio button") - .push(Text::new( - "A radio button is normally used to represent a choice... \ - Surprise test!", - )) - .push(question) - .push(Text::new( - "Iced works very well with iterators! The list above is \ - basically created by folding a column over the different \ - choices, creating a radio button for each one of them!", - )) - } - - fn image( - width: u16, - slider: &'a mut slider::State, - ) -> Column<'a, StepMessage> { - Self::container("Image") - .push(Text::new("An image that tries to keep its aspect ratio.")) - .push(ferris(width)) - .push(Slider::new( - slider, - 100.0..=500.0, - width as f32, - StepMessage::ImageWidthChanged, - )) - .push( - Text::new(&format!("Width: {} px", width.to_string())) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn scrollable() -> Column<'a, StepMessage> { - Self::container("Scrollable") - .push(Text::new( - "Iced supports scrollable content. Try it out! Find the \ - button further below.", - )) - .push( - Text::new( - "Tip: You can use the scrollbar to scroll down faster!", - ) - .size(16), - ) - .push(Column::new().height(Length::Units(4096))) - .push( - Text::new("You are halfway there!") - .width(Length::Fill) - .size(30) - .horizontal_alignment(HorizontalAlignment::Center), - ) - .push(Column::new().height(Length::Units(4096))) - .push(ferris(300)) - .push( - Text::new("You made it!") - .width(Length::Fill) - .size(50) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn text_input( - value: &str, - is_secure: bool, - state: &'a mut text_input::State, - ) -> Column<'a, StepMessage> { - let text_input = TextInput::new( - state, - "Type something to continue...", - value, - StepMessage::InputChanged, - ) - .padding(10) - .size(30); - Self::container("Text input") - .push(Text::new( - "Use a text input to ask for different kinds of information.", - )) - .push(if is_secure { - text_input.password() - } else { - text_input - }) - .push(Checkbox::new( - is_secure, - "Enable password mode", - StepMessage::ToggleSecureInput, - )) - .push(Text::new( - "A text input produces a message every time it changes. It is \ - very easy to keep track of its contents:", - )) - .push( - Text::new(if value.is_empty() { - "You have not typed anything yet..." - } else { - value - }) - .width(Length::Fill) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } - - fn debugger(debug: bool) -> Column<'a, StepMessage> { - Self::container("Debugger") - .push(Text::new( - "You can ask Iced to visually explain the layouting of the \ - different elements comprising your UI!", - )) - .push(Text::new( - "Give it a shot! Check the following checkbox to be able to \ - see element boundaries.", - )) - .push(if cfg!(target_arch = "wasm32") { - Element::new( - Text::new("Not available on web yet!") - .color([0.7, 0.7, 0.7]) - .horizontal_alignment(HorizontalAlignment::Center), - ) - } else { - Element::new(Checkbox::new( - debug, - "Explain layout", - StepMessage::DebugToggled, - )) - }) - .push(Text::new("Feel free to go back and take a look.")) - } - - fn end() -> Column<'a, StepMessage> { - Self::container("You reached the end!") - .push(Text::new( - "This tour will be updated as more features are added.", - )) - .push(Text::new("Make sure to keep an eye on it!")) - } -} - -fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { - Container::new( - // This should go away once we unify resource loading on native - // platforms - if cfg!(target_arch = "wasm32") { - Image::new("resources/ferris.png") - } else { - Image::new(format!( - "{}/examples/resources/ferris.png", - env!("CARGO_MANIFEST_DIR") - )) - } - .width(Length::Units(width)), - ) - .width(Length::Fill) - .center_x() -} - -fn button<'a, Message>( - state: &'a mut button::State, - label: &str, -) -> Button<'a, Message> { - Button::new( - state, - Text::new(label).horizontal_alignment(HorizontalAlignment::Center), - ) - .padding(12) - .min_width(100) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Language { - Rust, - Elm, - Ruby, - Haskell, - C, - Other, -} - -impl Language { - fn all() -> [Language; 6] { - [ - Language::C, - Language::Elm, - Language::Ruby, - Language::Haskell, - Language::Rust, - Language::Other, - ] - } -} - -impl From for &str { - fn from(language: Language) -> &'static str { - match language { - Language::Rust => "Rust", - Language::Elm => "Elm", - Language::Ruby => "Ruby", - Language::Haskell => "Haskell", - Language::C => "C", - Language::Other => "Other", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Layout { - Row, - Column, -} - -mod style { - use iced::{button, Background, Color, Vector}; - - pub enum Button { - Primary, - Secondary, - } - - impl button::StyleSheet for Button { - fn active(&self) -> button::Style { - button::Style { - background: Some(Background::Color(match self { - Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), - Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), - })), - border_radius: 12, - shadow_offset: Vector::new(1.0, 1.0), - text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), - ..button::Style::default() - } - } - - fn hovered(&self) -> button::Style { - button::Style { - text_color: Color::WHITE, - shadow_offset: Vector::new(1.0, 2.0), - ..self.active() - } - } - } -} - -// This should be gracefully handled by Iced in the future. Probably using our -// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at -// some point. -#[cfg(target_arch = "wasm32")] -mod wasm { - use wasm_bindgen::prelude::*; - - #[wasm_bindgen(start)] - pub fn run() { - super::main() - } -} diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml new file mode 100644 index 00000000..10c3f1da --- /dev/null +++ b/examples/tour/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tour" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } +env_logger = "0.7" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2.51" diff --git a/examples/tour/images/ferris.png b/examples/tour/images/ferris.png new file mode 100644 index 00000000..ebce1a14 Binary files /dev/null and b/examples/tour/images/ferris.png differ diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs new file mode 100644 index 00000000..43c7e50f --- /dev/null +++ b/examples/tour/src/main.rs @@ -0,0 +1,794 @@ +use iced::{ + button, scrollable, slider, text_input, Button, Checkbox, Color, Column, + Container, Element, HorizontalAlignment, Image, Length, Radio, Row, + Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, +}; + +pub fn main() { + env_logger::init(); + + Tour::run(Settings::default()) +} + +pub struct Tour { + steps: Steps, + scroll: scrollable::State, + back_button: button::State, + next_button: button::State, + debug: bool, +} + +impl Sandbox for Tour { + type Message = Message; + + fn new() -> Tour { + Tour { + steps: Steps::new(), + scroll: scrollable::State::new(), + back_button: button::State::new(), + next_button: button::State::new(), + debug: false, + } + } + + fn title(&self) -> String { + format!("{} - Iced", self.steps.title()) + } + + fn update(&mut self, event: Message) { + match event { + Message::BackPressed => { + self.steps.go_back(); + } + Message::NextPressed => { + self.steps.advance(); + } + Message::StepMessage(step_msg) => { + self.steps.update(step_msg, &mut self.debug); + } + } + } + + fn view(&mut self) -> Element { + let Tour { + steps, + scroll, + back_button, + next_button, + .. + } = self; + + let mut controls = Row::new(); + + if steps.has_previous() { + controls = controls.push( + button(back_button, "Back") + .on_press(Message::BackPressed) + .style(style::Button::Secondary), + ); + } + + controls = controls.push(Space::with_width(Length::Fill)); + + if steps.can_continue() { + controls = controls.push( + button(next_button, "Next") + .on_press(Message::NextPressed) + .style(style::Button::Primary), + ); + } + + let content: Element<_> = Column::new() + .max_width(540) + .spacing(20) + .padding(20) + .push(steps.view(self.debug).map(Message::StepMessage)) + .push(controls) + .into(); + + let content = if self.debug { + content.explain(Color::BLACK) + } else { + content + }; + + let scrollable = Scrollable::new(scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .height(Length::Fill) + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + BackPressed, + NextPressed, + StepMessage(StepMessage), +} + +struct Steps { + steps: Vec, + current: usize, +} + +impl Steps { + fn new() -> Steps { + Steps { + steps: vec![ + Step::Welcome, + Step::Slider { + state: slider::State::new(), + value: 50, + }, + Step::RowsAndColumns { + layout: Layout::Row, + spacing_slider: slider::State::new(), + spacing: 20, + }, + Step::Text { + size_slider: slider::State::new(), + size: 30, + color_sliders: [slider::State::new(); 3], + color: Color::BLACK, + }, + Step::Radio { selection: None }, + Step::Image { + width: 300, + slider: slider::State::new(), + }, + Step::Scrollable, + Step::TextInput { + value: String::new(), + is_secure: false, + state: text_input::State::new(), + }, + Step::Debugger, + Step::End, + ], + current: 0, + } + } + + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + self.steps[self.current].update(msg, debug); + } + + fn view(&mut self, debug: bool) -> Element { + self.steps[self.current].view(debug) + } + + fn advance(&mut self) { + if self.can_continue() { + self.current += 1; + } + } + + fn go_back(&mut self) { + if self.has_previous() { + self.current -= 1; + } + } + + fn has_previous(&self) -> bool { + self.current > 0 + } + + fn can_continue(&self) -> bool { + self.current + 1 < self.steps.len() + && self.steps[self.current].can_continue() + } + + fn title(&self) -> &str { + self.steps[self.current].title() + } +} + +enum Step { + Welcome, + Slider { + state: slider::State, + value: u16, + }, + RowsAndColumns { + layout: Layout, + spacing_slider: slider::State, + spacing: u16, + }, + Text { + size_slider: slider::State, + size: u16, + color_sliders: [slider::State; 3], + color: Color, + }, + Radio { + selection: Option, + }, + Image { + width: u16, + slider: slider::State, + }, + Scrollable, + TextInput { + value: String, + is_secure: bool, + state: text_input::State, + }, + Debugger, + End, +} + +#[derive(Debug, Clone)] +pub enum StepMessage { + SliderChanged(f32), + LayoutChanged(Layout), + SpacingChanged(f32), + TextSizeChanged(f32), + TextColorChanged(Color), + LanguageSelected(Language), + ImageWidthChanged(f32), + InputChanged(String), + ToggleSecureInput(bool), + DebugToggled(bool), +} + +impl<'a> Step { + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + match msg { + StepMessage::DebugToggled(value) => { + if let Step::Debugger = self { + *debug = value; + } + } + StepMessage::LanguageSelected(language) => { + if let Step::Radio { selection } = self { + *selection = Some(language); + } + } + StepMessage::SliderChanged(new_value) => { + if let Step::Slider { value, .. } = self { + *value = new_value.round() as u16; + } + } + StepMessage::TextSizeChanged(new_size) => { + if let Step::Text { size, .. } = self { + *size = new_size.round() as u16; + } + } + StepMessage::TextColorChanged(new_color) => { + if let Step::Text { color, .. } = self { + *color = new_color; + } + } + StepMessage::LayoutChanged(new_layout) => { + if let Step::RowsAndColumns { layout, .. } = self { + *layout = new_layout; + } + } + StepMessage::SpacingChanged(new_spacing) => { + if let Step::RowsAndColumns { spacing, .. } = self { + *spacing = new_spacing.round() as u16; + } + } + StepMessage::ImageWidthChanged(new_width) => { + if let Step::Image { width, .. } = self { + *width = new_width.round() as u16; + } + } + StepMessage::InputChanged(new_value) => { + if let Step::TextInput { value, .. } = self { + *value = new_value; + } + } + StepMessage::ToggleSecureInput(toggle) => { + if let Step::TextInput { is_secure, .. } = self { + *is_secure = toggle; + } + } + }; + } + + fn title(&self) -> &str { + match self { + Step::Welcome => "Welcome", + Step::Radio { .. } => "Radio button", + Step::Slider { .. } => "Slider", + Step::Text { .. } => "Text", + Step::Image { .. } => "Image", + Step::RowsAndColumns { .. } => "Rows and columns", + Step::Scrollable => "Scrollable", + Step::TextInput { .. } => "Text input", + Step::Debugger => "Debugger", + Step::End => "End", + } + } + + fn can_continue(&self) -> bool { + match self { + Step::Welcome => true, + Step::Radio { selection } => *selection == Some(Language::Rust), + Step::Slider { .. } => true, + Step::Text { .. } => true, + Step::Image { .. } => true, + Step::RowsAndColumns { .. } => true, + Step::Scrollable => true, + Step::TextInput { value, .. } => !value.is_empty(), + Step::Debugger => true, + Step::End => false, + } + } + + fn view(&mut self, debug: bool) -> Element { + match self { + Step::Welcome => Self::welcome(), + Step::Radio { selection } => Self::radio(*selection), + Step::Slider { state, value } => Self::slider(state, *value), + Step::Text { + size_slider, + size, + color_sliders, + color, + } => Self::text(size_slider, *size, color_sliders, *color), + Step::Image { width, slider } => Self::image(*width, slider), + Step::RowsAndColumns { + layout, + spacing_slider, + spacing, + } => Self::rows_and_columns(*layout, spacing_slider, *spacing), + Step::Scrollable => Self::scrollable(), + Step::TextInput { + value, + is_secure, + state, + } => Self::text_input(value, *is_secure, state), + Step::Debugger => Self::debugger(debug), + Step::End => Self::end(), + } + .into() + } + + fn container(title: &str) -> Column<'a, StepMessage> { + Column::new().spacing(20).push(Text::new(title).size(50)) + } + + fn welcome() -> Column<'a, StepMessage> { + Self::container("Welcome!") + .push(Text::new( + "This is a simple tour meant to showcase a bunch of widgets \ + that can be easily implemented on top of Iced.", + )) + .push(Text::new( + "Iced is a cross-platform GUI library for Rust focused on \ + simplicity and type-safety. It is heavily inspired by Elm.", + )) + .push(Text::new( + "It was originally born as part of Coffee, an opinionated \ + 2D game engine for Rust.", + )) + .push(Text::new( + "On native platforms, Iced provides by default a renderer \ + built on top of wgpu, a graphics library supporting Vulkan, \ + Metal, DX11, and DX12.", + )) + .push(Text::new( + "Additionally, this tour can also run on WebAssembly thanks \ + to dodrio, an experimental VDOM library for Rust.", + )) + .push(Text::new( + "You will need to interact with the UI in order to reach the \ + end!", + )) + } + + fn slider( + state: &'a mut slider::State, + value: u16, + ) -> Column<'a, StepMessage> { + Self::container("Slider") + .push(Text::new( + "A slider allows you to smoothly select a value from a range \ + of values.", + )) + .push(Text::new( + "The following slider lets you choose an integer from \ + 0 to 100:", + )) + .push(Slider::new( + state, + 0.0..=100.0, + value as f32, + StepMessage::SliderChanged, + )) + .push( + Text::new(&value.to_string()) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn rows_and_columns( + layout: Layout, + spacing_slider: &'a mut slider::State, + spacing: u16, + ) -> Column<'a, StepMessage> { + let row_radio = Radio::new( + Layout::Row, + "Row", + Some(layout), + StepMessage::LayoutChanged, + ); + + let column_radio = Radio::new( + Layout::Column, + "Column", + Some(layout), + StepMessage::LayoutChanged, + ); + + let layout_section: Element<_> = match layout { + Layout::Row => Row::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + Layout::Column => Column::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + }; + + let spacing_section = Column::new() + .spacing(10) + .push(Slider::new( + spacing_slider, + 0.0..=80.0, + spacing as f32, + StepMessage::SpacingChanged, + )) + .push( + Text::new(&format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ); + + Self::container("Rows and columns") + .spacing(spacing) + .push(Text::new( + "Iced uses a layout model based on flexbox to position UI \ + elements.", + )) + .push(Text::new( + "Rows and columns can be used to distribute content \ + horizontally or vertically, respectively.", + )) + .push(layout_section) + .push(Text::new( + "You can also easily change the spacing between elements:", + )) + .push(spacing_section) + } + + fn text( + size_slider: &'a mut slider::State, + size: u16, + color_sliders: &'a mut [slider::State; 3], + color: Color, + ) -> Column<'a, StepMessage> { + let size_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("You can change its size:")) + .push( + Text::new(&format!("This text is {} pixels", size)).size(size), + ) + .push(Slider::new( + size_slider, + 10.0..=70.0, + size as f32, + StepMessage::TextSizeChanged, + )); + + let [red, green, blue] = color_sliders; + let color_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("And its color:")) + .push(Text::new(&format!("{:?}", color)).color(color)) + .push( + Row::new() + .spacing(10) + .push(Slider::new(red, 0.0..=1.0, color.r, move |r| { + StepMessage::TextColorChanged(Color { r, ..color }) + })) + .push(Slider::new(green, 0.0..=1.0, color.g, move |g| { + StepMessage::TextColorChanged(Color { g, ..color }) + })) + .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| { + StepMessage::TextColorChanged(Color { b, ..color }) + })), + ); + + Self::container("Text") + .push(Text::new( + "Text is probably the most essential widget for your UI. \ + It will try to adapt to the dimensions of its container.", + )) + .push(size_section) + .push(color_section) + } + + fn radio(selection: Option) -> Column<'a, StepMessage> { + let question = Column::new() + .padding(20) + .spacing(10) + .push(Text::new("Iced is written in...").size(24)) + .push(Language::all().iter().cloned().fold( + Column::new().padding(10).spacing(20), + |choices, language| { + choices.push(Radio::new( + language, + language.into(), + selection, + StepMessage::LanguageSelected, + )) + }, + )); + + Self::container("Radio button") + .push(Text::new( + "A radio button is normally used to represent a choice... \ + Surprise test!", + )) + .push(question) + .push(Text::new( + "Iced works very well with iterators! The list above is \ + basically created by folding a column over the different \ + choices, creating a radio button for each one of them!", + )) + } + + fn image( + width: u16, + slider: &'a mut slider::State, + ) -> Column<'a, StepMessage> { + Self::container("Image") + .push(Text::new("An image that tries to keep its aspect ratio.")) + .push(ferris(width)) + .push(Slider::new( + slider, + 100.0..=500.0, + width as f32, + StepMessage::ImageWidthChanged, + )) + .push( + Text::new(&format!("Width: {} px", width.to_string())) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn scrollable() -> Column<'a, StepMessage> { + Self::container("Scrollable") + .push(Text::new( + "Iced supports scrollable content. Try it out! Find the \ + button further below.", + )) + .push( + Text::new( + "Tip: You can use the scrollbar to scroll down faster!", + ) + .size(16), + ) + .push(Column::new().height(Length::Units(4096))) + .push( + Text::new("You are halfway there!") + .width(Length::Fill) + .size(30) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .push(Column::new().height(Length::Units(4096))) + .push(ferris(300)) + .push( + Text::new("You made it!") + .width(Length::Fill) + .size(50) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn text_input( + value: &str, + is_secure: bool, + state: &'a mut text_input::State, + ) -> Column<'a, StepMessage> { + let text_input = TextInput::new( + state, + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); + Self::container("Text input") + .push(Text::new( + "Use a text input to ask for different kinds of information.", + )) + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(Checkbox::new( + is_secure, + "Enable password mode", + StepMessage::ToggleSecureInput, + )) + .push(Text::new( + "A text input produces a message every time it changes. It is \ + very easy to keep track of its contents:", + )) + .push( + Text::new(if value.is_empty() { + "You have not typed anything yet..." + } else { + value + }) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn debugger(debug: bool) -> Column<'a, StepMessage> { + Self::container("Debugger") + .push(Text::new( + "You can ask Iced to visually explain the layouting of the \ + different elements comprising your UI!", + )) + .push(Text::new( + "Give it a shot! Check the following checkbox to be able to \ + see element boundaries.", + )) + .push(if cfg!(target_arch = "wasm32") { + Element::new( + Text::new("Not available on web yet!") + .color([0.7, 0.7, 0.7]) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } else { + Element::new(Checkbox::new( + debug, + "Explain layout", + StepMessage::DebugToggled, + )) + }) + .push(Text::new("Feel free to go back and take a look.")) + } + + fn end() -> Column<'a, StepMessage> { + Self::container("You reached the end!") + .push(Text::new( + "This tour will be updated as more features are added.", + )) + .push(Text::new("Make sure to keep an eye on it!")) + } +} + +fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { + Container::new( + // This should go away once we unify resource loading on native + // platforms + if cfg!(target_arch = "wasm32") { + Image::new("images/ferris.png") + } else { + Image::new(format!( + "{}/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + } + .width(Length::Units(width)), + ) + .width(Length::Fill) + .center_x() +} + +fn button<'a, Message>( + state: &'a mut button::State, + label: &str, +) -> Button<'a, Message> { + Button::new( + state, + Text::new(label).horizontal_alignment(HorizontalAlignment::Center), + ) + .padding(12) + .min_width(100) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Language { + Rust, + Elm, + Ruby, + Haskell, + C, + Other, +} + +impl Language { + fn all() -> [Language; 6] { + [ + Language::C, + Language::Elm, + Language::Ruby, + Language::Haskell, + Language::Rust, + Language::Other, + ] + } +} + +impl From for &str { + fn from(language: Language) -> &'static str { + match language { + Language::Rust => "Rust", + Language::Elm => "Elm", + Language::Ruby => "Ruby", + Language::Haskell => "Haskell", + Language::C => "C", + Language::Other => "Other", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Layout { + Row, + Column, +} + +mod style { + use iced::{button, Background, Color, Vector}; + + pub enum Button { + Primary, + Secondary, + } + + impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(match self { + Button::Primary => Color::from_rgb(0.11, 0.42, 0.87), + Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5), + })), + border_radius: 12, + shadow_offset: Vector::new(1.0, 1.0), + text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE), + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + text_color: Color::WHITE, + shadow_offset: Vector::new(1.0, 2.0), + ..self.active() + } + } + } +} + +// This should be gracefully handled by Iced in the future. Probably using our +// own proc macro, or maybe the whole process is streamlined by `wasm-pack` at +// some point. +#[cfg(target_arch = "wasm32")] +mod wasm { + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(start)] + pub fn run() { + super::main() + } +} diff --git a/src/lib.rs b/src/lib.rs index 9c9bcff5..1da3f549 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,6 +204,6 @@ use iced_winit as common; use iced_web as common; pub use common::{ - Align, Background, Color, Command, Font, HorizontalAlignment, Length, - Space, Subscription, Vector, VerticalAlignment, + futures, Align, Background, Color, Command, Font, HorizontalAlignment, + Length, Space, Subscription, Vector, VerticalAlignment, }; -- cgit