From 0b5028b1ab47707a469176e9bf20cacdd3a19861 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Apr 2020 14:39:30 +0200 Subject: Draft `Program` interactivity for `Canvas` --- examples/clock/src/main.rs | 5 ++--- examples/solar_system/src/main.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) (limited to 'examples') diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 827379fa..2407db65 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -59,10 +59,9 @@ impl Application for Clock { } fn view(&mut self) -> Element { - let canvas = Canvas::new() + let canvas = Canvas::new(&mut self.clock, &self.now) .width(Length::Units(400)) - .height(Length::Units(400)) - .push(self.clock.with(&self.now)); + .height(Length::Units(400)); Container::new(canvas) .width(Length::Fill) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index bcd1dc71..eeb0796a 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -66,10 +66,9 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element { - let canvas = Canvas::new() + let canvas = Canvas::new(&mut self.solar_system, &self.state) .width(Length::Fill) - .height(Length::Fill) - .push(self.solar_system.with(&self.state)); + .height(Length::Fill); Container::new(canvas) .width(Length::Fill) -- cgit From bb424e54c5083402225a0fdda6130de575592dca Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Apr 2020 18:48:30 +0200 Subject: Add interactivity to `solar_system` example --- examples/solar_system/src/main.rs | 87 +++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 18 deletions(-) (limited to 'examples') diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index eeb0796a..8870fe52 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -10,6 +10,7 @@ use iced::{ canvas, executor, window, Application, Canvas, Color, Command, Container, Element, Length, Point, Settings, Size, Subscription, Vector, }; +use iced_native::input::{self, mouse}; use std::time::Instant; @@ -22,7 +23,7 @@ pub fn main() { struct SolarSystem { state: State, - solar_system: canvas::layer::Cache, + now: Instant, } #[derive(Debug, Clone, Copy)] @@ -39,7 +40,7 @@ impl Application for SolarSystem { ( SolarSystem { state: State::new(), - solar_system: Default::default(), + now: Instant::now(), }, Command::none(), ) @@ -52,8 +53,8 @@ impl Application for SolarSystem { fn update(&mut self, message: Message) -> Command { match message { Message::Tick(instant) => { - self.state.update(instant); - self.solar_system.clear(); + self.now = instant; + self.state.clear(); } } @@ -66,7 +67,7 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element { - let canvas = Canvas::new(&mut self.solar_system, &self.state) + let canvas = Canvas::new(&mut self.state, &self.now) .width(Length::Fill) .height(Length::Fill); @@ -81,25 +82,21 @@ impl Application for SolarSystem { #[derive(Debug)] struct State { + cache: canvas::layer::Cache, + cursor_position: Point, start: Instant, - current: Instant, stars: Vec<(Point, f32)>, } impl State { - const SUN_RADIUS: f32 = 70.0; - const ORBIT_RADIUS: f32 = 150.0; - const EARTH_RADIUS: f32 = 12.0; - const MOON_RADIUS: f32 = 4.0; - const MOON_DISTANCE: f32 = 28.0; - pub fn new() -> State { let now = Instant::now(); let (width, height) = window::Settings::default().size; State { + cache: Default::default(), + cursor_position: Point::ORIGIN, start: now, - current: now, stars: { use rand::Rng; @@ -120,12 +117,66 @@ impl State { } } - pub fn update(&mut self, now: Instant) { - self.current = now; + pub fn clear(&mut self) { + self.cache.clear(); } } -impl canvas::Drawable for State { +impl canvas::Program for State { + type Input = Instant; + + fn update( + &mut self, + event: canvas::Event, + _bounds: Size, + _input: &Instant, + ) { + match event { + canvas::Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::CursorMoved { x, y } => { + self.cursor_position = Point::new(x, y); + } + mouse::Event::Input { + button: mouse::Button::Left, + state: input::ButtonState::Released, + } => { + self.stars.push((self.cursor_position, 2.0)); + } + _ => {} + }, + } + } + + fn layers<'a>( + &'a self, + now: &'a Instant, + ) -> Vec> { + let system = System { + stars: &self.stars, + start: &self.start, + now, + }; + + vec![Box::new(self.cache.with(system))] + } +} + +#[derive(Debug)] +struct System<'a> { + stars: &'a [(Point, f32)], + start: &'a Instant, + now: &'a Instant, +} + +impl System<'_> { + const SUN_RADIUS: f32 = 70.0; + const ORBIT_RADIUS: f32 = 150.0; + const EARTH_RADIUS: f32 = 12.0; + const MOON_RADIUS: f32 = 4.0; + const MOON_DISTANCE: f32 = 28.0; +} + +impl<'a> canvas::Drawable for System<'a> { fn draw(&self, frame: &mut canvas::Frame) { use canvas::{Path, Stroke}; use std::f32::consts::PI; @@ -135,7 +186,7 @@ impl canvas::Drawable for State { let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); let stars = Path::new(|path| { - for (p, size) in &self.stars { + for (p, size) in self.stars { path.rectangle(*p, Size::new(*size, *size)); } }); @@ -155,7 +206,7 @@ impl canvas::Drawable for State { }, ); - let elapsed = self.current - self.start; + let elapsed = *self.now - *self.start; let elapsed_seconds = elapsed.as_secs() as f32; let elapsed_millis = elapsed.subsec_millis() as f32; -- cgit From 592cc685067c36cbba87e4db14f4ebc71d65b951 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 19 Apr 2020 21:55:23 +0200 Subject: Remove `Layer` trait and simplify `Canvas` --- examples/clock/src/main.rs | 4 +- examples/solar_system/src/main.rs | 138 +++++++++++++++++++++----------------- 2 files changed, 78 insertions(+), 64 deletions(-) (limited to 'examples') diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 2407db65..8b3a92c7 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -12,7 +12,7 @@ pub fn main() { struct Clock { now: LocalTime, - clock: canvas::layer::Cache, + clock: canvas::Cache, } #[derive(Debug, Clone, Copy)] @@ -59,7 +59,7 @@ impl Application for Clock { } fn view(&mut self) -> Element { - let canvas = Canvas::new(&mut self.clock, &self.now) + let canvas = Canvas::new(self.clock.with(&self.now)) .width(Length::Units(400)) .height(Length::Units(400)); diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 8870fe52..618f4206 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -23,7 +23,6 @@ pub fn main() { struct SolarSystem { state: State, - now: Instant, } #[derive(Debug, Clone, Copy)] @@ -40,7 +39,6 @@ impl Application for SolarSystem { ( SolarSystem { state: State::new(), - now: Instant::now(), }, Command::none(), ) @@ -53,8 +51,7 @@ impl Application for SolarSystem { fn update(&mut self, message: Message) -> Command { match message { Message::Tick(instant) => { - self.now = instant; - self.state.clear(); + self.state.update(instant); } } @@ -67,7 +64,7 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element { - let canvas = Canvas::new(&mut self.state, &self.now) + let canvas = Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill); @@ -82,9 +79,11 @@ impl Application for SolarSystem { #[derive(Debug)] struct State { - cache: canvas::layer::Cache, + space_cache: canvas::Cache, + system_cache: canvas::Cache, cursor_position: Point, start: Instant, + now: Instant, stars: Vec<(Point, f32)>, } @@ -94,43 +93,52 @@ impl State { let (width, height) = window::Settings::default().size; State { - cache: Default::default(), + space_cache: Default::default(), + system_cache: Default::default(), cursor_position: Point::ORIGIN, start: 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 clear(&mut self) { - self.cache.clear(); + pub fn space(&self) -> Space<'_> { + Space { stars: &self.stars } } -} -impl canvas::Program for State { - type Input = Instant; + pub fn system(&self) -> System { + System { + start: self.start, + now: self.now, + } + } + + pub fn update(&mut self, now: Instant) { + 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(0.0, width as f32), + rng.gen_range(0.0, height as f32), + ), + rng.gen_range(0.5, 1.0), + ) + }) + .collect() + } +} - fn update( - &mut self, - event: canvas::Event, - _bounds: Size, - _input: &Instant, - ) { +impl canvas::State for State { + fn update(&mut self, event: canvas::Event, _bounds: Size) { match event { canvas::Event::Mouse(mouse_event) => match mouse_event { mouse::Event::CursorMoved { x, y } => { @@ -141,34 +149,50 @@ impl canvas::Program for State { state: input::ButtonState::Released, } => { self.stars.push((self.cursor_position, 2.0)); + self.space_cache.clear(); } _ => {} }, } } - fn layers<'a>( - &'a self, - now: &'a Instant, - ) -> Vec> { - let system = System { - stars: &self.stars, - start: &self.start, - now, - }; - - vec![Box::new(self.cache.with(system))] + fn draw(&self, bounds: Size) -> Vec { + vec![ + self.space_cache.draw(bounds, self.space()), + self.system_cache.draw(bounds, self.system()), + ] } } #[derive(Debug)] -struct System<'a> { +struct Space<'a> { stars: &'a [(Point, f32)], - start: &'a Instant, - now: &'a Instant, } -impl System<'_> { +impl canvas::Drawable for Space<'_> { + fn draw(&self, frame: &mut canvas::Frame) { + use canvas::Path; + + 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)); + } + }); + + frame.fill(&space, Color::BLACK); + frame.fill(&stars, Color::WHITE); + } +} + +#[derive(Debug)] +struct System { + start: Instant, + now: Instant, +} + +impl System { const SUN_RADIUS: f32 = 70.0; const ORBIT_RADIUS: f32 = 150.0; const EARTH_RADIUS: f32 = 12.0; @@ -176,26 +200,16 @@ impl System<'_> { const MOON_DISTANCE: f32 = 28.0; } -impl<'a> canvas::Drawable for System<'a> { +impl canvas::Drawable for System { fn draw(&self, frame: &mut canvas::Frame) { use canvas::{Path, Stroke}; use std::f32::consts::PI; let center = frame.center(); - 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 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, @@ -206,7 +220,7 @@ impl<'a> canvas::Drawable for System<'a> { }, ); - let elapsed = *self.now - *self.start; + let elapsed = self.now - self.start; let elapsed_seconds = elapsed.as_secs() as f32; let elapsed_millis = elapsed.subsec_millis() as f32; -- cgit From dc97d6f33e1e8f8c276fd1cf1d4ed12892ce3ec9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 01:10:59 +0200 Subject: Remove interaction from `solar_system` example --- examples/solar_system/src/main.rs | 57 ++++++++++++--------------------------- 1 file changed, 17 insertions(+), 40 deletions(-) (limited to 'examples') diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 618f4206..5392f712 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,10 +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, executor, window, Application, Canvas, Color, Command, Element, + Length, Point, Settings, Size, Subscription, Vector, }; -use iced_native::input::{self, mouse}; use std::time::Instant; @@ -64,15 +63,9 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element { - let canvas = Canvas::new(&mut self.state) - .width(Length::Fill) - .height(Length::Fill); - - Container::new(canvas) + Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill) - .center_x() - .center_y() .into() } } @@ -127,8 +120,14 @@ impl State { .map(|_| { ( Point::new( - rng.gen_range(0.0, width as f32), - rng.gen_range(0.0, height as f32), + 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), ) @@ -138,24 +137,6 @@ impl State { } impl canvas::State for State { - fn update(&mut self, event: canvas::Event, _bounds: Size) { - match event { - canvas::Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::CursorMoved { x, y } => { - self.cursor_position = Point::new(x, y); - } - mouse::Event::Input { - button: mouse::Button::Left, - state: input::ButtonState::Released, - } => { - self.stars.push((self.cursor_position, 2.0)); - self.space_cache.clear(); - } - _ => {} - }, - } - } - fn draw(&self, bounds: Size) -> Vec { vec![ self.space_cache.draw(bounds, self.space()), @@ -182,6 +163,8 @@ impl canvas::Drawable for Space<'_> { }); frame.fill(&space, Color::BLACK); + + frame.translate(frame.center() - Point::ORIGIN); frame.fill(&stars, Color::WHITE); } } @@ -221,15 +204,12 @@ impl canvas::Drawable for System { ); let elapsed = self.now - self.start; - let elapsed_seconds = elapsed.as_secs() as f32; - let elapsed_millis = elapsed.subsec_millis() as f32; + 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.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.rotate(rotation); frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); @@ -241,10 +221,7 @@ impl canvas::Drawable for System { frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); frame.with_save(|frame| { - frame.rotate( - ((2.0 * PI) / 6.0) * elapsed_seconds - + ((2.0 * PI) / 6_000.0) * elapsed_millis, - ); + frame.rotate(rotation * 10.0); frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); -- cgit From 59b1e90661ee9e479f404bae71029db824cc7b46 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 03:18:31 +0200 Subject: Introduce `Translate` primitive in `iced_wgpu` --- examples/bezier_tool/src/main.rs | 14 ++--- examples/geometry/src/main.rs | 108 ++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 59 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fcb7733c..8012ea0a 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -172,12 +172,14 @@ mod bezier { ) .unwrap(); - let mesh = Primitive::Mesh2D { - origin: Point::new(bounds.x, bounds.y), - buffers: Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, + let mesh = Primitive::Translate { + translation: Vector::new(bounds.x, bounds.y), + content: Box::new(Primitive::Mesh2D { + buffers: Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + }), }; ( diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 13a687ab..63e1bacd 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -12,7 +12,7 @@ mod rainbow { // implemented by `iced_wgpu` and other renderers. use iced_native::{ layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, - Widget, + Vector, Widget, }; use iced_wgpu::{ triangle::{Mesh2D, Vertex2D}, @@ -85,58 +85,60 @@ 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 { + 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, ) -- cgit From e4eb0553de13053c9828fd5454c281e27e598d65 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 03:46:03 +0200 Subject: Allow `canvas::State` to produce messages --- examples/solar_system/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 5392f712..9337c7b5 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -136,7 +136,7 @@ impl State { } } -impl canvas::State for State { +impl canvas::State for State { fn draw(&self, bounds: Size) -> Vec { vec![ self.space_cache.draw(bounds, self.space()), -- cgit From fd1ceac3633c4f60852eb9f75da9fbb5e1f35df3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 03:47:41 +0200 Subject: Port `bezier_tool` example to use `Canvas` --- examples/README.md | 2 +- examples/bezier_tool/Cargo.toml | 5 +- examples/bezier_tool/README.md | 3 +- examples/bezier_tool/src/main.rs | 439 +++++++++++++-------------------------- 4 files changed, 150 insertions(+), 299 deletions(-) (limited to 'examples') diff --git a/examples/README.md b/examples/README.md index 5aea51eb..5d880d71 100644 --- a/examples/README.md +++ b/examples/README.md @@ -69,7 +69,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. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). - [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle. 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 8012ea0a..a00a1005 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,290 +1,4 @@ -//! 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 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(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::Translate { - translation: Vector::new(bounds.x, bounds.y), - content: Box::new(Primitive::Mesh2D { - 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, - _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; +//! This example showcases an interactive `Canvas` for drawing Bézier curves. use iced::{ button, Align, Button, Column, Container, Element, Length, Sandbox, Settings, Text, @@ -325,6 +39,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(); @@ -343,11 +58,7 @@ 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) @@ -362,3 +73,147 @@ impl Sandbox for Example { .into() } } + +mod bezier { + use iced::{ + canvas::{ + self, Canvas, Drawable, Event, Frame, Geometry, Path, Stroke, + }, + mouse, ButtonState, Element, Length, Point, Size, + }; + + #[derive(Default)] + pub struct State { + pending: Option, + cursor_position: Point, + cache: canvas::Cache, + } + + 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) + .into() + } + + pub fn request_redraw(&mut self) { + self.cache.clear() + } + } + + struct Bezier<'a> { + state: &'a mut State, + curves: &'a [Curve], + } + + impl<'a> canvas::State for Bezier<'a> { + fn update(&mut self, event: Event, _bounds: Size) -> Option { + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::CursorMoved { x, y } => { + self.state.cursor_position = Point::new(x, y); + + None + } + mouse::Event::Input { + button: mouse::Button::Left, + state: ButtonState::Pressed, + } => match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: self.state.cursor_position, + }); + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: self.state.cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: self.state.cursor_position, + }) + } + }, + _ => None, + }, + } + } + + fn draw(&self, bounds: Size) -> Vec { + let curves = self.state.cache.draw(bounds, &self.curves); + + if let Some(pending) = &self.state.pending { + let pending_curve = + pending.draw(bounds, self.state.cursor_position); + + vec![curves, pending_curve] + } else { + vec![curves] + } + } + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + impl Drawable for Curve { + fn draw(&self, frame: &mut Frame) { + let curve = Path::new(|p| { + p.move_to(self.from); + p.quadratic_curve_to(self.control, self.to); + }); + + frame.stroke(&curve, 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: Size, cursor_position: Point) -> Geometry { + let mut frame = Frame::new(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(&mut frame); + } + }; + + frame.into_geometry() + } + } +} -- cgit From e65585ae17bf2fae1bbad1cde839d6f767a29b82 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 04:41:09 +0200 Subject: Clip and cull `Mesh2D` primitives in `iced_wgpu` --- examples/geometry/src/main.rs | 1 + 1 file changed, 1 insertion(+) (limited to 'examples') diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 63e1bacd..795cac27 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -88,6 +88,7 @@ mod rainbow { Primitive::Translate { translation: Vector::new(b.x, b.y), content: Box::new(Primitive::Mesh2D { + size: b.size(), buffers: Mesh2D { vertices: vec![ Vertex2D { -- cgit From 7f1e7aea07bb448471470093a47898b059d940d3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 04:41:25 +0200 Subject: Remove unnecessary `Container` in `bezier_tool` --- examples/bezier_tool/src/main.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index a00a1005..5473bc07 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,7 +1,6 @@ //! 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() { @@ -49,7 +48,7 @@ impl Sandbox for Example { } fn view(&mut self) -> Element { - let content = Column::new() + Column::new() .padding(20) .spacing(20) .align_items(Align::Center) @@ -63,13 +62,7 @@ impl Sandbox for Example { 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() } } -- cgit From 2539042b71d70afd4d8f262783d441e768811ee9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Apr 2020 06:24:12 +0200 Subject: Remove `Drawable` and rename `State` to `Program` --- examples/bezier_tool/src/main.rs | 91 ++++++++++++---------- examples/clock/Cargo.toml | 3 - examples/clock/src/main.rs | 98 ++++++++++------------- examples/solar_system/Cargo.toml | 3 - examples/solar_system/src/main.rs | 158 +++++++++++++++----------------------- 5 files changed, 157 insertions(+), 196 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 5473bc07..2112f662 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -69,10 +69,8 @@ impl Sandbox for Example { mod bezier { use iced::{ - canvas::{ - self, Canvas, Drawable, Event, Frame, Geometry, Path, Stroke, - }, - mouse, ButtonState, Element, Length, Point, Size, + canvas::{self, Canvas, Event, Frame, Geometry, Path, Stroke}, + mouse, ButtonState, Element, Length, Point, Rectangle, Size, }; #[derive(Default)] @@ -106,8 +104,10 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::State for Bezier<'a> { - fn update(&mut self, event: Event, _bounds: Size) -> Option { + impl<'a> canvas::Program for Bezier<'a> { + fn update(&mut self, event: Event, bounds: Size) -> Option { + let bounds = Rectangle::new(Point::ORIGIN, bounds); + match event { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::CursorMoved { x, y } => { @@ -118,46 +118,55 @@ mod bezier { mouse::Event::Input { button: mouse::Button::Left, state: ButtonState::Pressed, - } => match self.state.pending { - None => { - self.state.pending = Some(Pending::One { - from: self.state.cursor_position, - }); - None - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: self.state.cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - Some(Curve { - from, - to, - control: self.state.cursor_position, - }) + } if bounds.contains(self.state.cursor_position) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: self.state.cursor_position, + }); + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: self.state.cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: self.state.cursor_position, + }) + } } - }, + } _ => None, }, } } fn draw(&self, bounds: Size) -> Vec { - let curves = self.state.cache.draw(bounds, &self.curves); + let content = self.state.cache.draw(bounds, |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, self.state.cursor_position); - vec![curves, pending_curve] + vec![content, pending_curve] } else { - vec![curves] + vec![content] } } } @@ -169,14 +178,16 @@ mod bezier { control: Point, } - impl Drawable for Curve { - fn draw(&self, frame: &mut Frame) { - let curve = Path::new(|p| { - p.move_to(self.from); - p.quadratic_curve_to(self.control, self.to); + 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(&curve, Stroke::default().with_width(2.0)); + frame.stroke(&curves, Stroke::default().with_width(2.0)); } } @@ -202,7 +213,7 @@ mod bezier { control: cursor_position, }; - curve.draw(&mut frame); + Curve::draw_all(&[curve], &mut frame); } }; diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 308cbfbb..ab771405 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -5,9 +5,6 @@ authors = ["Héctor Ramón Jiménez "] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] iced = { path = "../..", features = ["canvas", "async-std", "debug"] } iced_native = { path = "../../native" } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 8b3a92c7..c2beddef 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, Geometry, LineCap, Path, Stroke}, + executor, Application, Color, Command, Container, Element, Length, Point, + Settings, Size, Subscription, Vector, }; pub fn main() { @@ -11,8 +12,8 @@ pub fn main() { } struct Clock { - now: LocalTime, - clock: canvas::Cache, + now: chrono::DateTime, + clock: Cache, } #[derive(Debug, Clone, Copy)] @@ -28,7 +29,7 @@ impl Application for Clock { fn new(_flags: ()) -> (Self, Command) { ( Clock { - now: chrono::Local::now().into(), + now: chrono::Local::now(), clock: Default::default(), }, Command::none(), @@ -59,7 +60,7 @@ impl Application for Clock { } fn view(&mut self) -> Element { - let canvas = Canvas::new(self.clock.with(&self.now)) + let canvas = Canvas::new(self) .width(Length::Units(400)) .height(Length::Units(400)); @@ -73,69 +74,54 @@ impl Application for Clock { } } -#[derive(Debug, PartialEq, Eq)] -struct LocalTime { - hour: u32, - minute: u32, - second: u32, -} - -impl From> for LocalTime { - fn from(date_time: chrono::DateTime) -> LocalTime { +impl canvas::Program for Clock { + fn draw(&self, bounds: Size) -> Vec { use chrono::Timelike; - LocalTime { - hour: date_time.hour(), - minute: date_time.minute(), - second: date_time.second(), - } - } -} - -impl canvas::Drawable for LocalTime { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; + let clock = self.clock.draw(bounds, |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 2.0; + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); - let clock = Path::circle(center, radius); - frame.fill(&clock, Color::from_rgb8(0x12, 0x93, 0xD8)); + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); - let short_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - let long_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + let thin_stroke = Stroke { + width: radius / 100.0, + color: Color::WHITE, + line_cap: LineCap::Round, + ..Stroke::default() + }; - let thin_stroke = canvas::Stroke { - width: radius / 100.0, - color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }; + let wide_stroke = Stroke { + width: thin_stroke.width * 3.0, + ..thin_stroke + }; - let wide_stroke = canvas::Stroke { - width: thin_stroke.width * 3.0, - ..thin_stroke - }; + frame.translate(Vector::new(center.x, center.y)); - frame.translate(Vector::new(center.x, center.y)); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_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.minute(), 60)); + frame.stroke(&long_hand, wide_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.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke); + }) }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.second, 60)); - frame.stroke(&long_hand, thin_stroke); - }); + vec![clock] } } diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index c88cda50..0555aa96 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -5,9 +5,6 @@ authors = ["Héctor Ramón Jiménez "] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] iced = { path = "../..", features = ["canvas", "async-std", "debug"] } iced_native = { path = "../../native" } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9337c7b5..e2f107bd 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -81,6 +81,12 @@ struct State { } impl State { + const SUN_RADIUS: f32 = 70.0; + const ORBIT_RADIUS: f32 = 150.0; + const EARTH_RADIUS: f32 = 12.0; + const MOON_RADIUS: f32 = 4.0; + const MOON_DISTANCE: f32 = 28.0; + pub fn new() -> State { let now = Instant::now(); let (width, height) = window::Settings::default().size; @@ -95,17 +101,6 @@ impl State { } } - pub fn space(&self) -> Space<'_> { - Space { stars: &self.stars } - } - - pub fn system(&self) -> System { - System { - start: self.start, - now: self.now, - } - } - pub fn update(&mut self, now: Instant) { self.now = now; self.system_cache.clear(); @@ -136,106 +131,81 @@ impl State { } } -impl canvas::State for State { +impl canvas::Program for State { fn draw(&self, bounds: Size) -> Vec { - vec![ - self.space_cache.draw(bounds, self.space()), - self.system_cache.draw(bounds, self.system()), - ] - } -} + use canvas::{Path, Stroke}; + use std::f32::consts::PI; -#[derive(Debug)] -struct Space<'a> { - stars: &'a [(Point, f32)], -} + let background = self.space_cache.draw(bounds, |frame| { + let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); -impl canvas::Drawable for Space<'_> { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } + }); - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); + frame.fill(&space, Color::BLACK); - let stars = Path::new(|path| { - for (p, size) in self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); }); - frame.fill(&space, Color::BLACK); + let system = self.system_cache.draw(bounds, |frame| { + let center = frame.center(); - frame.translate(frame.center() - Point::ORIGIN); - frame.fill(&stars, Color::WHITE); - } -} + let sun = Path::circle(center, Self::SUN_RADIUS); + let orbit = Path::circle(center, Self::ORBIT_RADIUS); -#[derive(Debug)] -struct System { - start: Instant, - now: Instant, -} - -impl System { - const SUN_RADIUS: f32 = 70.0; - const ORBIT_RADIUS: f32 = 150.0; - const EARTH_RADIUS: f32 = 12.0; - const MOON_RADIUS: f32 = 4.0; - const MOON_DISTANCE: f32 = 28.0; -} - -impl canvas::Drawable for System { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::{Path, Stroke}; - use std::f32::consts::PI; - - let center = frame.center(); - - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); - - 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(&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() + }, + ); - 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; + 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.translate(Vector::new(center.x, center.y)); - frame.rotate(rotation); - frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); + frame.with_save(|frame| { + 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, + ), + ); - 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(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); - frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6)); + frame.with_save(|frame| { + frame.rotate(rotation * 10.0); + frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - frame.with_save(|frame| { - frame.rotate(rotation * 10.0); - frame.translate(Vector::new(0.0, Self::MOON_DISTANCE)); + let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); + frame.fill(&moon, Color::WHITE); + }); - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); + frame.fill( + &shadow, + Color { + a: 0.7, + ..Color::BLACK + }, + ); }); - - frame.fill( - &shadow, - Color { - a: 0.7, - ..Color::BLACK - }, - ); }); + + vec![background, system] } } -- cgit From 59403b6ca80081fa419fbef76c92397db68f1ab1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 03:11:15 +0200 Subject: Remove `OutOfBounds` variant from `MouseCursor` --- examples/custom_widget/src/main.rs | 2 +- examples/geometry/src/main.rs | 2 +- examples/integration/src/main.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'examples') diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0a570745..d0bceb73 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -66,7 +66,7 @@ mod circle { border_width: 0, border_color: Color::TRANSPARENT, }, - MouseCursor::OutOfBounds, + MouseCursor::default(), ) } } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 795cac27..2a3efd4a 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -141,7 +141,7 @@ mod rainbow { }, }), }, - MouseCursor::OutOfBounds, + MouseCursor::default(), ) } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7203d4b6..da571ed1 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -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, MouseCursor::default()); let clipboard = Clipboard::new(&window); // Initialize scene and GUI controls -- cgit From 5586034d6626e013cdd718aca1c4f19f6a060ff3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 03:23:27 +0200 Subject: Display crosshair cursor in `bezier_tool` example --- examples/bezier_tool/src/main.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 2112f662..8c9ebd75 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -70,7 +70,8 @@ impl Sandbox for Example { mod bezier { use iced::{ canvas::{self, Canvas, Event, Frame, Geometry, Path, Stroke}, - mouse, ButtonState, Element, Length, Point, Rectangle, Size, + mouse, ButtonState, Element, Length, MouseCursor, Point, Rectangle, + Size, }; #[derive(Default)] @@ -106,8 +107,6 @@ mod bezier { impl<'a> canvas::Program for Bezier<'a> { fn update(&mut self, event: Event, bounds: Size) -> Option { - let bounds = Rectangle::new(Point::ORIGIN, bounds); - match event { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::CursorMoved { x, y } => { @@ -118,7 +117,9 @@ mod bezier { mouse::Event::Input { button: mouse::Button::Left, state: ButtonState::Pressed, - } if bounds.contains(self.state.cursor_position) => { + } if Rectangle::with_size(bounds) + .contains(self.state.cursor_position) => + { match self.state.pending { None => { self.state.pending = Some(Pending::One { @@ -169,6 +170,15 @@ mod bezier { vec![content] } } + + fn mouse_cursor(&self, bounds: Size) -> MouseCursor { + if Rectangle::with_size(bounds).contains(self.state.cursor_position) + { + MouseCursor::Crosshair + } else { + MouseCursor::default() + } + } } #[derive(Debug, Clone, Copy)] -- cgit From dc51080328caa12d2b1fc02febc72cab70bb9f50 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 04:25:49 +0200 Subject: Introduce `Cursor` type in `canvas` --- examples/bezier_tool/src/main.rs | 137 +++++++++++++++++++------------------- examples/clock/src/main.rs | 8 +-- examples/solar_system/src/main.rs | 16 +++-- 3 files changed, 83 insertions(+), 78 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 8c9ebd75..6473db75 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -69,15 +69,13 @@ impl Sandbox for Example { mod bezier { use iced::{ - canvas::{self, Canvas, Event, Frame, Geometry, Path, Stroke}, + canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, mouse, ButtonState, Element, Length, MouseCursor, Point, Rectangle, - Size, }; #[derive(Default)] pub struct State { pending: Option, - cursor_position: Point, cache: canvas::Cache, } @@ -106,64 +104,62 @@ mod bezier { } impl<'a> canvas::Program for Bezier<'a> { - fn update(&mut self, event: Event, bounds: Size) -> Option { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + let cursor_position = cursor.internal_position(&bounds)?; + match event { Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::CursorMoved { x, y } => { - self.state.cursor_position = Point::new(x, y); - - None - } mouse::Event::Input { button: mouse::Button::Left, state: ButtonState::Pressed, - } if Rectangle::with_size(bounds) - .contains(self.state.cursor_position) => - { - match self.state.pending { - None => { - self.state.pending = Some(Pending::One { - from: self.state.cursor_position, - }); - None - } - Some(Pending::One { from }) => { - self.state.pending = Some(Pending::Two { - from, - to: self.state.cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - self.state.pending = None; - - Some(Curve { - from, - to, - control: self.state.cursor_position, - }) - } + } => 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: Size) -> Vec { - let content = self.state.cache.draw(bounds, |frame: &mut Frame| { - Curve::draw_all(self.curves, frame); + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + 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(), - ); - }); + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default(), + ); + }); if let Some(pending) = &self.state.pending { - let pending_curve = - pending.draw(bounds, self.state.cursor_position); + let pending_curve = pending.draw(bounds, cursor); vec![content, pending_curve] } else { @@ -171,9 +167,12 @@ mod bezier { } } - fn mouse_cursor(&self, bounds: Size) -> MouseCursor { - if Rectangle::with_size(bounds).contains(self.state.cursor_position) - { + fn mouse_cursor( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> MouseCursor { + if cursor.is_over(&bounds) { MouseCursor::Crosshair } else { MouseCursor::default() @@ -208,24 +207,26 @@ mod bezier { } impl Pending { - fn draw(&self, bounds: Size, cursor_position: Point) -> Geometry { - let mut frame = Frame::new(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); - } - }; + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { + let mut frame = Frame::new(bounds.size()); + + if let Some(cursor_position) = cursor.internal_position(&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/src/main.rs b/examples/clock/src/main.rs index c2beddef..e6b17d8a 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,7 +1,7 @@ use iced::{ - canvas::{self, Cache, Canvas, Geometry, LineCap, Path, Stroke}, + canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, executor, Application, Color, Command, Container, Element, Length, Point, - Settings, Size, Subscription, Vector, + Rectangle, Settings, Subscription, Vector, }; pub fn main() { @@ -75,10 +75,10 @@ impl Application for Clock { } impl canvas::Program for Clock { - fn draw(&self, bounds: Size) -> Vec { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { use chrono::Timelike; - let clock = self.clock.draw(bounds, |frame| { + let clock = self.clock.draw(bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index e2f107bd..a25e43df 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, Element, - Length, Point, Settings, Size, Subscription, Vector, + canvas::{self, Cursor, Path, Stroke}, + executor, window, Application, Canvas, Color, Command, Element, Length, + Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -132,11 +133,14 @@ impl State { } impl canvas::Program for State { - fn draw(&self, bounds: Size) -> Vec { - use canvas::{Path, Stroke}; + fn draw( + &self, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { use std::f32::consts::PI; - let background = self.space_cache.draw(bounds, |frame| { + let background = self.space_cache.draw(bounds.size(), |frame| { let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); let stars = Path::new(|path| { @@ -151,7 +155,7 @@ impl canvas::Program for State { frame.fill(&stars, Color::WHITE); }); - let system = self.system_cache.draw(bounds, |frame| { + let system = self.system_cache.draw(bounds.size(), |frame| { let center = frame.center(); let sun = Path::circle(center, Self::SUN_RADIUS); -- cgit From 70f86f998b6db102d5b77f756750414efd53aa9e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 08:25:42 +0200 Subject: Add `game_of_life` example RIP John Conway --- examples/game_of_life/Cargo.toml | 12 ++ examples/game_of_life/README.md | 18 ++ examples/game_of_life/src/main.rs | 408 +++++++++++++++++++++++++++++++++++++ examples/game_of_life/src/style.rs | 96 +++++++++ examples/game_of_life/src/time.rs | 34 ++++ 5 files changed, 568 insertions(+) create mode 100644 examples/game_of_life/Cargo.toml create mode 100644 examples/game_of_life/README.md create mode 100644 examples/game_of_life/src/main.rs create mode 100644 examples/game_of_life/src/style.rs create mode 100644 examples/game_of_life/src/time.rs (limited to 'examples') diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..8855b3e8 --- /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 "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["async-std", "canvas", "debug"] } +iced_native = { path = "../../native" } +async-std = { version = "1.0", features = ["unstable"] } +itertools = "0.9" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..ebbb12cc --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,18 @@ +## Bézier tool + +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. + +The __[`main`]__ file contains all the code of the example. + + + +You can run it with `cargo run`: +``` +cargo run --package bezier_tool +``` + +[`main`]: src/main.rs diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..3e6848df --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,408 @@ +//! 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; +mod time; + +use grid::Grid; +use iced::{ + button::{self, Button}, + executor, + slider::{self, Slider}, + Align, Application, 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, + is_playing: bool, + speed: u64, + next_speed: Option, + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message), + Tick(Instant), + Toggle, + Next, + Clear, + SpeedChanged(f32), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + ( + Self { + speed: 1, + ..Self::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Game of Life - Iced") + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Grid(message) => { + self.grid.update(message); + } + Message::Tick(_) | Message::Next => { + self.grid.tick(); + + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + } + Message::Toggle => { + self.is_playing = !self.is_playing; + } + Message::Clear => { + self.grid = Grid::default(); + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as u64); + } else { + self.speed = speed.round() as u64; + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if self.is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::Toggle) + .style(style::Button), + ) + .push( + Button::new(&mut self.next_button, Text::new("Next")) + .on_press(Message::Next) + .style(style::Button), + ) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Button), + ); + + let selected_speed = self.next_speed.unwrap_or(self.speed); + let speed_controls = Row::new() + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=20.0, + selected_speed as f32, + Message::SpeedChanged, + ) + .width(Length::Units(200)) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", selected_speed)).size(16)) + .align_items(Align::Center); + + let controls = Row::new() + .spacing(20) + .push(playback_controls) + .push(speed_controls); + + let content = Column::new() + .spacing(10) + .padding(10) + .align_items(Align::Center) + .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, Canvas, Cursor, Event, Frame, Geometry, Path}, + mouse, ButtonState, Color, Element, Length, MouseCursor, Point, + Rectangle, Size, Vector, + }; + + const SIZE: usize = 32; + + #[derive(Default)] + pub struct Grid { + cells: [[Cell; SIZE]; SIZE], + mouse_pressed: bool, + cache: canvas::Cache, + } + + impl Grid { + pub fn tick(&mut self) { + let mut populated_neighbors: [[usize; SIZE]; SIZE] = + [[0; SIZE]; SIZE]; + + for (i, row) in self.cells.iter().enumerate() { + for (j, _) in row.iter().enumerate() { + populated_neighbors[i][j] = self.populated_neighbors(i, j); + } + } + + for (i, row) in populated_neighbors.iter().enumerate() { + for (j, amount) in row.iter().enumerate() { + let is_populated = self.cells[i][j] == Cell::Populated; + + self.cells[i][j] = match amount { + 2 if is_populated => Cell::Populated, + 3 => Cell::Populated, + _ => Cell::Unpopulated, + }; + } + } + + self.cache.clear() + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate { i, j } => { + self.cells[i][j] = Cell::Populated; + self.cache.clear() + } + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + fn populated_neighbors(&self, row: usize, column: usize) -> usize { + use itertools::Itertools; + + let rows = row.saturating_sub(1)..=row + 1; + let columns = column.saturating_sub(1)..=column + 1; + + let is_inside_bounds = |i: usize, j: usize| i < SIZE && j < SIZE; + let is_neighbor = |i: usize, j: usize| i != row || j != column; + + let is_populated = + |i: usize, j: usize| self.cells[i][j] == Cell::Populated; + + rows.cartesian_product(columns) + .filter(|&(i, j)| { + is_inside_bounds(i, j) + && is_neighbor(i, j) + && is_populated(i, j) + }) + .count() + } + + fn region(&self, size: Size) -> Rectangle { + let side = size.width.min(size.height); + + Rectangle { + x: (size.width - side) / 2.0, + y: (size.height - side) / 2.0, + width: side, + height: side, + } + } + + fn cell_at( + &self, + region: Rectangle, + position: Point, + ) -> Option<(usize, usize)> { + if region.contains(position) { + let cell_size = region.width / SIZE as f32; + + let i = ((position.y - region.y) / cell_size).ceil() as usize; + let j = ((position.x - region.x) / cell_size).ceil() as usize; + + Some((i.saturating_sub(1), j.saturating_sub(1))) + } else { + None + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Cell { + Unpopulated, + Populated, + } + + impl Default for Cell { + fn default() -> Cell { + Cell::Unpopulated + } + } + + #[derive(Debug, Clone, Copy)] + pub enum Message { + Populate { i: usize, j: usize }, + } + + impl<'a> canvas::Program for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + if let Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + state, + }) = event + { + self.mouse_pressed = state == ButtonState::Pressed; + } + + let cursor_position = cursor.internal_position(&bounds)?; + + let region = self.region(bounds.size()); + let (i, j) = self.cell_at(region, cursor_position)?; + + let populate = if self.cells[i][j] != Cell::Populated { + Some(Message::Populate { i, j }) + } else { + None + }; + + match event { + Event::Mouse(mouse::Event::Input { + button: mouse::Button::Left, + .. + }) if self.mouse_pressed => populate, + Event::Mouse(mouse::Event::CursorMoved { .. }) + if self.mouse_pressed => + { + populate + } + _ => None, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + let region = self.region(bounds.size()); + let cell_size = Size::new(1.0, 1.0); + + let life = self.cache.draw(bounds.size(), |frame| { + let background = + Path::rectangle(region.position(), region.size()); + frame.fill( + &background, + Color::from_rgb( + 0x40 as f32 / 255.0, + 0x44 as f32 / 255.0, + 0x4B as f32 / 255.0, + ), + ); + + frame.with_save(|frame| { + frame.translate(Vector::new(region.x, region.y)); + frame.scale(region.width / SIZE as f32); + + let cells = Path::new(|p| { + for (i, row) in self.cells.iter().enumerate() { + for (j, cell) in row.iter().enumerate() { + if *cell == Cell::Populated { + p.rectangle( + Point::new(j as f32, i as f32), + cell_size, + ); + } + } + } + }); + frame.fill(&cells, Color::WHITE); + }); + }); + + let hovered_cell = { + let mut frame = Frame::new(bounds.size()); + + frame.translate(Vector::new(region.x, region.y)); + frame.scale(region.width / SIZE as f32); + + if let Some(cursor_position) = cursor.internal_position(&bounds) + { + if let Some((i, j)) = self.cell_at(region, cursor_position) + { + let interaction = Path::rectangle( + Point::new(j as f32, i as f32), + cell_size, + ); + + frame.fill( + &interaction, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + } + } + + frame.into_geometry() + }; + + vec![life, hovered_cell] + } + + fn mouse_cursor( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> MouseCursor { + let region = self.region(bounds.size()); + + match cursor.internal_position(&bounds) { + Some(position) if region.contains(position) => { + MouseCursor::Crosshair + } + _ => MouseCursor::default(), + } + } + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..0becb5be --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,96 @@ +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 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 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/game_of_life/src/time.rs b/examples/game_of_life/src/time.rs new file mode 100644 index 00000000..7b475ecd --- /dev/null +++ b/examples/game_of_life/src/time.rs @@ -0,0 +1,34 @@ +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() + } +} -- cgit From 5d12e194f45b4a01034f3f52fae16c10bc0192dd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 20:58:59 +0200 Subject: Rename `Cursor::*_position` methods in `canvas` --- examples/bezier_tool/src/main.rs | 4 ++-- examples/game_of_life/src/main.rs | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 6473db75..3cecd058 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -110,7 +110,7 @@ mod bezier { bounds: Rectangle, cursor: Cursor, ) -> Option { - let cursor_position = cursor.internal_position(&bounds)?; + let cursor_position = cursor.position_in(&bounds)?; match event { Event::Mouse(mouse_event) => match mouse_event { @@ -210,7 +210,7 @@ mod bezier { fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { let mut frame = Frame::new(bounds.size()); - if let Some(cursor_position) = cursor.internal_position(&bounds) { + if let Some(cursor_position) = cursor.position_in(&bounds) { match *self { Pending::One { from } => { let line = Path::line(from, cursor_position); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3e6848df..a2628594 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -298,7 +298,7 @@ mod grid { self.mouse_pressed = state == ButtonState::Pressed; } - let cursor_position = cursor.internal_position(&bounds)?; + let cursor_position = cursor.position_in(&bounds)?; let region = self.region(bounds.size()); let (i, j) = self.cell_at(region, cursor_position)?; @@ -365,8 +365,7 @@ mod grid { frame.translate(Vector::new(region.x, region.y)); frame.scale(region.width / SIZE as f32); - if let Some(cursor_position) = cursor.internal_position(&bounds) - { + if let Some(cursor_position) = cursor.position_in(&bounds) { if let Some((i, j)) = self.cell_at(region, cursor_position) { let interaction = Path::rectangle( @@ -397,7 +396,7 @@ mod grid { ) -> MouseCursor { let region = self.region(bounds.size()); - match cursor.internal_position(&bounds) { + match cursor.position_in(&bounds) { Some(position) if region.contains(position) => { MouseCursor::Crosshair } -- cgit From 5e014a70e864964570e4c1568926b8c647f73c59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 23:50:15 +0200 Subject: Use sparse grid representation in `game_of_life` --- examples/game_of_life/src/main.rs | 137 +++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 68 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index a2628594..fb12afa1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -14,10 +14,7 @@ use iced::{ use std::time::{Duration, Instant}; pub fn main() { - GameOfLife::run(Settings { - antialiasing: true, - ..Settings::default() - }) + GameOfLife::run(Settings::default()) } #[derive(Default)] @@ -164,36 +161,55 @@ mod grid { mouse, ButtonState, Color, Element, Length, MouseCursor, Point, Rectangle, Size, Vector, }; + use std::collections::{HashMap, HashSet}; - const SIZE: usize = 32; + const CELL_SIZE: usize = 20; #[derive(Default)] pub struct Grid { - cells: [[Cell; SIZE]; SIZE], + alive_cells: HashSet<(usize, usize)>, mouse_pressed: bool, cache: canvas::Cache, } impl Grid { - pub fn tick(&mut self) { - let mut populated_neighbors: [[usize; SIZE]; SIZE] = - [[0; SIZE]; SIZE]; + fn with_neighbors( + i: usize, + j: usize, + ) -> impl Iterator { + use itertools::Itertools; - for (i, row) in self.cells.iter().enumerate() { - for (j, _) in row.iter().enumerate() { - populated_neighbors[i][j] = self.populated_neighbors(i, j); - } - } + let rows = i.saturating_sub(1)..=i.saturating_add(1); + let columns = j.saturating_sub(1)..=j.saturating_add(1); - for (i, row) in populated_neighbors.iter().enumerate() { - for (j, amount) in row.iter().enumerate() { - let is_populated = self.cells[i][j] == Cell::Populated; + rows.cartesian_product(columns) + } - self.cells[i][j] = match amount { - 2 if is_populated => Cell::Populated, - 3 => Cell::Populated, - _ => Cell::Unpopulated, - }; + pub fn tick(&mut self) { + use itertools::Itertools; + + let populated_neighbors: HashMap<(usize, usize), usize> = self + .alive_cells + .iter() + .flat_map(|&(i, j)| Self::with_neighbors(i, j)) + .unique() + .map(|(i, j)| ((i, j), self.populated_neighbors(i, j))) + .collect(); + + for (&(i, j), amount) in populated_neighbors.iter() { + let is_populated = self.alive_cells.contains(&(i, j)); + + match amount { + 2 if is_populated => {} + 3 => { + if !is_populated { + self.alive_cells.insert((i, j)); + } + } + _ if is_populated => { + self.alive_cells.remove(&(i, j)); + } + _ => {} } } @@ -202,8 +218,8 @@ mod grid { pub fn update(&mut self, message: Message) { match message { - Message::Populate { i, j } => { - self.cells[i][j] = Cell::Populated; + Message::Populate { cell } => { + self.alive_cells.insert(cell); self.cache.clear() } } @@ -217,34 +233,28 @@ mod grid { } fn populated_neighbors(&self, row: usize, column: usize) -> usize { - use itertools::Itertools; + let with_neighbors = Self::with_neighbors(row, column); - let rows = row.saturating_sub(1)..=row + 1; - let columns = column.saturating_sub(1)..=column + 1; - - let is_inside_bounds = |i: usize, j: usize| i < SIZE && j < SIZE; let is_neighbor = |i: usize, j: usize| i != row || j != column; - let is_populated = - |i: usize, j: usize| self.cells[i][j] == Cell::Populated; + |i: usize, j: usize| self.alive_cells.contains(&(i, j)); - rows.cartesian_product(columns) - .filter(|&(i, j)| { - is_inside_bounds(i, j) - && is_neighbor(i, j) - && is_populated(i, j) - }) + with_neighbors + .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) .count() } fn region(&self, size: Size) -> Rectangle { - let side = size.width.min(size.height); + let width = + (size.width / CELL_SIZE as f32).floor() * CELL_SIZE as f32; + let height = + (size.height / CELL_SIZE as f32).floor() * CELL_SIZE as f32; Rectangle { - x: (size.width - side) / 2.0, - y: (size.height - side) / 2.0, - width: side, - height: side, + x: (size.width - width) / 2.0, + y: (size.height - height) / 2.0, + width, + height, } } @@ -254,10 +264,10 @@ mod grid { position: Point, ) -> Option<(usize, usize)> { if region.contains(position) { - let cell_size = region.width / SIZE as f32; - - let i = ((position.y - region.y) / cell_size).ceil() as usize; - let j = ((position.x - region.x) / cell_size).ceil() as usize; + let i = ((position.y - region.y) / CELL_SIZE as f32).ceil() + as usize; + let j = ((position.x - region.x) / CELL_SIZE as f32).ceil() + as usize; Some((i.saturating_sub(1), j.saturating_sub(1))) } else { @@ -266,21 +276,9 @@ mod grid { } } - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - enum Cell { - Unpopulated, - Populated, - } - - impl Default for Cell { - fn default() -> Cell { - Cell::Unpopulated - } - } - #[derive(Debug, Clone, Copy)] pub enum Message { - Populate { i: usize, j: usize }, + Populate { cell: (usize, usize) }, } impl<'a> canvas::Program for Grid { @@ -301,12 +299,12 @@ mod grid { let cursor_position = cursor.position_in(&bounds)?; let region = self.region(bounds.size()); - let (i, j) = self.cell_at(region, cursor_position)?; + let cell = self.cell_at(region, cursor_position)?; - let populate = if self.cells[i][j] != Cell::Populated { - Some(Message::Populate { i, j }) - } else { + let populate = if self.alive_cells.contains(&cell) { None + } else { + Some(Message::Populate { cell }) }; match event { @@ -339,14 +337,17 @@ mod grid { ), ); + let visible_rows = region.height as usize / CELL_SIZE; + let visible_columns = region.width as usize / CELL_SIZE; + frame.with_save(|frame| { frame.translate(Vector::new(region.x, region.y)); - frame.scale(region.width / SIZE as f32); + frame.scale(CELL_SIZE as f32); let cells = Path::new(|p| { - for (i, row) in self.cells.iter().enumerate() { - for (j, cell) in row.iter().enumerate() { - if *cell == Cell::Populated { + for i in 0..visible_rows { + for j in 0..visible_columns { + if self.alive_cells.contains(&(i, j)) { p.rectangle( Point::new(j as f32, i as f32), cell_size, @@ -363,7 +364,7 @@ mod grid { let mut frame = Frame::new(bounds.size()); frame.translate(Vector::new(region.x, region.y)); - frame.scale(region.width / SIZE as f32); + frame.scale(CELL_SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { if let Some((i, j)) = self.cell_at(region, cursor_position) -- cgit From 611d9e399c95268a3daf41bd6cbcc55391ccff87 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 29 Apr 2020 23:55:15 +0200 Subject: Clarify `tick` logic in `game_of_life` --- examples/game_of_life/src/main.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index fb12afa1..3989e3ea 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -200,14 +200,12 @@ mod grid { let is_populated = self.alive_cells.contains(&(i, j)); match amount { - 2 if is_populated => {} + 2 | 3 if is_populated => {} 3 => { - if !is_populated { - self.alive_cells.insert((i, j)); - } + let _ = self.alive_cells.insert((i, j)); } _ if is_populated => { - self.alive_cells.remove(&(i, j)); + let _ = self.alive_cells.remove(&(i, j)); } _ => {} } -- cgit From af95d3972e4ab6bf4ace54ddd44379ffcebbcff6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 04:12:13 +0200 Subject: Implement camera panning in `game_of_life` example --- examples/game_of_life/src/main.rs | 165 +++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 75 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3989e3ea..983d6cb4 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -136,13 +136,13 @@ impl Application for GameOfLife { .align_items(Align::Center); let controls = Row::new() + .padding(10) .spacing(20) .push(playback_controls) .push(speed_controls); let content = Column::new() .spacing(10) - .padding(10) .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -167,16 +167,27 @@ mod grid { #[derive(Default)] pub struct Grid { - alive_cells: HashSet<(usize, usize)>, - mouse_pressed: bool, + alive_cells: HashSet<(isize, isize)>, + interaction: Option, cache: canvas::Cache, + translation: Vector, + } + + #[derive(Debug, Clone, Copy)] + pub enum Message { + Populate { cell: (isize, isize) }, + } + + enum Interaction { + Drawing, + Panning { translation: Vector, start: Point }, } impl Grid { fn with_neighbors( - i: usize, - j: usize, - ) -> impl Iterator { + i: isize, + j: isize, + ) -> impl Iterator { use itertools::Itertools; let rows = i.saturating_sub(1)..=i.saturating_add(1); @@ -188,7 +199,7 @@ mod grid { pub fn tick(&mut self) { use itertools::Itertools; - let populated_neighbors: HashMap<(usize, usize), usize> = self + let populated_neighbors: HashMap<(isize, isize), usize> = self .alive_cells .iter() .flat_map(|&(i, j)| Self::with_neighbors(i, j)) @@ -230,55 +241,26 @@ mod grid { .into() } - fn populated_neighbors(&self, row: usize, column: usize) -> usize { + fn populated_neighbors(&self, row: isize, column: isize) -> usize { let with_neighbors = Self::with_neighbors(row, column); - let is_neighbor = |i: usize, j: usize| i != row || j != column; + let is_neighbor = |i: isize, j: isize| i != row || j != column; let is_populated = - |i: usize, j: usize| self.alive_cells.contains(&(i, j)); + |i: isize, j: isize| self.alive_cells.contains(&(i, j)); with_neighbors .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) .count() } - fn region(&self, size: Size) -> Rectangle { - let width = - (size.width / CELL_SIZE as f32).floor() * CELL_SIZE as f32; - let height = - (size.height / CELL_SIZE as f32).floor() * CELL_SIZE as f32; - - Rectangle { - x: (size.width - width) / 2.0, - y: (size.height - height) / 2.0, - width, - height, - } - } + fn cell_at(&self, position: Point) -> Option<(isize, isize)> { + let i = (position.y / CELL_SIZE as f32).ceil() as isize; + let j = (position.x / CELL_SIZE as f32).ceil() as isize; - fn cell_at( - &self, - region: Rectangle, - position: Point, - ) -> Option<(usize, usize)> { - if region.contains(position) { - let i = ((position.y - region.y) / CELL_SIZE as f32).ceil() - as usize; - let j = ((position.x - region.x) / CELL_SIZE as f32).ceil() - as usize; - - Some((i.saturating_sub(1), j.saturating_sub(1))) - } else { - None - } + Some((i.saturating_sub(1), j.saturating_sub(1))) } } - #[derive(Debug, Clone, Copy)] - pub enum Message { - Populate { cell: (usize, usize) }, - } - impl<'a> canvas::Program for Grid { fn update( &mut self, @@ -287,17 +269,15 @@ mod grid { cursor: Cursor, ) -> Option { if let Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, + state: ButtonState::Released, + .. }) = event { - self.mouse_pressed = state == ButtonState::Pressed; + self.interaction = None; } let cursor_position = cursor.position_in(&bounds)?; - - let region = self.region(bounds.size()); - let cell = self.cell_at(region, cursor_position)?; + let cell = self.cell_at(cursor_position - self.translation)?; let populate = if self.alive_cells.contains(&cell) { None @@ -306,26 +286,53 @@ mod grid { }; match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - .. - }) if self.mouse_pressed => populate, - Event::Mouse(mouse::Event::CursorMoved { .. }) - if self.mouse_pressed => - { - populate - } - _ => None, + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::Input { + button, + state: ButtonState::Pressed, + } => match button { + mouse::Button::Left => { + self.interaction = Some(Interaction::Drawing); + + populate + } + mouse::Button::Right => { + self.interaction = Some(Interaction::Panning { + translation: self.translation, + start: cursor_position, + }); + + None + } + _ => None, + }, + mouse::Event::CursorMoved { .. } => { + match self.interaction { + Some(Interaction::Drawing) => populate, + Some(Interaction::Panning { + translation, + start, + }) => { + self.translation = + translation + (cursor_position - start); + + self.cache.clear(); + + None + } + _ => None, + } + } + _ => None, + }, } } fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { - let region = self.region(bounds.size()); let cell_size = Size::new(1.0, 1.0); let life = self.cache.draw(bounds.size(), |frame| { - let background = - Path::rectangle(region.position(), region.size()); + let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill( &background, Color::from_rgb( @@ -335,16 +342,25 @@ mod grid { ), ); - let visible_rows = region.height as usize / CELL_SIZE; - let visible_columns = region.width as usize / CELL_SIZE; + let first_row = + (-self.translation.y / CELL_SIZE as f32).floor() as isize; + let first_column = + (-self.translation.x / CELL_SIZE as f32).floor() as isize; + + let visible_rows = + (frame.height() / CELL_SIZE as f32).ceil() as isize; + let visible_columns = + (frame.width() / CELL_SIZE as f32).ceil() as isize; frame.with_save(|frame| { - frame.translate(Vector::new(region.x, region.y)); + frame.translate(self.translation); frame.scale(CELL_SIZE as f32); let cells = Path::new(|p| { - for i in 0..visible_rows { - for j in 0..visible_columns { + for i in first_row..=(first_row + visible_rows) { + for j in + first_column..=(first_column + visible_columns) + { if self.alive_cells.contains(&(i, j)) { p.rectangle( Point::new(j as f32, i as f32), @@ -361,11 +377,12 @@ mod grid { let hovered_cell = { let mut frame = Frame::new(bounds.size()); - frame.translate(Vector::new(region.x, region.y)); + frame.translate(self.translation); frame.scale(CELL_SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - if let Some((i, j)) = self.cell_at(region, cursor_position) + if let Some((i, j)) = + self.cell_at(cursor_position - self.translation) { let interaction = Path::rectangle( Point::new(j as f32, i as f32), @@ -393,12 +410,10 @@ mod grid { bounds: Rectangle, cursor: Cursor, ) -> MouseCursor { - let region = self.region(bounds.size()); - - match cursor.position_in(&bounds) { - Some(position) if region.contains(position) => { - MouseCursor::Crosshair - } + match self.interaction { + Some(Interaction::Drawing) => MouseCursor::Crosshair, + Some(Interaction::Panning { .. }) => MouseCursor::Grabbing, + None if cursor.is_over(&bounds) => MouseCursor::Crosshair, _ => MouseCursor::default(), } } -- cgit From e55cd9652e7c7aea4dc2c6ccb83769246d1a808e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 04:53:15 +0200 Subject: Split `Input` mouse event by `ButtonState` --- examples/bezier_tool/src/main.rs | 55 +++++++++++++++++++-------------------- examples/game_of_life/src/main.rs | 15 +++-------- 2 files changed, 31 insertions(+), 39 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 3cecd058..fe4136b4 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -70,7 +70,7 @@ impl Sandbox for Example { mod bezier { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, - mouse, ButtonState, Element, Length, MouseCursor, Point, Rectangle, + mouse, Element, Length, MouseCursor, Point, Rectangle, }; #[derive(Default)] @@ -114,34 +114,33 @@ mod bezier { match event { Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - } => match self.state.pending { - None => { - self.state.pending = Some(Pending::One { - from: cursor_position, - }); - None + 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, + }) + } } - 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, }, } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 983d6cb4..9fb4c3e7 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -158,8 +158,8 @@ impl Application for GameOfLife { mod grid { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, ButtonState, Color, Element, Length, MouseCursor, Point, - Rectangle, Size, Vector, + mouse, Color, Element, Length, MouseCursor, Point, Rectangle, Size, + Vector, }; use std::collections::{HashMap, HashSet}; @@ -268,11 +268,7 @@ mod grid { bounds: Rectangle, cursor: Cursor, ) -> Option { - if let Event::Mouse(mouse::Event::Input { - state: ButtonState::Released, - .. - }) = event - { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { self.interaction = None; } @@ -287,10 +283,7 @@ mod grid { match event { Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::Input { - button, - state: ButtonState::Pressed, - } => match button { + mouse::Event::ButtonPressed(button) => match button { mouse::Button::Left => { self.interaction = Some(Interaction::Drawing); -- cgit From e2076612cb98d04a8a48add14d0068c2974d5653 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 05:37:44 +0200 Subject: Implement `time::every` in `iced_futures` --- examples/clock/Cargo.toml | 4 +--- examples/clock/src/main.rs | 46 +++++---------------------------------- examples/game_of_life/Cargo.toml | 4 +--- examples/game_of_life/src/main.rs | 3 +-- examples/game_of_life/src/time.rs | 34 ----------------------------- examples/solar_system/Cargo.toml | 4 +--- examples/solar_system/src/main.rs | 40 ++-------------------------------- examples/stopwatch/Cargo.toml | 5 +---- examples/stopwatch/src/main.rs | 44 ++++--------------------------------- 9 files changed, 16 insertions(+), 168 deletions(-) delete mode 100644 examples/game_of_life/src/time.rs (limited to 'examples') diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index ab771405..c6e32379 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -6,7 +6,5 @@ edition = "2018" publish = false [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 e6b17d8a..9c583c78 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,7 +1,7 @@ use iced::{ canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, - executor, Application, Color, Command, Container, Element, Length, Point, - Rectangle, Settings, Subscription, Vector, + executor, time, Application, Color, Command, Container, Element, Length, + Point, Rectangle, Settings, Subscription, Vector, }; pub fn main() { @@ -43,7 +43,7 @@ impl Application for Clock { fn update(&mut self, message: Message) -> Command { match message { Message::Tick(local_time) => { - let now = local_time.into(); + let now = local_time; if now != self.now { self.now = now; @@ -56,7 +56,8 @@ impl Application for Clock { } fn subscription(&self) -> Subscription { - 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 { @@ -130,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> { - 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 = chrono::DateTime; - - 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(|_| chrono::Local::now()) - .boxed() - } - } -} diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 8855b3e8..413493ae 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -6,7 +6,5 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["async-std", "canvas", "debug"] } -iced_native = { path = "../../native" } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["canvas", "tokio"] } itertools = "0.9" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9fb4c3e7..5a58a8cb 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,14 +1,13 @@ //! 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; -mod time; use grid::Grid; use iced::{ button::{self, Button}, executor, slider::{self, Slider}, - Align, Application, Column, Command, Container, Element, Length, Row, + time, Align, Application, Column, Command, Container, Element, Length, Row, Settings, Subscription, Text, }; use std::time::{Duration, Instant}; diff --git a/examples/game_of_life/src/time.rs b/examples/game_of_life/src/time.rs deleted file mode 100644 index 7b475ecd..00000000 --- a/examples/game_of_life/src/time.rs +++ /dev/null @@ -1,34 +0,0 @@ -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() - } -} diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index 0555aa96..44ced729 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -6,7 +6,5 @@ edition = "2018" publish = false [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 a25e43df..98bd3b21 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -8,8 +8,8 @@ //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::{ canvas::{self, Cursor, Path, Stroke}, - executor, window, Application, Canvas, Color, Command, Element, Length, - Point, Rectangle, Settings, Size, Subscription, Vector, + executor, time, window, Application, Canvas, Color, Command, Element, + Length, Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -212,39 +212,3 @@ impl canvas::Program for State { vec![background, system] } } - -mod time { - use iced::futures; - use std::time::Instant; - - 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 = 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(|_| Instant::now()) - .boxed() - } - } -} 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 { - 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}; -- cgit From 98bc8cf2a7c4944d762a0148ca9f615d6ccc0d6e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 08:16:38 +0200 Subject: Rename `MouseCursor` to `mouse::Interaction` --- examples/bezier_tool/src/main.rs | 10 +++++----- examples/custom_widget/src/main.rs | 8 ++++---- examples/game_of_life/src/main.rs | 19 +++++++++++-------- examples/geometry/src/main.rs | 8 ++++---- examples/integration/src/main.rs | 14 ++++++++------ 5 files changed, 32 insertions(+), 27 deletions(-) (limited to 'examples') diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fe4136b4..fe41e1b2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -70,7 +70,7 @@ impl Sandbox for Example { mod bezier { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, - mouse, Element, Length, MouseCursor, Point, Rectangle, + mouse, Element, Length, Point, Rectangle, }; #[derive(Default)] @@ -166,15 +166,15 @@ mod bezier { } } - fn mouse_cursor( + fn mouse_interaction( &self, bounds: Rectangle, cursor: Cursor, - ) -> MouseCursor { + ) -> mouse::Interaction { if cursor.is_over(&bounds) { - MouseCursor::Crosshair + mouse::Interaction::Crosshair } else { - MouseCursor::default() + mouse::Interaction::default() } } } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index d0bceb73..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::default(), + mouse::Interaction::default(), ) } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 5a58a8cb..f0891db1 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -157,8 +157,7 @@ impl Application for GameOfLife { mod grid { use iced::{ canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, Color, Element, Length, MouseCursor, Point, Rectangle, Size, - Vector, + mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use std::collections::{HashMap, HashSet}; @@ -397,16 +396,20 @@ mod grid { vec![life, hovered_cell] } - fn mouse_cursor( + fn mouse_interaction( &self, bounds: Rectangle, cursor: Cursor, - ) -> MouseCursor { + ) -> mouse::Interaction { match self.interaction { - Some(Interaction::Drawing) => MouseCursor::Crosshair, - Some(Interaction::Panning { .. }) => MouseCursor::Grabbing, - None if cursor.is_over(&bounds) => MouseCursor::Crosshair, - _ => MouseCursor::default(), + Some(Interaction::Drawing) => mouse::Interaction::Crosshair, + Some(Interaction::Panning { .. }) => { + mouse::Interaction::Grabbing + } + None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), } } } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 2a3efd4a..aabe6b21 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -11,8 +11,8 @@ 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, - Vector, Widget, + layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, + Widget, }; use iced_wgpu::{ triangle::{Mesh2D, Vertex2D}, @@ -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 @@ -141,7 +141,7 @@ mod rainbow { }, }), }, - MouseCursor::default(), + mouse::Interaction::default(), ) } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index da571ed1..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::default()); + 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, + ), + ); } _ => {} } -- cgit From 005ad6215aa9e2a8f159454c8006b5dc6aeb3635 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 30 Apr 2020 08:59:47 +0200 Subject: Update `README` of `game_of_life` example --- examples/game_of_life/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md index ebbb12cc..bb1b9736 100644 --- a/examples/game_of_life/README.md +++ b/examples/game_of_life/README.md @@ -1,18 +1,19 @@ -## Bézier tool +## Game of Life -A Paint-like tool for drawing Bézier curves using the `Canvas` widget. +An interactive version of the Game of Life, invented by John Conway, implemented +on top of a `Canvas` widget and other controls. -The __[`main`]__ file contains all the code of the example. +The __[`main`]__ file contains the relevant code of the example. You can run it with `cargo run`: ``` -cargo run --package bezier_tool +cargo run --package game_of_life ``` [`main`]: src/main.rs -- cgit From ee97887409849395ecfd63e499c5d5372b121aa3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 00:50:40 +0200 Subject: Introduce `Cell` type in `game_of_life` --- examples/game_of_life/Cargo.toml | 2 +- examples/game_of_life/src/main.rs | 182 ++++++++++++++++++++------------------ 2 files changed, 98 insertions(+), 86 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 413493ae..b1054537 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -6,5 +6,5 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../..", features = ["canvas", "tokio"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } itertools = "0.9" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index f0891db1..9f43b56a 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -161,11 +161,9 @@ mod grid { }; use std::collections::{HashMap, HashSet}; - const CELL_SIZE: usize = 20; - #[derive(Default)] pub struct Grid { - alive_cells: HashSet<(isize, isize)>, + life: HashSet, interaction: Option, cache: canvas::Cache, translation: Vector, @@ -173,7 +171,7 @@ mod grid { #[derive(Debug, Clone, Copy)] pub enum Message { - Populate { cell: (isize, isize) }, + Populate(Cell), } enum Interaction { @@ -182,41 +180,26 @@ mod grid { } impl Grid { - fn with_neighbors( - i: isize, - j: isize, - ) -> impl Iterator { - use itertools::Itertools; - - let rows = i.saturating_sub(1)..=i.saturating_add(1); - let columns = j.saturating_sub(1)..=j.saturating_add(1); - - rows.cartesian_product(columns) - } - pub fn tick(&mut self) { use itertools::Itertools; - let populated_neighbors: HashMap<(isize, isize), usize> = self - .alive_cells + let populated_neighbors: HashMap = self + .life .iter() - .flat_map(|&(i, j)| Self::with_neighbors(i, j)) + .flat_map(Cell::cluster) .unique() - .map(|(i, j)| ((i, j), self.populated_neighbors(i, j))) + .map(|cell| (cell, self.count_adjacent_life(cell))) .collect(); - for (&(i, j), amount) in populated_neighbors.iter() { - let is_populated = self.alive_cells.contains(&(i, j)); - + for (cell, amount) in populated_neighbors.iter() { match amount { - 2 | 3 if is_populated => {} + 2 => {} 3 => { - let _ = self.alive_cells.insert((i, j)); + let _ = self.life.insert(*cell); } - _ if is_populated => { - let _ = self.alive_cells.remove(&(i, j)); + _ => { + let _ = self.life.remove(cell); } - _ => {} } } @@ -225,8 +208,8 @@ mod grid { pub fn update(&mut self, message: Message) { match message { - Message::Populate { cell } => { - self.alive_cells.insert(cell); + Message::Populate(cell) => { + self.life.insert(cell); self.cache.clear() } } @@ -239,24 +222,16 @@ mod grid { .into() } - fn populated_neighbors(&self, row: isize, column: isize) -> usize { - let with_neighbors = Self::with_neighbors(row, column); + fn count_adjacent_life(&self, cell: Cell) -> usize { + let cluster = Cell::cluster(&cell); - let is_neighbor = |i: isize, j: isize| i != row || j != column; - let is_populated = - |i: isize, j: isize| self.alive_cells.contains(&(i, j)); + let is_neighbor = |candidate| candidate != cell; + let is_populated = |cell| self.life.contains(&cell); - with_neighbors - .filter(|&(i, j)| is_neighbor(i, j) && is_populated(i, j)) + cluster + .filter(|&cell| is_neighbor(cell) && is_populated(cell)) .count() } - - fn cell_at(&self, position: Point) -> Option<(isize, isize)> { - let i = (position.y / CELL_SIZE as f32).ceil() as isize; - let j = (position.x / CELL_SIZE as f32).ceil() as isize; - - Some((i.saturating_sub(1), j.saturating_sub(1))) - } } impl<'a> canvas::Program for Grid { @@ -271,12 +246,12 @@ mod grid { } let cursor_position = cursor.position_in(&bounds)?; - let cell = self.cell_at(cursor_position - self.translation)?; + let cell = Cell::at(cursor_position - self.translation); - let populate = if self.alive_cells.contains(&cell) { + let populate = if self.life.contains(&cell) { None } else { - Some(Message::Populate { cell }) + Some(Message::Populate(cell)) }; match event { @@ -333,31 +308,24 @@ mod grid { ), ); - let first_row = - (-self.translation.y / CELL_SIZE as f32).floor() as isize; - let first_column = - (-self.translation.x / CELL_SIZE as f32).floor() as isize; - - let visible_rows = - (frame.height() / CELL_SIZE as f32).ceil() as isize; - let visible_columns = - (frame.width() / CELL_SIZE as f32).ceil() as isize; - frame.with_save(|frame| { frame.translate(self.translation); - frame.scale(CELL_SIZE as f32); + frame.scale(Cell::SIZE as f32); let cells = Path::new(|p| { - for i in first_row..=(first_row + visible_rows) { - for j in - first_column..=(first_column + visible_columns) - { - if self.alive_cells.contains(&(i, j)) { - p.rectangle( - Point::new(j as f32, i as f32), - cell_size, - ); - } + let region = Rectangle { + x: -self.translation.x, + y: -self.translation.y, + width: frame.width(), + height: frame.height(), + }; + + for cell in Cell::all_visible_in(region) { + if self.life.contains(&cell) { + p.rectangle( + Point::new(cell.j as f32, cell.i as f32), + cell_size, + ); } } }); @@ -369,25 +337,23 @@ mod grid { let mut frame = Frame::new(bounds.size()); frame.translate(self.translation); - frame.scale(CELL_SIZE as f32); + frame.scale(Cell::SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - if let Some((i, j)) = - self.cell_at(cursor_position - self.translation) - { - let interaction = Path::rectangle( - Point::new(j as f32, i as f32), - cell_size, - ); - - frame.fill( - &interaction, - Color { - a: 0.5, - ..Color::BLACK - }, - ); - } + let cell = Cell::at(cursor_position - self.translation); + + let interaction = Path::rectangle( + Point::new(cell.j as f32, cell.i as f32), + cell_size, + ); + + frame.fill( + &interaction, + Color { + a: 0.5, + ..Color::BLACK + }, + ); } frame.into_geometry() @@ -413,4 +379,50 @@ mod grid { } } } + + #[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 { + 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 all_visible_in(region: Rectangle) -> impl Iterator { + use itertools::Itertools; + + let first_row = (region.y / Cell::SIZE as f32).floor() as isize; + let first_column = (region.x / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (region.height / Cell::SIZE as f32).ceil() as isize; + let visible_columns = + (region.width / Cell::SIZE as f32).ceil() as isize; + + let rows = first_row..=first_row + visible_rows; + let columns = first_column..=first_column + visible_columns; + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + } } -- cgit From 71323c51bbdcb7dcccd6249fcd4ea3b1df589a9b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 00:54:43 +0200 Subject: Simplify `Interaction` handling in `game_of_life` --- examples/game_of_life/src/main.rs | 42 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9f43b56a..8a841c91 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -164,7 +164,7 @@ mod grid { #[derive(Default)] pub struct Grid { life: HashSet, - interaction: Option, + interaction: Interaction, cache: canvas::Cache, translation: Vector, } @@ -174,11 +174,6 @@ mod grid { Populate(Cell), } - enum Interaction { - Drawing, - Panning { translation: Vector, start: Point }, - } - impl Grid { pub fn tick(&mut self) { use itertools::Itertools; @@ -242,7 +237,7 @@ mod grid { cursor: Cursor, ) -> Option { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - self.interaction = None; + self.interaction = Interaction::None; } let cursor_position = cursor.position_in(&bounds)?; @@ -258,15 +253,15 @@ mod grid { Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(button) => match button { mouse::Button::Left => { - self.interaction = Some(Interaction::Drawing); + self.interaction = Interaction::Drawing; populate } mouse::Button::Right => { - self.interaction = Some(Interaction::Panning { + self.interaction = Interaction::Panning { translation: self.translation, start: cursor_position, - }); + }; None } @@ -274,11 +269,8 @@ mod grid { }, mouse::Event::CursorMoved { .. } => { match self.interaction { - Some(Interaction::Drawing) => populate, - Some(Interaction::Panning { - translation, - start, - }) => { + Interaction::Drawing => populate, + Interaction::Panning { translation, start } => { self.translation = translation + (cursor_position - start); @@ -368,11 +360,9 @@ mod grid { cursor: Cursor, ) -> mouse::Interaction { match self.interaction { - Some(Interaction::Drawing) => mouse::Interaction::Crosshair, - Some(Interaction::Panning { .. }) => { - mouse::Interaction::Grabbing - } - None if cursor.is_over(&bounds) => { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { mouse::Interaction::Crosshair } _ => mouse::Interaction::default(), @@ -425,4 +415,16 @@ mod grid { rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) } } + + enum Interaction { + None, + Drawing, + Panning { translation: Vector, start: Point }, + } + + impl Default for Interaction { + fn default() -> Interaction { + Interaction::None + } + } } -- cgit From a6db1e1fb3e512f86be076e70eff92abb11fd457 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 01:08:39 +0200 Subject: Introduce `Life` type in `game_of_life` --- examples/game_of_life/src/main.rs | 88 +++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 35 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8a841c91..b539247b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -163,7 +163,7 @@ mod grid { #[derive(Default)] pub struct Grid { - life: HashSet, + life: Life, interaction: Interaction, cache: canvas::Cache, translation: Vector, @@ -176,35 +176,14 @@ mod grid { impl Grid { pub fn tick(&mut self) { - use itertools::Itertools; - - let populated_neighbors: HashMap = self - .life - .iter() - .flat_map(Cell::cluster) - .unique() - .map(|cell| (cell, self.count_adjacent_life(cell))) - .collect(); - - for (cell, amount) in populated_neighbors.iter() { - match amount { - 2 => {} - 3 => { - let _ = self.life.insert(*cell); - } - _ => { - let _ = self.life.remove(cell); - } - } - } - + self.life.tick(); self.cache.clear() } pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { - self.life.insert(cell); + self.life.populate(cell); self.cache.clear() } } @@ -216,17 +195,6 @@ mod grid { .height(Length::Fill) .into() } - - fn count_adjacent_life(&self, cell: Cell) -> usize { - let cluster = Cell::cluster(&cell); - - let is_neighbor = |candidate| candidate != cell; - let is_populated = |cell| self.life.contains(&cell); - - cluster - .filter(|&cell| is_neighbor(cell) && is_populated(cell)) - .count() - } } impl<'a> canvas::Program for Grid { @@ -370,6 +338,56 @@ mod grid { } } + #[derive(Default)] + pub struct Life { + cells: HashSet, + } + + impl Life { + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn tick(&mut self) { + use itertools::Itertools; + + let populated_neighbors: HashMap = self + .cells + .iter() + .flat_map(Cell::cluster) + .unique() + .map(|cell| (cell, self.count_adjacent(cell))) + .collect(); + + for (cell, amount) in populated_neighbors.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + fn count_adjacent(&self, cell: Cell) -> usize { + let cluster = Cell::cluster(&cell); + + let is_neighbor = |candidate| candidate != cell; + let is_populated = |cell| self.cells.contains(&cell); + + cluster + .filter(|&cell| is_neighbor(cell) && is_populated(cell)) + .count() + } + } + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Cell { i: isize, -- cgit From 377ead93d6d506e7fe1e49d4b8b54c0f1d4c5e14 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 01:24:31 +0200 Subject: Improve tick performance in `game_of_life` --- examples/game_of_life/src/main.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b539247b..3b37dc34 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -353,17 +353,19 @@ mod grid { } fn tick(&mut self) { - use itertools::Itertools; + let mut adjacent_life = HashMap::new(); + + 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); - let populated_neighbors: HashMap = self - .cells - .iter() - .flat_map(Cell::cluster) - .unique() - .map(|cell| (cell, self.count_adjacent(cell))) - .collect(); + *amount += 1; + } + } - for (cell, amount) in populated_neighbors.iter() { + for (cell, amount) in adjacent_life.iter() { match amount { 2 => {} 3 => { @@ -375,17 +377,6 @@ mod grid { } } } - - fn count_adjacent(&self, cell: Cell) -> usize { - let cluster = Cell::cluster(&cell); - - let is_neighbor = |candidate| candidate != cell; - let is_populated = |cell| self.cells.contains(&cell); - - cluster - .filter(|&cell| is_neighbor(cell) && is_populated(cell)) - .count() - } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -407,7 +398,7 @@ mod grid { } } - fn cluster(cell: &Cell) -> impl Iterator { + fn cluster(cell: Cell) -> impl Iterator { use itertools::Itertools; let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); @@ -416,6 +407,10 @@ mod grid { rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) } + fn neighbors(cell: Cell) -> impl Iterator { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + fn all_visible_in(region: Rectangle) -> impl Iterator { use itertools::Itertools; -- cgit From 404122e0b17300aa46cdb5ec5f0366f24b8ea931 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:35:59 +0200 Subject: Implement zooming for `game_of_life` example --- examples/game_of_life/src/main.rs | 157 +++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 60 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 3b37dc34..d9ff5a14 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -156,17 +156,17 @@ impl Application for GameOfLife { mod grid { use iced::{ - canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path}, + canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use std::collections::{HashMap, HashSet}; - #[derive(Default)] pub struct Grid { life: Life, interaction: Interaction, - cache: canvas::Cache, + cache: Cache, translation: Vector, + scaling: f32, } #[derive(Debug, Clone, Copy)] @@ -174,6 +174,18 @@ mod grid { Populate(Cell), } + impl Default for Grid { + fn default() -> Self { + Self { + life: Life::default(), + interaction: Interaction::default(), + cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + } + } + } + impl Grid { pub fn tick(&mut self) { self.life.tick(); @@ -195,6 +207,27 @@ mod grid { .height(Length::Fill) .into() } + + pub fn visible_region(&self, size: Size) -> Rectangle { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Rectangle { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + pub 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 for Grid { @@ -209,7 +242,7 @@ mod grid { } let cursor_position = cursor.position_in(&bounds)?; - let cell = Cell::at(cursor_position - self.translation); + let cell = Cell::at(self.project(cursor_position, bounds.size())); let populate = if self.life.contains(&cell) { None @@ -239,8 +272,9 @@ mod grid { match self.interaction { Interaction::Drawing => populate, Interaction::Panning { translation, start } => { - self.translation = - translation + (cursor_position - start); + self.translation = translation + + (cursor_position - start) + * (1.0 / self.scaling); self.cache.clear(); @@ -249,62 +283,65 @@ mod grid { _ => None, } } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y > 0.0 && self.scaling < 2.0 + || y < 0.0 && self.scaling > 0.25 + { + self.scaling += y / 30.0; + + self.cache.clear(); + } + + None + } + }, _ => None, }, } } fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { - let cell_size = Size::new(1.0, 1.0); + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); let life = self.cache.draw(bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill( - &background, - Color::from_rgb( - 0x40 as f32 / 255.0, - 0x44 as f32 / 255.0, - 0x4B as f32 / 255.0, - ), - ); + 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 cells = Path::new(|p| { - let region = Rectangle { - x: -self.translation.x, - y: -self.translation.y, - width: frame.width(), - height: frame.height(), - }; - - for cell in Cell::all_visible_in(region) { - if self.life.contains(&cell) { - p.rectangle( - Point::new(cell.j as f32, cell.i as f32), - cell_size, - ); - } - } - }); - frame.fill(&cells, Color::WHITE); + let region = self.visible_region(frame.size()); + + for cell in self.life.visible_in(region) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } }); }); let hovered_cell = { let mut frame = Frame::new(bounds.size()); + frame.translate(center); + frame.scale(self.scaling); frame.translate(self.translation); frame.scale(Cell::SIZE as f32); if let Some(cursor_position) = cursor.position_in(&bounds) { - let cell = Cell::at(cursor_position - self.translation); + let cell = + Cell::at(self.project(cursor_position, frame.size())); let interaction = Path::rectangle( Point::new(cell.j as f32, cell.i as f32), - cell_size, + Size::UNIT, ); frame.fill( @@ -344,14 +381,6 @@ mod grid { } impl Life { - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - fn tick(&mut self) { let mut adjacent_life = HashMap::new(); @@ -377,6 +406,31 @@ mod grid { } } } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn visible_in(&self, region: Rectangle) -> impl Iterator { + let first_row = (region.y / Cell::SIZE as f32).floor() as isize; + let first_column = (region.x / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (region.height / Cell::SIZE as f32).ceil() as isize; + let visible_columns = + (region.width / Cell::SIZE as f32).ceil() as isize; + + let rows = first_row..=first_row + visible_rows; + let columns = first_column..=first_column + visible_columns; + + self.cells.iter().filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -410,23 +464,6 @@ mod grid { fn neighbors(cell: Cell) -> impl Iterator { Cell::cluster(cell).filter(move |candidate| *candidate != cell) } - - fn all_visible_in(region: Rectangle) -> impl Iterator { - use itertools::Itertools; - - let first_row = (region.y / Cell::SIZE as f32).floor() as isize; - let first_column = (region.x / Cell::SIZE as f32).floor() as isize; - - let visible_rows = - (region.height / Cell::SIZE as f32).ceil() as isize; - let visible_columns = - (region.width / Cell::SIZE as f32).ceil() as isize; - - let rows = first_row..=first_row + visible_rows; - let columns = first_column..=first_column + visible_columns; - - rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) - } } enum Interaction { -- cgit From f9227546ca975bf3bf7f293d10e79004935b8645 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:41:04 +0200 Subject: Use `fill_rectangle` for cursor in `game_of_life` --- examples/game_of_life/src/main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index d9ff5a14..ef040263 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -339,13 +339,9 @@ mod grid { let cell = Cell::at(self.project(cursor_position, frame.size())); - let interaction = Path::rectangle( + frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, - ); - - frame.fill( - &interaction, Color { a: 0.5, ..Color::BLACK -- cgit From c23995ecb4ddc0c9cc33b0d50404a478b8b5e659 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 04:48:26 +0200 Subject: Increase speed limit to `200` in `game_of_life` --- examples/game_of_life/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ef040263..8a140ed4 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -124,7 +124,7 @@ impl Application for GameOfLife { .push( Slider::new( &mut self.speed_slider, - 1.0..=20.0, + 1.0..=200.0, selected_speed as f32, Message::SpeedChanged, ) -- cgit From 0a5f1bb676f89a26711a8885935ffe94a370c261 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 05:19:05 +0200 Subject: Improve zooming logic in `game_of_life` --- examples/game_of_life/src/main.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 8a140ed4..92500309 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -178,7 +178,7 @@ mod grid { fn default() -> Self { Self { life: Life::default(), - interaction: Interaction::default(), + interaction: Interaction::None, cache: Cache::default(), translation: Vector::default(), scaling: 1.0, @@ -187,6 +187,9 @@ mod grid { } impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + pub fn tick(&mut self) { self.life.tick(); self.cache.clear() @@ -286,10 +289,12 @@ mod grid { mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - if y > 0.0 && self.scaling < 2.0 - || y < 0.0 && self.scaling > 0.25 + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING { - self.scaling += y / 30.0; + self.scaling = (self.scaling + y / 30.0) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); self.cache.clear(); } @@ -467,10 +472,4 @@ mod grid { Drawing, Panning { translation: Vector, start: Point }, } - - impl Default for Interaction { - fn default() -> Interaction { - Interaction::None - } - } } -- cgit From ffbe59f8129c80afb3e86eae67efaa5370fbfa8e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 05:42:07 +0200 Subject: Zoom to cursor in `game_of_life` example --- examples/game_of_life/src/main.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 92500309..c818d99f 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -292,7 +292,21 @@ mod grid { if y < 0.0 && self.scaling > Self::MIN_SCALING || y > 0.0 && self.scaling < Self::MAX_SCALING { - self.scaling = (self.scaling + y / 30.0) + let factor = y / 30.0; + + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + self.translation = self.translation + - Vector::new( + cursor_to_center.x * factor + / (self.scaling * self.scaling), + cursor_to_center.y * factor + / (self.scaling * self.scaling), + ); + } + + self.scaling = (self.scaling + factor) .max(Self::MIN_SCALING) .min(Self::MAX_SCALING); -- cgit From 1833c77312ede2e5d47b62df0eea771f6fa559e9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 06:23:05 +0200 Subject: Improve scrolling smoothness in `game_of_life` --- examples/game_of_life/src/main.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index c818d99f..44ab4da6 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -292,24 +292,27 @@ mod grid { if y < 0.0 && self.scaling > Self::MIN_SCALING || y > 0.0 && self.scaling < Self::MAX_SCALING { - let factor = y / 30.0; + 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 - / (self.scaling * self.scaling), + / (old_scaling * old_scaling), cursor_to_center.y * factor - / (self.scaling * self.scaling), + / (old_scaling * old_scaling), ); } - self.scaling = (self.scaling + factor) - .max(Self::MIN_SCALING) - .min(Self::MAX_SCALING); - self.cache.clear(); } -- cgit From e7e8e76c28e5bc8eac0c98d6d72c7e49d65468fc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 1 May 2020 06:23:30 +0200 Subject: Change speed limit to `100` in `game_of_life` --- examples/game_of_life/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 44ab4da6..17b4090d 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -124,7 +124,7 @@ impl Application for GameOfLife { .push( Slider::new( &mut self.speed_slider, - 1.0..=200.0, + 1.0..=100.0, selected_speed as f32, Message::SpeedChanged, ) -- cgit From 4fd8e47737e82817d652d86b306400da663f7a98 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 03:31:31 +0200 Subject: Use `rustc_hash` for hashing in `game_of_life` This seems to produce a 2x speedup. --- examples/game_of_life/Cargo.toml | 1 + examples/game_of_life/src/main.rs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index b1054537..2b945c4c 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -8,3 +8,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["canvas", "tokio", "debug"] } itertools = "0.9" +rustc-hash = "1.1" diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 17b4090d..fb4b5b75 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -159,7 +159,7 @@ mod grid { canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; - use std::collections::{HashMap, HashSet}; + use rustc_hash::{FxHashMap, FxHashSet}; pub struct Grid { life: Life, @@ -395,12 +395,12 @@ mod grid { #[derive(Default)] pub struct Life { - cells: HashSet, + cells: FxHashSet, } impl Life { fn tick(&mut self) { - let mut adjacent_life = HashMap::new(); + let mut adjacent_life = FxHashMap::default(); for cell in &self.cells { let _ = adjacent_life.entry(*cell).or_insert(0); -- cgit From 8fa9e4c94eb9d6b6e13b45fd6a99209536880a2d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 03:37:20 +0200 Subject: Rename `visible_in` to `within` in `game_of_life` --- examples/game_of_life/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index fb4b5b75..88fd3a93 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -339,7 +339,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in self.life.visible_in(region) { + for cell in self.life.within(region) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -433,7 +433,7 @@ mod grid { self.cells.insert(cell); } - fn visible_in(&self, region: Rectangle) -> impl Iterator { + fn within(&self, region: Rectangle) -> impl Iterator { let first_row = (region.y / Cell::SIZE as f32).floor() as isize; let first_column = (region.x / Cell::SIZE as f32).floor() as isize; -- cgit From 916a1bfc7049867669b81f446e711021d92a4132 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 07:01:27 +0200 Subject: Run ticks in a background thread in `game_of_life` --- examples/game_of_life/Cargo.toml | 1 + examples/game_of_life/src/main.rs | 260 ++++++++++++++++++++++++++++++-------- 2 files changed, 210 insertions(+), 51 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 2b945c4c..b9bb7f2a 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -7,5 +7,6 @@ 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/src/main.rs b/examples/game_of_life/src/main.rs index 88fd3a93..b8cabf24 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,7 +19,7 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, - is_playing: bool, + state: State, speed: u64, next_speed: Option, toggle_button: button::State, @@ -28,6 +28,17 @@ struct GameOfLife { speed_slider: slider::State, } +enum State { + Paused, + Playing { last_tick: Instant }, +} + +impl Default for State { + fn default() -> State { + State::Paused + } +} + #[derive(Debug, Clone)] enum Message { Grid(grid::Message), @@ -62,37 +73,61 @@ impl Application for GameOfLife { Message::Grid(message) => { self.grid.update(message); } - Message::Tick(_) | Message::Next => { - self.grid.tick(); + Message::Tick(_) | Message::Next => match &mut self.state { + State::Paused => { + if let Some(task) = self.grid.tick(1) { + return Command::perform(task, Message::Grid); + } + } + State::Playing { last_tick } => { + let seconds_elapsed = + last_tick.elapsed().as_millis() as f32 / 1000.0; + + let needed_ticks = + (self.speed as f32 * seconds_elapsed).ceil() as usize; + + if let Some(task) = self.grid.tick(needed_ticks) { + *last_tick = Instant::now(); - if let Some(speed) = self.next_speed.take() { - self.speed = speed; + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + return Command::perform(task, Message::Grid); + } } - } + }, Message::Toggle => { - self.is_playing = !self.is_playing; + self.state = match self.state { + State::Paused => State::Playing { + last_tick: Instant::now(), + }, + State::Playing { .. } => State::Paused, + }; } Message::Clear => { - self.grid = Grid::default(); + self.grid.clear(); } - Message::SpeedChanged(speed) => { - if self.is_playing { - self.next_speed = Some(speed.round() as u64); - } else { + Message::SpeedChanged(speed) => match self.state { + State::Paused => { self.speed = speed.round() as u64; } - } + State::Playing { .. } => { + self.next_speed = Some(speed.round() as u64); + } + }, } Command::none() } fn subscription(&self) -> Subscription { - if self.is_playing { - time::every(Duration::from_millis(1000 / self.speed)) - .map(Message::Tick) - } else { - Subscription::none() + match self.state { + State::Paused => Subscription::none(), + State::Playing { .. } => { + time::every(Duration::from_millis(1000 / self.speed)) + .map(Message::Tick) + } } } @@ -102,7 +137,11 @@ impl Application for GameOfLife { .push( Button::new( &mut self.toggle_button, - Text::new(if self.is_playing { "Pause" } else { "Play" }), + Text::new(if let State::Paused = self.state { + "Play" + } else { + "Pause" + }), ) .on_press(Message::Toggle) .style(style::Button), @@ -111,11 +150,6 @@ impl Application for GameOfLife { Button::new(&mut self.next_button, Text::new("Next")) .on_press(Message::Next) .style(style::Button), - ) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Button), ); let selected_speed = self.next_speed.unwrap_or(self.speed); @@ -138,10 +172,14 @@ impl Application for GameOfLife { .padding(10) .spacing(20) .push(playback_controls) - .push(speed_controls); + .push(speed_controls) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Button), + ); let content = Column::new() - .spacing(10) .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -160,28 +198,40 @@ mod grid { mouse, Color, Element, Length, Point, Rectangle, Size, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; pub struct Grid { - life: Life, + state: State, interaction: Interaction, cache: Cache, translation: Vector, scaling: f32, + version: usize, } - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone)] pub enum Message { Populate(Cell), + Ticked { + result: Result, + version: usize, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, } impl Default for Grid { fn default() -> Self { Self { - life: Life::default(), + state: State::default(), interaction: Interaction::None, cache: Cache::default(), translation: Vector::default(), scaling: 1.0, + version: 0, } } } @@ -190,17 +240,44 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; - pub fn tick(&mut self) { - self.life.tick(); - self.cache.clear() + pub fn tick( + &mut self, + amount: usize, + ) -> Option> { + use iced::futures::FutureExt; + + let version = self.version; + let tick = self.state.tick(amount)?; + + Some(tick.map(move |result| Message::Ticked { result, version })) + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.cache.clear(); } pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { - self.life.populate(cell); + self.state.populate(cell); + self.cache.clear() + } + Message::Ticked { + result: Ok(life), + version, + } if version == self.version => { + self.state.update(life); self.cache.clear() } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + Message::Ticked { .. } => {} } } @@ -211,11 +288,11 @@ mod grid { .into() } - pub fn visible_region(&self, size: Size) -> Rectangle { + pub fn visible_region(&self, size: Size) -> Region { let width = size.width / self.scaling; let height = size.height / self.scaling; - Rectangle { + Region { x: -self.translation.x - width / 2.0, y: -self.translation.y - height / 2.0, width, @@ -247,7 +324,7 @@ mod grid { let cursor_position = cursor.position_in(&bounds)?; let cell = Cell::at(self.project(cursor_position, bounds.size())); - let populate = if self.life.contains(&cell) { + let populate = if self.state.contains(&cell) { None } else { Some(Message::Populate(cell)) @@ -339,7 +416,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in self.life.within(region) { + for cell in region.view(self.state.cells()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -394,6 +471,63 @@ mod grid { } #[derive(Default)] + struct State { + life: Life, + births: FxHashSet, + is_ticking: bool, + } + + impl State { + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator { + 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 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>> { + 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, } @@ -433,21 +567,16 @@ mod grid { self.cells.insert(cell); } - fn within(&self, region: Rectangle) -> impl Iterator { - let first_row = (region.y / Cell::SIZE as f32).floor() as isize; - let first_column = (region.x / Cell::SIZE as f32).floor() as isize; - - let visible_rows = - (region.height / Cell::SIZE as f32).ceil() as isize; - let visible_columns = - (region.width / Cell::SIZE as f32).ceil() as isize; - - let rows = first_row..=first_row + visible_rows; - let columns = first_column..=first_column + visible_columns; + pub fn iter(&self) -> impl Iterator { + self.cells.iter() + } + } - self.cells.iter().filter(move |cell| { - rows.contains(&cell.i) && columns.contains(&cell.j) - }) + 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() } } @@ -484,6 +613,35 @@ mod grid { } } + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn view<'a>( + &self, + cells: impl Iterator, + ) -> impl Iterator { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + let rows = first_row..=first_row + visible_rows; + let columns = first_column..=first_column + visible_columns; + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + enum Interaction { None, Drawing, -- cgit From 0025b8c3f8f029d6fb7b8b5a599cc6450248aad6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 09:27:49 +0200 Subject: Display some statistics in `game_of_life` --- examples/game_of_life/src/main.rs | 104 +++++++++++++++++++++++++++++-------- examples/game_of_life/src/style.rs | 38 ++++++++++++++ 2 files changed, 119 insertions(+), 23 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b8cabf24..b77d06ea 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -26,6 +26,8 @@ struct GameOfLife { next_button: button::State, clear_button: button::State, speed_slider: slider::State, + tick_duration: Duration, + tick_amount: usize, } enum State { @@ -71,7 +73,12 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - self.grid.update(message); + if let Some((tick_duration, tick_amount)) = + self.grid.update(message) + { + self.tick_duration = tick_duration; + self.tick_amount = tick_amount; + } } Message::Tick(_) | Message::Next => match &mut self.state { State::Paused => { @@ -86,7 +93,7 @@ impl Application for GameOfLife { let needed_ticks = (self.speed as f32 * seconds_elapsed).ceil() as usize; - if let Some(task) = self.grid.tick(needed_ticks) { + if let Some(task) = self.grid.tick(needed_ticks.max(1)) { *last_tick = Instant::now(); if let Some(speed) = self.next_speed.take() { @@ -154,33 +161,49 @@ impl Application for GameOfLife { let selected_speed = self.next_speed.unwrap_or(self.speed); let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) .spacing(10) .push( Slider::new( &mut self.speed_slider, - 1.0..=100.0, + 1.0..=1000.0, selected_speed as f32, Message::SpeedChanged, ) - .width(Length::Units(200)) .style(style::Slider), ) - .push(Text::new(format!("x{}", selected_speed)).size(16)) - .align_items(Align::Center); + .push(Text::new(format!("x{}", selected_speed)).size(16)); + + let stats = Column::new() + .width(Length::Units(150)) + .align_items(Align::Center) + .spacing(2) + .push( + Text::new(format!("{} cells", self.grid.cell_count())).size(14), + ) + .push( + Text::new(format!( + "{:?} ({})", + self.tick_duration, self.tick_amount + )) + .size(14), + ); let controls = Row::new() .padding(10) .spacing(20) + .align_items(Align::Center) .push(playback_controls) .push(speed_controls) + .push(stats) .push( Button::new(&mut self.clear_button, Text::new("Clear")) .on_press(Message::Clear) - .style(style::Button), + .style(style::Clear), ); let content = Column::new() - .align_items(Align::Center) .push(self.grid.view().map(Message::Grid)) .push(controls); @@ -199,6 +222,7 @@ mod grid { }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; + use std::time::{Duration, Instant}; pub struct Grid { state: State, @@ -214,6 +238,8 @@ mod grid { Populate(Cell), Ticked { result: Result, + tick_duration: Duration, + tick_amount: usize, version: usize, }, } @@ -240,16 +266,29 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; + pub fn cell_count(&self) -> usize { + self.state.cell_count() + } + pub fn tick( &mut self, amount: usize, ) -> Option> { - use iced::futures::FutureExt; - let version = self.version; let tick = self.state.tick(amount)?; - Some(tick.map(move |result| Message::Ticked { result, version })) + 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, + tick_amount: amount, + } + }) } pub fn clear(&mut self) { @@ -259,25 +298,36 @@ mod grid { self.cache.clear(); } - pub fn update(&mut self, message: Message) { + pub fn update( + &mut self, + message: Message, + ) -> Option<(Duration, usize)> { match message { Message::Populate(cell) => { self.state.populate(cell); - self.cache.clear() + self.cache.clear(); + + None } Message::Ticked { result: Ok(life), version, + tick_duration, + tick_amount, } if version == self.version => { self.state.update(life); - self.cache.clear() + self.cache.clear(); + + Some((tick_duration, tick_amount)) } Message::Ticked { result: Err(error), .. } => { dbg!(error); + + None } - Message::Ticked { .. } => {} + Message::Ticked { .. } => None, } } @@ -478,6 +528,10 @@ mod grid { } 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) } @@ -533,6 +587,18 @@ mod grid { } 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 tick(&mut self) { let mut adjacent_life = FxHashMap::default(); @@ -559,14 +625,6 @@ mod grid { } } - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - pub fn iter(&self) -> impl Iterator { self.cells.iter() } diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs index 0becb5be..d59569f2 100644 --- a/examples/game_of_life/src/style.rs +++ b/examples/game_of_life/src/style.rs @@ -6,6 +6,12 @@ const ACTIVE: Color = Color::from_rgb( 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, @@ -55,6 +61,38 @@ impl button::StyleSheet for Button { } } +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 { -- cgit From cc8f5b6fc82e253466f7fab3a9285b0b7531f189 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 2 May 2020 10:48:42 +0200 Subject: Simplify logic and limit ticks in `game_of_life` --- examples/game_of_life/src/main.rs | 106 ++++++++++++-------------------------- 1 file changed, 33 insertions(+), 73 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index b77d06ea..1b9bad44 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,26 +19,16 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, - state: State, - speed: u64, - next_speed: Option, + is_playing: bool, + speed: usize, + next_speed: Option, toggle_button: button::State, next_button: button::State, clear_button: button::State, speed_slider: slider::State, tick_duration: Duration, - tick_amount: usize, -} - -enum State { - Paused, - Playing { last_tick: Instant }, -} - -impl Default for State { - fn default() -> State { - State::Paused - } + queued_ticks: usize, + last_ticks: usize, } #[derive(Debug, Clone)] @@ -73,68 +63,48 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - if let Some((tick_duration, tick_amount)) = - self.grid.update(message) - { + if let Some(tick_duration) = self.grid.update(message) { self.tick_duration = tick_duration; - self.tick_amount = tick_amount; } } - Message::Tick(_) | Message::Next => match &mut self.state { - State::Paused => { - if let Some(task) = self.grid.tick(1) { - return Command::perform(task, Message::Grid); - } - } - State::Playing { last_tick } => { - let seconds_elapsed = - last_tick.elapsed().as_millis() as f32 / 1000.0; - - let needed_ticks = - (self.speed as f32 * seconds_elapsed).ceil() as usize; - - if let Some(task) = self.grid.tick(needed_ticks.max(1)) { - *last_tick = Instant::now(); - - if let Some(speed) = self.next_speed.take() { - self.speed = speed; - } + Message::Tick(_) | Message::Next => { + if let Some(task) = self.grid.tick(self.queued_ticks + 1) { + self.last_ticks = self.queued_ticks; + self.queued_ticks = 0; - return Command::perform(task, Message::Grid); + if let Some(speed) = self.next_speed.take() { + self.speed = speed; } + + return Command::perform(task, Message::Grid); + } else { + self.queued_ticks = (self.queued_ticks + 1).min(self.speed); } - }, + } Message::Toggle => { - self.state = match self.state { - State::Paused => State::Playing { - last_tick: Instant::now(), - }, - State::Playing { .. } => State::Paused, - }; + self.is_playing = !self.is_playing; } Message::Clear => { self.grid.clear(); } - Message::SpeedChanged(speed) => match self.state { - State::Paused => { - self.speed = speed.round() as u64; + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; } - State::Playing { .. } => { - self.next_speed = Some(speed.round() as u64); - } - }, + } } Command::none() } fn subscription(&self) -> Subscription { - match self.state { - State::Paused => Subscription::none(), - State::Playing { .. } => { - time::every(Duration::from_millis(1000 / self.speed)) - .map(Message::Tick) - } + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() } } @@ -144,11 +114,7 @@ impl Application for GameOfLife { .push( Button::new( &mut self.toggle_button, - Text::new(if let State::Paused = self.state { - "Play" - } else { - "Pause" - }), + Text::new(if self.is_playing { "Pause" } else { "Play" }), ) .on_press(Message::Toggle) .style(style::Button), @@ -185,7 +151,7 @@ impl Application for GameOfLife { .push( Text::new(format!( "{:?} ({})", - self.tick_duration, self.tick_amount + self.tick_duration, self.last_ticks )) .size(14), ); @@ -239,7 +205,6 @@ mod grid { Ticked { result: Result, tick_duration: Duration, - tick_amount: usize, version: usize, }, } @@ -286,7 +251,6 @@ mod grid { result, version, tick_duration, - tick_amount: amount, } }) } @@ -298,10 +262,7 @@ mod grid { self.cache.clear(); } - pub fn update( - &mut self, - message: Message, - ) -> Option<(Duration, usize)> { + pub fn update(&mut self, message: Message) -> Option { match message { Message::Populate(cell) => { self.state.populate(cell); @@ -313,12 +274,11 @@ mod grid { result: Ok(life), version, tick_duration, - tick_amount, } if version == self.version => { self.state.update(life); self.cache.clear(); - Some((tick_duration, tick_amount)) + Some(tick_duration) } Message::Ticked { result: Err(error), .. -- cgit From a43fb42428cbcef3d80e0ec21ec92c6db506353d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 00:08:41 +0200 Subject: Reorganize `view` code in `game_of_life` --- examples/game_of_life/src/main.rs | 173 ++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 71 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 1b9bad44..0e66c237 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -19,16 +19,12 @@ pub fn main() { #[derive(Default)] struct GameOfLife { grid: Grid, + controls: Controls, + statistics: Statistics, is_playing: bool, + queued_ticks: usize, speed: usize, next_speed: Option, - toggle_button: button::State, - next_button: button::State, - clear_button: button::State, - speed_slider: slider::State, - tick_duration: Duration, - queued_ticks: usize, - last_ticks: usize, } #[derive(Debug, Clone)] @@ -64,21 +60,21 @@ impl Application for GameOfLife { match message { Message::Grid(message) => { if let Some(tick_duration) = self.grid.update(message) { - self.tick_duration = tick_duration; + self.statistics.tick_duration = tick_duration; } } Message::Tick(_) | Message::Next => { - if let Some(task) = self.grid.tick(self.queued_ticks + 1) { - self.last_ticks = self.queued_ticks; - self.queued_ticks = 0; + 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.statistics.last_queued_ticks = self.queued_ticks; + self.queued_ticks = 0; + return Command::perform(task, Message::Grid); - } else { - self.queued_ticks = (self.queued_ticks + 1).min(self.speed); } } Message::Toggle => { @@ -109,65 +105,13 @@ impl Application for GameOfLife { } fn view(&mut self) -> Element { - let playback_controls = Row::new() - .spacing(10) - .push( - Button::new( - &mut self.toggle_button, - Text::new(if self.is_playing { "Pause" } else { "Play" }), - ) - .on_press(Message::Toggle) - .style(style::Button), - ) - .push( - Button::new(&mut self.next_button, Text::new("Next")) - .on_press(Message::Next) - .style(style::Button), - ); - let selected_speed = self.next_speed.unwrap_or(self.speed); - 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, - selected_speed as f32, - Message::SpeedChanged, - ) - .style(style::Slider), - ) - .push(Text::new(format!("x{}", selected_speed)).size(16)); - - let stats = Column::new() - .width(Length::Units(150)) - .align_items(Align::Center) - .spacing(2) - .push( - Text::new(format!("{} cells", self.grid.cell_count())).size(14), - ) - .push( - Text::new(format!( - "{:?} ({})", - self.tick_duration, self.last_ticks - )) - .size(14), - ); - - let controls = Row::new() - .padding(10) - .spacing(20) - .align_items(Align::Center) - .push(playback_controls) - .push(speed_controls) - .push(stats) - .push( - Button::new(&mut self.clear_button, Text::new("Clear")) - .on_press(Message::Clear) - .style(style::Clear), - ); + let controls = self.controls.view( + &self.grid, + &self.statistics, + self.is_playing, + selected_speed, + ); let content = Column::new() .push(self.grid.view().map(Message::Grid)) @@ -666,3 +610,90 @@ mod grid { 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, + grid: &Grid, + statistics: &'a Statistics, + is_playing: 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::Toggle) + .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(statistics.view(grid)) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Clear), + ) + .into() + } +} + +#[derive(Default)] +struct Statistics { + tick_duration: Duration, + last_queued_ticks: usize, +} + +impl Statistics { + fn view(&self, grid: &Grid) -> Element { + Column::new() + .width(Length::Units(150)) + .align_items(Align::Center) + .spacing(2) + .push(Text::new(format!("{} cells", grid.cell_count())).size(14)) + .push( + Text::new(format!( + "{:?} ({})", + self.tick_duration, self.last_queued_ticks + )) + .size(14), + ) + .into() + } +} -- cgit From c3c5161386cb527bf6d0fe34e5f4103392733599 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 00:57:15 +0200 Subject: Draw grid in `game_of_life` --- examples/game_of_life/src/main.rs | 91 +++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 18 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 0e66c237..52b04696 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -13,7 +13,10 @@ use iced::{ use std::time::{Duration, Instant}; pub fn main() { - GameOfLife::run(Settings::default()) + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) } #[derive(Default)] @@ -132,12 +135,14 @@ mod grid { }; 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, - cache: Cache, + life_cache: Cache, + grid_cache: Cache, translation: Vector, scaling: f32, version: usize, @@ -163,7 +168,8 @@ mod grid { Self { state: State::default(), interaction: Interaction::None, - cache: Cache::default(), + life_cache: Cache::default(), + grid_cache: Cache::default(), translation: Vector::default(), scaling: 1.0, version: 0, @@ -203,14 +209,14 @@ mod grid { self.state = State::default(); self.version += 1; - self.cache.clear(); + self.life_cache.clear(); } pub fn update(&mut self, message: Message) -> Option { match message { Message::Populate(cell) => { self.state.populate(cell); - self.cache.clear(); + self.life_cache.clear(); None } @@ -220,7 +226,7 @@ mod grid { tick_duration, } if version == self.version => { self.state.update(life); - self.cache.clear(); + self.life_cache.clear(); Some(tick_duration) } @@ -310,7 +316,8 @@ mod grid { + (cursor_position - start) * (1.0 / self.scaling); - self.cache.clear(); + self.life_cache.clear(); + self.grid_cache.clear(); None } @@ -344,7 +351,8 @@ mod grid { ); } - self.cache.clear(); + self.life_cache.clear(); + self.grid_cache.clear(); } None @@ -358,7 +366,7 @@ mod grid { fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - let life = self.cache.draw(bounds.size(), |frame| { + 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)); @@ -370,7 +378,7 @@ mod grid { let region = self.visible_region(frame.size()); - for cell in region.view(self.state.cells()) { + for cell in region.cull(self.state.cells()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, @@ -405,7 +413,44 @@ mod grid { frame.into_geometry() }; - vec![life, hovered_cell] + if self.scaling < 0.2 { + vec![life, hovered_cell] + } 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, hovered_cell] + } } fn mouse_interaction( @@ -583,20 +628,30 @@ mod grid { } impl Region { - fn view<'a>( - &self, - cells: impl Iterator, - ) -> impl Iterator { + fn rows(&self) -> RangeInclusive { let first_row = (self.y / Cell::SIZE as f32).floor() as isize; - let first_column = (self.x / 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 { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + let visible_columns = (self.width / Cell::SIZE as f32).ceil() as isize; - let rows = first_row..=first_row + visible_rows; - let columns = first_column..=first_column + visible_columns; + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator, + ) -> impl Iterator { + let rows = self.rows(); + let columns = self.columns(); cells.filter(move |cell| { rows.contains(&cell.i) && columns.contains(&cell.j) -- cgit From 5aaaea7c8824fb65bac35307cdf760c57f2bf5df Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 01:53:45 +0200 Subject: Render stats as an overlay in `game_of_life` Also allow toggling the grid lines --- examples/game_of_life/src/main.rs | 185 +++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 82 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 52b04696..018ebc50 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -7,8 +7,8 @@ use iced::{ button::{self, Button}, executor, slider::{self, Slider}, - time, Align, Application, Column, Command, Container, Element, Length, Row, - Settings, Subscription, Text, + time, Align, Application, Checkbox, Column, Command, Container, Element, + Length, Row, Settings, Subscription, Text, }; use std::time::{Duration, Instant}; @@ -23,7 +23,6 @@ pub fn main() { struct GameOfLife { grid: Grid, controls: Controls, - statistics: Statistics, is_playing: bool, queued_ticks: usize, speed: usize, @@ -34,7 +33,8 @@ struct GameOfLife { enum Message { Grid(grid::Message), Tick(Instant), - Toggle, + TogglePlayback, + ToggleGrid(bool), Next, Clear, SpeedChanged(f32), @@ -62,9 +62,7 @@ impl Application for GameOfLife { fn update(&mut self, message: Message) -> Command { match message { Message::Grid(message) => { - if let Some(tick_duration) = self.grid.update(message) { - self.statistics.tick_duration = tick_duration; - } + self.grid.update(message); } Message::Tick(_) | Message::Next => { self.queued_ticks = (self.queued_ticks + 1).min(self.speed); @@ -74,15 +72,17 @@ impl Application for GameOfLife { self.speed = speed; } - self.statistics.last_queued_ticks = self.queued_ticks; self.queued_ticks = 0; return Command::perform(task, Message::Grid); } } - Message::Toggle => { + 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(); } @@ -110,9 +110,8 @@ impl Application for GameOfLife { fn view(&mut self) -> Element { let selected_speed = self.next_speed.unwrap_or(self.speed); let controls = self.controls.view( - &self.grid, - &self.statistics, self.is_playing, + self.grid.are_lines_visible(), selected_speed, ); @@ -130,8 +129,11 @@ impl Application for GameOfLife { mod grid { use iced::{ - canvas::{self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path}, - mouse, Color, Element, Length, Point, Rectangle, Size, Vector, + 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; @@ -145,6 +147,9 @@ mod grid { grid_cache: Cache, translation: Vector, scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, version: usize, } @@ -172,6 +177,9 @@ mod grid { grid_cache: Cache::default(), translation: Vector::default(), scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, version: 0, } } @@ -181,10 +189,6 @@ mod grid { const MIN_SCALING: f32 = 0.1; const MAX_SCALING: f32 = 2.0; - pub fn cell_count(&self) -> usize { - self.state.cell_count() - } - pub fn tick( &mut self, amount: usize, @@ -192,6 +196,8 @@ mod grid { 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; @@ -205,20 +211,11 @@ mod grid { }) } - pub fn clear(&mut self) { - self.state = State::default(); - self.version += 1; - - self.life_cache.clear(); - } - - pub fn update(&mut self, message: Message) -> Option { + pub fn update(&mut self, message: Message) { match message { Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); - - None } Message::Ticked { result: Ok(life), @@ -228,16 +225,14 @@ mod grid { self.state.update(life); self.life_cache.clear(); - Some(tick_duration) + self.last_tick_duration = tick_duration; } Message::Ticked { result: Err(error), .. } => { dbg!(error); - - None } - Message::Ticked { .. } => None, + Message::Ticked { .. } => {} } } @@ -248,7 +243,22 @@ mod grid { .into() } - pub fn visible_region(&self, size: Size) -> Region { + 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; @@ -260,7 +270,7 @@ mod grid { } } - pub fn project(&self, position: Point, size: Size) -> Point { + fn project(&self, position: Point, size: Size) -> Point { let region = self.visible_region(size); Point::new( @@ -388,33 +398,64 @@ mod grid { }); }); - let hovered_cell = { + let overlay = { let mut frame = Frame::new(bounds.size()); - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - if let Some(cursor_position) = cursor.position_in(&bounds) { - let cell = - Cell::at(self.project(cursor_position, frame.size())); - - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color { - a: 0.5, - ..Color::BLACK - }, - ); + 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.i, cell.j), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); } + frame.fill_text(Text { + content: format!( + "{} cells @ {:?} ({})", + self.state.cell_count(), + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + frame.into_geometry() }; - if self.scaling < 0.2 { - vec![life, hovered_cell] + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] } else { let grid = self.grid_cache.draw(bounds.size(), |frame| { frame.translate(center); @@ -449,7 +490,7 @@ mod grid { } }); - vec![life, grid, hovered_cell] + vec![life, grid, overlay] } } @@ -677,9 +718,8 @@ struct Controls { impl Controls { fn view<'a>( &'a mut self, - grid: &Grid, - statistics: &'a Statistics, is_playing: bool, + is_grid_enabled: bool, speed: usize, ) -> Element<'a, Message> { let playback_controls = Row::new() @@ -689,7 +729,7 @@ impl Controls { &mut self.toggle_button, Text::new(if is_playing { "Pause" } else { "Play" }), ) - .on_press(Message::Toggle) + .on_press(Message::TogglePlayback) .style(style::Button), ) .push( @@ -719,7 +759,12 @@ impl Controls { .align_items(Align::Center) .push(playback_controls) .push(speed_controls) - .push(statistics.view(grid)) + .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) @@ -728,27 +773,3 @@ impl Controls { .into() } } - -#[derive(Default)] -struct Statistics { - tick_duration: Duration, - last_queued_ticks: usize, -} - -impl Statistics { - fn view(&self, grid: &Grid) -> Element { - Column::new() - .width(Length::Units(150)) - .align_items(Align::Center) - .spacing(2) - .push(Text::new(format!("{} cells", grid.cell_count())).size(14)) - .push( - Text::new(format!( - "{:?} ({})", - self.tick_duration, self.last_queued_ticks - )) - .size(14), - ) - .into() - } -} -- cgit From 4417a34edb7d002276f0419a5f62c6eee4a3af87 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 02:15:11 +0200 Subject: Fix "1 cells" overlay in `game_of_life` --- examples/game_of_life/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 018ebc50..c2f80dfc 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -435,16 +435,19 @@ mod grid { if let Some(cell) = hovered_cell { frame.fill_text(Text { - content: format!("({}, {})", cell.i, cell.j), + 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!( - "{} cells @ {:?} ({})", - self.state.cell_count(), + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, self.last_tick_duration, self.last_queued_ticks ), -- cgit From 917199197f9719bbb3f6f98c63985cb64dfd147c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 02:43:20 +0200 Subject: Allow erasing cells in `game_of_life` --- examples/game_of_life/src/main.rs | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index c2f80dfc..080d55c0 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -156,6 +156,7 @@ mod grid { #[derive(Debug, Clone)] pub enum Message { Populate(Cell), + Unpopulate(Cell), Ticked { result: Result, tick_duration: Duration, @@ -217,6 +218,10 @@ mod grid { 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, @@ -293,20 +298,25 @@ mod grid { 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 = if self.state.contains(&cell) { - None + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) } else { - Some(Message::Populate(cell)) + (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 = Interaction::Drawing; + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; - populate + populate.or(unpopulate) } mouse::Button::Right => { self.interaction = Interaction::Panning { @@ -321,6 +331,7 @@ mod grid { mouse::Event::CursorMoved { .. } => { match self.interaction { Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, Interaction::Panning { translation, start } => { self.translation = translation + (cursor_position - start) @@ -504,6 +515,7 @@ mod grid { ) -> 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 @@ -541,6 +553,14 @@ mod grid { } } + 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)); @@ -592,6 +612,10 @@ mod grid { 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(); @@ -706,6 +730,7 @@ mod grid { enum Interaction { None, Drawing, + Erasing, Panning { translation: Vector, start: Point }, } } -- cgit From 2f41ccee1c7b52f872d68d2e5ebd68ea49a1559b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 3 May 2020 02:49:04 +0200 Subject: Update GIF of `game_of_life` example --- examples/game_of_life/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md index bb1b9736..5741cbf6 100644 --- a/examples/game_of_life/README.md +++ b/examples/game_of_life/README.md @@ -6,8 +6,8 @@ on top of a `Canvas` widget and other controls. The __[`main`]__ file contains the relevant code of the example. -- cgit From 80e2d1b08bd8e6d9a049367d49c1508ce8ffe2a3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 4 May 2020 23:46:15 +0200 Subject: Adapt `color_palette` to new `canvas` API --- examples/color_palette/src/main.rs | 48 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) (limited to 'examples') 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, lab: ColorPicker, lch: ColorPicker, - canvas_layer: canvas::layer::Cache, } #[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 { @@ -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, base: Color, higher: Vec, + canvas_cache: canvas::Cache, } impl Theme { pub fn new(base: impl Into) -> 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 { + 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 for Theme { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { + 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)) -- cgit From 93c6be5eef577f0778b5787dac37351c035ed471 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 4 May 2020 23:54:28 +0200 Subject: Update `README` of `game_of_life` example --- examples/README.md | 21 +++++++++++++++++++++ examples/game_of_life/README.md | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) (limited to 'examples') diff --git a/examples/README.md b/examples/README.md index 7e7bda9d..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. + + + +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. diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md index 5741cbf6..1aeb1455 100644 --- a/examples/game_of_life/README.md +++ b/examples/game_of_life/README.md @@ -1,7 +1,8 @@ ## Game of Life -An interactive version of the Game of Life, invented by John Conway, implemented -on top of a `Canvas` widget and other controls. +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. @@ -17,3 +18,5 @@ 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 -- cgit