diff options
Diffstat (limited to 'examples')
-rw-r--r-- | examples/README.md | 23 | ||||
-rw-r--r-- | examples/bezier_tool/Cargo.toml | 5 | ||||
-rw-r--r-- | examples/bezier_tool/README.md | 3 | ||||
-rw-r--r-- | examples/bezier_tool/src/main.rs | 465 | ||||
-rw-r--r-- | examples/clock/Cargo.toml | 7 | ||||
-rw-r--r-- | examples/clock/src/main.rs | 143 | ||||
-rw-r--r-- | examples/color_palette/src/main.rs | 48 | ||||
-rw-r--r-- | examples/custom_widget/src/main.rs | 8 | ||||
-rw-r--r-- | examples/game_of_life/Cargo.toml | 12 | ||||
-rw-r--r-- | examples/game_of_life/README.md | 22 | ||||
-rw-r--r-- | examples/game_of_life/src/main.rs | 803 | ||||
-rw-r--r-- | examples/game_of_life/src/style.rs | 134 | ||||
-rw-r--r-- | examples/geometry/src/main.rs | 113 | ||||
-rw-r--r-- | examples/integration/src/main.rs | 14 | ||||
-rw-r--r-- | examples/solar_system/Cargo.toml | 7 | ||||
-rw-r--r-- | examples/solar_system/src/main.rs | 219 | ||||
-rw-r--r-- | examples/stopwatch/Cargo.toml | 5 | ||||
-rw-r--r-- | examples/stopwatch/src/main.rs | 44 |
18 files changed, 1412 insertions, 663 deletions
diff --git a/examples/README.md b/examples/README.md index f67a0dd2..8e1b781f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T [TodoMVC]: http://todomvc.com/ +## [Game of Life](game_of_life) +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file. + +<div align="center"> + <a href="https://gfycat.com/briefaccurateaardvark"> + <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway + ## [Styling](styling) An example showcasing custom styling with a light and dark theme. @@ -69,7 +90,7 @@ cargo run --package styling ## Extras A bunch of simpler examples exist: -- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`]. +- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget. - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. - [`color_palette`](color_palette), a color palette generator based on a user-defined root color. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml index b13a0aa5..a88975a7 100644 --- a/examples/bezier_tool/Cargo.toml +++ b/examples/bezier_tool/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } -lyon = "0.15" +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md index 933f2120..ebbb12cc 100644 --- a/examples/bezier_tool/README.md +++ b/examples/bezier_tool/README.md @@ -1,6 +1,6 @@ ## Bézier tool -A Paint-like tool for drawing Bézier curves using [`lyon`]. +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. The __[`main`]__ file contains all the code of the example. @@ -16,4 +16,3 @@ cargo run --package bezier_tool ``` [`main`]: src/main.rs -[`lyon`]: https://github.com/nical/lyon diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fcb7733c..fe41e1b2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,291 +1,6 @@ -//! 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, Rectangle, - Size, Vector, VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box<dyn Fn(Curve) -> Message>, - } - - #[derive(Debug, Clone, Copy)] - pub struct Curve { - from: Point, - to: Point, - control: Point, - } - - #[derive(Default)] - pub struct State { - pending: Option<Pending>, - } - - enum Pending { - One { from: Point }, - Two { from: Point, to: Point }, - } - - impl<'a, Message> Bezier<'a, Message> { - pub fn new<F>( - 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<Message, Renderer> 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<Vertex2D, u32> = 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(0.5, 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, - curve.from.y, - )); - - path_builder.quadratic_bezier_to( - lyon::math::Point::new(curve.control.x, curve.control.y), - lyon::math::Point::new(curve.to.x, curve.to.y), - ); - } - - match self.state.pending { - None => {} - Some(Pending::One { from }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.line_to(lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - )); - } - Some(Pending::Two { from, to }) => { - path_builder - .move_to(lyon::math::Point::new(from.x, from.y)); - path_builder.quadratic_bezier_to( - lyon::math::Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ), - lyon::math::Point::new(to.x, to.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 { - origin: Point::new(bounds.x, bounds.y), - buffers: 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: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..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<Message>, - _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<Element<'a, Message, Renderer>> for Bezier<'a, Message> - where - Message: 'static, - { - fn into(self) -> Element<'a, Message, Renderer> { - Element::new(self) - } - } -} - -use bezier::Bezier; +//! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::{ - button, Align, Button, Column, Container, Element, Length, Sandbox, - Settings, Text, + button, Align, Button, Column, Element, Length, Sandbox, Settings, Text, }; pub fn main() { @@ -323,6 +38,7 @@ impl Sandbox for Example { match message { Message::AddCurve(curve) => { self.curves.push(curve); + self.bezier.request_redraw(); } Message::Clear => { self.bezier = bezier::State::default(); @@ -332,7 +48,7 @@ impl Sandbox for Example { } fn view(&mut self) -> Element<Message> { - let content = Column::new() + Column::new() .padding(20) .spacing(20) .align_items(Align::Center) @@ -341,22 +57,177 @@ impl Sandbox for Example { .width(Length::Shrink) .size(50), ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) + .push(self.bezier.view(&self.curves).map(Message::AddCurve)) .push( Button::new(&mut self.button_state, Text::new("Clear")) .padding(8) .on_press(Message::Clear), - ); + ) + .into() + } +} + +mod bezier { + use iced::{ + canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, + mouse, Element, Length, Point, Rectangle, + }; + + #[derive(Default)] + pub struct State { + pending: Option<Pending>, + cache: canvas::Cache, + } - Container::new(content) + impl State { + pub fn view<'a>( + &'a mut self, + curves: &'a [Curve], + ) -> Element<'a, Curve> { + Canvas::new(Bezier { + state: self, + curves, + }) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() .into() + } + + pub fn request_redraw(&mut self) { + self.cache.clear() + } + } + + struct Bezier<'a> { + state: &'a mut State, + curves: &'a [Curve], + } + + impl<'a> canvas::Program<Curve> for Bezier<'a> { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option<Curve> { + let cursor_position = cursor.position_in(&bounds)?; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: cursor_position, + }); + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: cursor_position, + }) + } + } + } + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + let content = + self.state.cache.draw(bounds.size(), |frame: &mut Frame| { + Curve::draw_all(self.curves, frame); + + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default(), + ); + }); + + if let Some(pending) = &self.state.pending { + let pending_curve = pending.draw(bounds, cursor); + + vec![content, pending_curve] + } else { + vec![content] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + if cursor.is_over(&bounds) { + mouse::Interaction::Crosshair + } else { + mouse::Interaction::default() + } + } + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + impl Curve { + fn draw_all(curves: &[Curve], frame: &mut Frame) { + let curves = Path::new(|p| { + for curve in curves { + p.move_to(curve.from); + p.quadratic_curve_to(curve.control, curve.to); + } + }); + + frame.stroke(&curves, Stroke::default().with_width(2.0)); + } + } + + #[derive(Debug, Clone, Copy)] + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl Pending { + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { + let mut frame = Frame::new(bounds.size()); + + if let Some(cursor_position) = cursor.position_in(&bounds) { + match *self { + Pending::One { from } => { + let line = Path::line(from, cursor_position); + frame.stroke(&line, Stroke::default().with_width(2.0)); + } + Pending::Two { from, to } => { + let curve = Curve { + from, + to, + control: cursor_position, + }; + + Curve::draw_all(&[curve], &mut frame); + } + }; + } + + frame.into_geometry() + } } } diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 308cbfbb..c6e32379 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } chrono = "0.4" -async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 827379fa..9c583c78 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - canvas, executor, Application, Canvas, Color, Command, Container, Element, - Length, Point, Settings, Subscription, Vector, + canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, + executor, time, Application, Color, Command, Container, Element, Length, + Point, Rectangle, Settings, Subscription, Vector, }; pub fn main() { @@ -11,8 +12,8 @@ pub fn main() { } struct Clock { - now: LocalTime, - clock: canvas::layer::Cache<LocalTime>, + now: chrono::DateTime<chrono::Local>, + clock: Cache, } #[derive(Debug, Clone, Copy)] @@ -28,7 +29,7 @@ impl Application for Clock { fn new(_flags: ()) -> (Self, Command<Message>) { ( Clock { - now: chrono::Local::now().into(), + now: chrono::Local::now(), clock: Default::default(), }, Command::none(), @@ -42,7 +43,7 @@ impl Application for Clock { fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Tick(local_time) => { - let now = local_time.into(); + let now = local_time; if now != self.now { self.now = now; @@ -55,14 +56,14 @@ impl Application for Clock { } fn subscription(&self) -> Subscription<Message> { - time::every(std::time::Duration::from_millis(500)).map(Message::Tick) + time::every(std::time::Duration::from_millis(500)) + .map(|_| Message::Tick(chrono::Local::now())) } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + let canvas = Canvas::new(self) .width(Length::Units(400)) - .height(Length::Units(400)) - .push(self.clock.with(&self.now)); + .height(Length::Units(400)); Container::new(canvas) .width(Length::Fill) @@ -74,69 +75,54 @@ impl Application for Clock { } } -#[derive(Debug, PartialEq, Eq)] -struct LocalTime { - hour: u32, - minute: u32, - second: u32, -} - -impl From<chrono::DateTime<chrono::Local>> for LocalTime { - fn from(date_time: chrono::DateTime<chrono::Local>) -> LocalTime { +impl canvas::Program<Message> for Clock { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { use chrono::Timelike; - LocalTime { - hour: date_time.hour(), - minute: date_time.minute(), - second: date_time.second(), - } - } -} + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; -impl canvas::Drawable for LocalTime { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 2.0; + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); - let clock = Path::circle(center, radius); - frame.fill(&clock, Color::from_rgb8(0x12, 0x93, 0xD8)); + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - let short_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + let thin_stroke = Stroke { + width: radius / 100.0, + color: Color::WHITE, + line_cap: LineCap::Round, + ..Stroke::default() + }; - let long_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + let wide_stroke = Stroke { + width: thin_stroke.width * 3.0, + ..thin_stroke + }; - let thin_stroke = canvas::Stroke { - width: radius / 100.0, - color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }; + frame.translate(Vector::new(center.x, center.y)); - let wide_stroke = canvas::Stroke { - width: thin_stroke.width * 3.0, - ..thin_stroke - }; + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke); + }); - frame.translate(Vector::new(center.x, center.y)); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke); + }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.hour, 12)); - frame.stroke(&short_hand, wide_stroke); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke); + }) }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.minute, 60)); - frame.stroke(&long_hand, wide_stroke); - }); - - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.second, 60)); - frame.stroke(&long_hand, thin_stroke); - }); + vec![clock] } } @@ -145,40 +131,3 @@ fn hand_rotation(n: u32, total: u32) -> f32 { 2.0 * std::f32::consts::PI * turns } - -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<chrono::DateTime<chrono::Local>> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = chrono::DateTime<chrono::Local>; - - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; - - async_std::stream::interval(self.0) - .map(|_| chrono::Local::now()) - .boxed() - } - } -} diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 073a6734..cec6ac79 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,9 +1,10 @@ +use iced::canvas::{self, Cursor, Frame, Geometry, Path}; use iced::{ - canvas, slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, - Length, Point, Row, Sandbox, Settings, Size, Slider, Text, Vector, + slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, Length, + Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector, VerticalAlignment, }; -use palette::{self, Limited}; +use palette::{self, Hsl, Limited, Srgb}; use std::marker::PhantomData; use std::ops::RangeInclusive; @@ -23,7 +24,6 @@ pub struct ColorPalette { hwb: ColorPicker<palette::Hwb>, lab: ColorPicker<palette::Lab>, lch: ColorPicker<palette::Lch>, - canvas_layer: canvas::layer::Cache<Theme>, } #[derive(Debug, Clone, Copy)] @@ -58,7 +58,6 @@ impl Sandbox for ColorPalette { }; self.theme = Theme::new(srgb.clamp()); - self.canvas_layer.clear(); } fn view(&mut self) -> Element<Message> { @@ -80,12 +79,7 @@ impl Sandbox for ColorPalette { .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) .push(self.lab.view(lab).map(Message::LabColorChanged)) .push(self.lch.view(lch).map(Message::LchColorChanged)) - .push( - Canvas::new() - .width(Length::Fill) - .height(Length::Fill) - .push(self.canvas_layer.with(&self.theme)), - ) + .push(self.theme.view()) .into() } } @@ -95,11 +89,12 @@ pub struct Theme { lower: Vec<Color>, base: Color, higher: Vec<Color>, + canvas_cache: canvas::Cache, } impl Theme { pub fn new(base: impl Into<Color>) -> Theme { - use palette::{Hsl, Hue, Shade, Srgb}; + use palette::{Hue, Shade}; let base = base.into(); @@ -130,6 +125,7 @@ impl Theme { .iter() .map(|&color| Srgb::from(color).clamp().into()) .collect(), + canvas_cache: canvas::Cache::default(), } } @@ -143,13 +139,15 @@ impl Theme { .chain(std::iter::once(&self.base)) .chain(self.higher.iter()) } -} -impl canvas::Drawable for Theme { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; - use palette::{Hsl, Srgb}; + pub fn view(&mut self) -> Element<Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + fn draw(&self, frame: &mut Frame) { let pad = 20.0; let box_size = Size { @@ -176,8 +174,7 @@ impl canvas::Drawable for Theme { x: (i as f32) * box_size.width, y: 0.0, }; - let rect = Path::rectangle(anchor, box_size); - frame.fill(&rect, color); + frame.fill_rectangle(anchor, box_size, color); // We show a little indicator for the base color if color == self.base { @@ -225,8 +222,7 @@ impl canvas::Drawable for Theme { y: box_size.height + 2.0 * pad, }; - let rect = Path::rectangle(anchor, box_size); - frame.fill(&rect, color); + frame.fill_rectangle(anchor, box_size, color); frame.fill_text(canvas::Text { content: color_hex_string(&color), @@ -240,6 +236,16 @@ impl canvas::Drawable for Theme { } } +impl canvas::Program<Message> for Theme { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> { + let theme = self.canvas_cache.draw(bounds.size(), |frame| { + self.draw(frame); + }); + + vec![theme] + } +} + impl Default for Theme { fn default() -> Self { Theme::new(Color::from_rgb8(75, 128, 190)) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0a570745..f096fb54 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -10,8 +10,8 @@ mod circle { // 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, + layout, mouse, Background, Color, Element, Hasher, Layout, Length, + Point, Size, Widget, }; use iced_wgpu::{Defaults, Primitive, Renderer}; @@ -57,7 +57,7 @@ mod circle { _defaults: &Defaults, layout: Layout<'_>, _cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { ( Primitive::Quad { bounds: layout.bounds(), @@ -66,7 +66,7 @@ mod circle { border_width: 0, border_color: Color::TRANSPARENT, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..b9bb7f2a --- /dev/null +++ b/examples/game_of_life/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +tokio = { version = "0.2", features = ["blocking"] } +itertools = "0.9" +rustc-hash = "1.1" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..1aeb1455 --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,22 @@ +## Game of Life + +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The __[`main`]__ file contains the relevant code of the example. + +<div align="center"> + <a href="https://gfycat.com/briefaccurateaardvark"> + <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif"> + </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[`main`]: src/main.rs +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..080d55c0 --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,803 @@ +//! This example showcases an interactive version of the Game of Life, invented +//! by John Conway. It leverages a `Canvas` together with other widgets. +mod style; + +use grid::Grid; +use iced::{ + button::{self, Button}, + executor, + slider::{self, Slider}, + time, Align, Application, Checkbox, Column, Command, Container, Element, + Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + controls: Controls, + is_playing: bool, + queued_ticks: usize, + speed: usize, + next_speed: Option<usize>, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message), + Tick(Instant), + TogglePlayback, + ToggleGrid(bool), + Next, + Clear, + SpeedChanged(f32), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command<Message>) { + ( + Self { + speed: 1, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn update(&mut self, message: Message) -> Command<Message> { + match message { + Message::Grid(message) => { + self.grid.update(message); + } + Message::Tick(_) | Message::Next => { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + + if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + self.queued_ticks = 0; + + return Command::perform(task, Message::Grid); + } + } + Message::TogglePlayback => { + self.is_playing = !self.is_playing; + } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } + Message::Clear => { + self.grid.clear(); + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element<Message> { + let selected_speed = self.next_speed.unwrap_or(self.speed); + let controls = self.controls.view( + self.is_playing, + self.grid.are_lines_visible(), + selected_speed, + ); + + let content = Column::new() + .push(self.grid.view().map(Message::Grid)) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +mod grid { + use iced::{ + canvas::{ + self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, + }, + mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, + Size, Vector, VerticalAlignment, + }; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; + use std::ops::RangeInclusive; + use std::time::{Duration, Instant}; + + pub struct Grid { + state: State, + interaction: Interaction, + life_cache: Cache, + grid_cache: Cache, + translation: Vector, + scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, + version: usize, + } + + #[derive(Debug, Clone)] + pub enum Message { + Populate(Cell), + Unpopulate(Cell), + Ticked { + result: Result<Life, TickError>, + tick_duration: Duration, + version: usize, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, + } + + impl Default for Grid { + fn default() -> Self { + Self { + state: State::default(), + interaction: Interaction::None, + life_cache: Cache::default(), + grid_cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, + version: 0, + } + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Message>> { + let version = self.version; + let tick = self.state.tick(amount)?; + + self.last_queued_ticks = amount; + + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + version, + tick_duration, + } + }) + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate(cell) => { + self.state.populate(cell); + self.life_cache.clear(); + } + Message::Unpopulate(cell) => { + self.state.unpopulate(&cell); + self.life_cache.clear(); + } + Message::Ticked { + result: Ok(life), + version, + tick_duration, + } if version == self.version => { + self.state.update(life); + self.life_cache.clear(); + + self.last_tick_duration = tick_duration; + } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + Message::Ticked { .. } => {} + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.life_cache.clear(); + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Region { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } + } + + impl<'a> canvas::Program<Message> for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option<Message> { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + self.interaction = Interaction::None; + } + + let cursor_position = cursor.position_in(&bounds)?; + let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); + + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) + } else { + (Some(Message::Populate(cell)), None) + }; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(button) => match button { + mouse::Button::Left => { + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + self.interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; + + None + } + _ => None, + }, + mouse::Event::CursorMoved { .. } => { + match self.interaction { + Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, + Interaction::Panning { translation, start } => { + self.translation = translation + + (cursor_position - start) + * (1.0 / self.scaling); + + self.life_cache.clear(); + self.grid_cache.clear(); + + None + } + _ => None, + } + } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING + { + let old_scaling = self.scaling; + + self.scaling = (self.scaling + * (1.0 + y / 30.0)) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); + + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + let factor = self.scaling - old_scaling; + + self.translation = self.translation + - Vector::new( + cursor_to_center.x * factor + / (old_scaling * old_scaling), + cursor_to_center.y * factor + / (old_scaling * old_scaling), + ); + } + + self.life_cache.clear(); + self.grid_cache.clear(); + } + + None + } + }, + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> { + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); + + let life = self.life_cache.draw(bounds.size(), |frame| { + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + + for cell in region.cull(self.state.cells()) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.size()); + + let hovered_cell = + cursor.position_in(&bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); + + if let Some(cell) = hovered_cell { + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); + } + + let cell_count = self.state.cell_count(); + + frame.fill_text(Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + + frame.into_geometry() + }; + + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, overlay] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + match self.interaction { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), + } + } + } + + #[derive(Default)] + struct State { + life: Life, + births: FxHashSet<Cell>, + is_ticking: bool, + } + + impl State { + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator<Item = &Cell> { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option<impl Future<Output = Result<Life, TickError>>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } + } + + #[derive(Clone, Default)] + pub struct Life { + cells: FxHashSet<Cell>, + } + + impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator<Item = &Cell> { + self.cells.iter() + } + } + + impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator<Item = Cell> { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + } + + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn rows(&self) -> RangeInclusive<isize> { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive<isize> { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator<Item = &'a Cell>, + ) -> impl Iterator<Item = &'a Cell> { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + + enum Interaction { + None, + Drawing, + Erasing, + Panning { translation: Vector, start: Point }, + } +} + +#[derive(Default)] +struct Controls { + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +impl Controls { + fn view<'a>( + &'a mut self, + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + ) -> Element<'a, Message> { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::TogglePlayback) + .style(style::Button), + ) + .push( + Button::new(&mut self.next_button, Text::new("Next")) + .on_press(Message::Next) + .style(style::Button), + ); + + let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=1000.0, + speed as f32, + Message::SpeedChanged, + ) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", speed)).size(16)); + + Row::new() + .padding(10) + .spacing(20) + .align_items(Align::Center) + .push(playback_controls) + .push(speed_controls) + .push( + Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + ) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Clear), + ) + .into() + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..d59569f2 --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,134 @@ +use iced::{button, container, slider, Background, Color}; + +const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, +); + +const DESTRUCTIVE: Color = Color::from_rgb( + 0xC0 as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 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 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 Clear; + +impl button::StyleSheet for Clear { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(DESTRUCTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color { + a: 0.5, + ..DESTRUCTIVE + })), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.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 + } + } +} diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 13a687ab..aabe6b21 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -11,7 +11,7 @@ mod rainbow { // 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, + layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, Widget, }; use iced_wgpu::{ @@ -54,7 +54,7 @@ mod rainbow { _defaults: &Defaults, layout: Layout<'_>, cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { let b = layout.bounds(); // R O Y G B I V @@ -85,60 +85,63 @@ mod rainbow { let posn_l = [0.0, b.height / 2.0]; ( - Primitive::Mesh2D { - origin: Point::new(b.x, b.y), - buffers: 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 - ], - }, + Primitive::Translate { + translation: Vector::new(b.x, b.y), + content: Box::new(Primitive::Mesh2D { + size: b.size(), + buffers: 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, + mouse::Interaction::default(), ) } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7203d4b6..92d2fa8d 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -8,7 +8,7 @@ use iced_wgpu::{ wgpu, window::SwapChain, Primitive, Renderer, Settings, Target, }; use iced_winit::{ - futures, winit, Cache, Clipboard, MouseCursor, Size, UserInterface, + futures, mouse, winit, Cache, Clipboard, Size, UserInterface, }; use winit::{ @@ -63,7 +63,7 @@ pub fn main() { let mut events = Vec::new(); let mut cache = Some(Cache::default()); let mut renderer = Renderer::new(&mut device, Settings::default()); - let mut output = (Primitive::None, MouseCursor::OutOfBounds); + let mut output = (Primitive::None, mouse::Interaction::default()); let clipboard = Clipboard::new(&window); // Initialize scene and GUI controls @@ -189,7 +189,7 @@ pub fn main() { scene.draw(&mut encoder, &frame.view); // And then iced on top - let mouse_cursor = renderer.draw( + let mouse_interaction = renderer.draw( &mut device, &mut encoder, Target { @@ -205,9 +205,11 @@ pub fn main() { queue.submit(&[encoder.finish()]); // And update the mouse cursor - window.set_cursor_icon(iced_winit::conversion::mouse_cursor( - mouse_cursor, - )); + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); } _ => {} } diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index c88cda50..44ced729 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } rand = "0.7" diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index bcd1dc71..98bd3b21 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,8 +7,9 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::{ - canvas, executor, window, Application, Canvas, Color, Command, Container, - Element, Length, Point, Settings, Size, Subscription, Vector, + canvas::{self, Cursor, Path, Stroke}, + executor, time, window, Application, Canvas, Color, Command, Element, + Length, Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -22,7 +23,6 @@ pub fn main() { struct SolarSystem { state: State, - solar_system: canvas::layer::Cache<State>, } #[derive(Debug, Clone, Copy)] @@ -39,7 +39,6 @@ impl Application for SolarSystem { ( SolarSystem { state: State::new(), - solar_system: Default::default(), }, Command::none(), ) @@ -53,7 +52,6 @@ impl Application for SolarSystem { match message { Message::Tick(instant) => { self.state.update(instant); - self.solar_system.clear(); } } @@ -66,24 +64,20 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element<Message> { - let canvas = Canvas::new() + Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill) - .push(self.solar_system.with(&self.state)); - - Container::new(canvas) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } } #[derive(Debug)] struct State { + space_cache: canvas::Cache, + system_cache: canvas::Cache, + cursor_position: Point, start: Instant, - current: Instant, + now: Instant, stars: Vec<(Point, f32)>, } @@ -99,137 +93,122 @@ impl State { let (width, height) = window::Settings::default().size; State { + space_cache: Default::default(), + system_cache: Default::default(), + cursor_position: Point::ORIGIN, start: now, - current: now, - stars: { - use rand::Rng; - - let mut rng = rand::thread_rng(); - - (0..100) - .map(|_| { - ( - Point::new( - rng.gen_range(0.0, width as f32), - rng.gen_range(0.0, height as f32), - ), - rng.gen_range(0.5, 1.0), - ) - }) - .collect() - }, + now, + stars: Self::generate_stars(width, height), } } pub fn update(&mut self, now: Instant) { - self.current = now; + self.now = now; + self.system_cache.clear(); + } + + fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + use rand::Rng; + + let mut rng = rand::thread_rng(); + + (0..100) + .map(|_| { + ( + Point::new( + rng.gen_range( + -(width as f32) / 2.0, + width as f32 / 2.0, + ), + rng.gen_range( + -(height as f32) / 2.0, + height as f32 / 2.0, + ), + ), + rng.gen_range(0.5, 1.0), + ) + }) + .collect() } } -impl canvas::Drawable for State { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::{Path, Stroke}; +impl<Message> canvas::Program<Message> for State { + fn draw( + &self, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec<canvas::Geometry> { use std::f32::consts::PI; - let center = frame.center(); + let background = self.space_cache.draw(bounds.size(), |frame| { + let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } + }); - let stars = Path::new(|path| { - for (p, size) in &self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } - }); + frame.fill(&space, Color::BLACK); - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); - - frame.fill(&space, Color::BLACK); - frame.fill(&stars, Color::WHITE); - frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); - frame.stroke( - &orbit, - Stroke { - width: 1.0, - color: Color::from_rgba8(0, 153, 255, 0.1), - ..Stroke::default() - }, - ); + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); + }); - let elapsed = self.current - self.start; - let elapsed_seconds = elapsed.as_secs() as f32; - let elapsed_millis = elapsed.subsec_millis() as f32; + let system = self.system_cache.draw(bounds.size(), |frame| { + let center = frame.center(); - frame.with_save(|frame| { - frame.translate(Vector::new(center.x, center.y)); - frame.rotate( - (2.0 * PI / 60.0) * elapsed_seconds - + (2.0 * PI / 60_000.0) * elapsed_millis, - ); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + let sun = Path::circle(center, Self::SUN_RADIUS); + let orbit = Path::circle(center, Self::ORBIT_RADIUS); - let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - let shadow = Path::rectangle( - Point::new(0.0, -Self::EARTH_RADIUS), - Size::new(Self::EARTH_RADIUS * 4.0, Self::EARTH_RADIUS * 2.0), + frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); + frame.stroke( + &orbit, + Stroke { + width: 1.0, + color: Color::from_rgba8(0, 153, 255, 0.1), + ..Stroke::default() + }, ); - frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); + let elapsed = self.now - self.start; + let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32 + + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32; frame.with_save(|frame| { - frame.rotate( - ((2.0 * PI) / 6.0) * elapsed_seconds - + ((2.0 * PI) / 6_000.0) * elapsed_millis, + frame.translate(Vector::new(center.x, center.y)); + frame.rotate(rotation); + frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + + let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); + let shadow = Path::rectangle( + Point::new(0.0, -Self::EARTH_RADIUS), + Size::new( + Self::EARTH_RADIUS * 4.0, + Self::EARTH_RADIUS * 2.0, + ), ); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); - }); - frame.fill( - &shadow, - Color { - a: 0.7, - ..Color::BLACK - }, - ); - }); - } -} - -mod time { - use iced::futures; - use std::time::Instant; - - pub fn every(duration: std::time::Duration) -> iced::Subscription<Instant> { - iced::Subscription::from_recipe(Every(duration)) - } + frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); - struct Every(std::time::Duration); + frame.with_save(|frame| { + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - impl<H, I> iced_native::subscription::Recipe<H, I> for Every - where - H: std::hash::Hasher, - { - type Output = Instant; + let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); + frame.fill(&moon, Color::WHITE); + }); - fn hash(&self, state: &mut H) { - use std::hash::Hash; - - std::any::TypeId::of::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _input: futures::stream::BoxStream<'static, I>, - ) -> futures::stream::BoxStream<'static, Self::Output> { - use futures::stream::StreamExt; + frame.fill( + &shadow, + Color { + a: 0.7, + ..Color::BLACK + }, + ); + }); + }); - async_std::stream::interval(self.0) - .map(|_| Instant::now()) - .boxed() - } + vec![background, system] } } diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml index 1dae3b83..075aa111 100644 --- a/examples/stopwatch/Cargo.toml +++ b/examples/stopwatch/Cargo.toml @@ -6,7 +6,4 @@ 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"] } +iced = { path = "../..", features = ["tokio"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 5a54ed2b..9de6d39e 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, + button, executor, time, Align, Application, Button, Column, Command, + Container, Element, HorizontalAlignment, Length, Row, Settings, + Subscription, Text, }; use std::time::{Duration, Instant}; @@ -28,7 +29,7 @@ enum Message { } impl Application for Stopwatch { - type Executor = iced_futures::executor::AsyncStd; + type Executor = executor::Default; type Message = Message; type Flags = (); @@ -143,43 +144,6 @@ impl Application for Stopwatch { } } -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription<std::time::Instant> { - iced::Subscription::from_recipe(Every(duration)) - } - - struct Every(std::time::Duration); - - impl<H, I> iced_native::subscription::Recipe<H, I> 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::<Self>().hash(state); - self.0.hash(state); - } - - fn stream( - self: Box<Self>, - _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}; |