diff --git a/Cargo.toml b/Cargo.toml index 206409bf..6dd30b42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "examples/custom_widget", "examples/download_progress", "examples/events", + "examples/game_of_life", "examples/geometry", "examples/integration", "examples/pane_grid", diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index d98b2989..b26bdb3d 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,6 +1,8 @@ //! Reuse basic keyboard types. +mod event; mod key_code; mod modifiers_state; +pub use event::Event; pub use key_code::KeyCode; pub use modifiers_state::ModifiersState; diff --git a/native/src/input/keyboard/event.rs b/core/src/keyboard/event.rs similarity index 68% rename from native/src/input/keyboard/event.rs rename to core/src/keyboard/event.rs index 862f30c4..bc8437a8 100644 --- a/native/src/input/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,5 +1,4 @@ use super::{KeyCode, ModifiersState}; -use crate::input::ButtonState; /// A keyboard event. /// @@ -9,11 +8,17 @@ use crate::input::ButtonState; /// [open an issue]: https://github.com/hecrj/iced/issues #[derive(Debug, Clone, Copy, PartialEq)] pub enum Event { - /// A keyboard key was pressed or released. - Input { - /// The state of the key - state: ButtonState, + /// A keyboard key was pressed. + KeyPressed { + /// The key identifier + key_code: KeyCode, + /// The state of the modifier keys + modifiers: ModifiersState, + }, + + /// A keyboard key was released. + KeyReleased { /// The key identifier key_code: KeyCode, diff --git a/core/src/lib.rs b/core/src/lib.rs index c2887a0b..6b9e612e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -15,6 +15,7 @@ #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] pub mod keyboard; +pub mod mouse; mod align; mod background; diff --git a/native/src/input/mouse.rs b/core/src/mouse.rs similarity index 51% rename from native/src/input/mouse.rs rename to core/src/mouse.rs index 7198b233..25ce6ac3 100644 --- a/native/src/input/mouse.rs +++ b/core/src/mouse.rs @@ -1,9 +1,8 @@ -//! Build mouse events. +//! Reuse basic mouse types. mod button; mod event; - -pub mod click; +mod interaction; pub use button::Button; -pub use click::Click; pub use event::{Event, ScrollDelta}; +pub use interaction::Interaction; diff --git a/native/src/input/mouse/button.rs b/core/src/mouse/button.rs similarity index 100% rename from native/src/input/mouse/button.rs rename to core/src/mouse/button.rs diff --git a/native/src/input/mouse/event.rs b/core/src/mouse/event.rs similarity index 84% rename from native/src/input/mouse/event.rs rename to core/src/mouse/event.rs index aafc4fe3..2f07b207 100644 --- a/native/src/input/mouse/event.rs +++ b/core/src/mouse/event.rs @@ -1,5 +1,4 @@ use super::Button; -use crate::input::ButtonState; /// A mouse event. /// @@ -24,14 +23,11 @@ pub enum Event { y: f32, }, - /// A mouse button was pressed or released. - Input { - /// The state of the button - state: ButtonState, + /// A mouse button was pressed. + ButtonPressed(Button), - /// The button identifier - button: Button, - }, + /// A mouse button was released. + ButtonReleased(Button), /// The mouse wheel was scrolled. WheelScrolled { diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs new file mode 100644 index 00000000..664147a7 --- /dev/null +++ b/core/src/mouse/interaction.rs @@ -0,0 +1,20 @@ +/// The interaction of a mouse cursor. +#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] +#[allow(missing_docs)] +pub enum Interaction { + Idle, + Pointer, + Grab, + Text, + Crosshair, + Working, + Grabbing, + ResizingHorizontally, + ResizingVertically, +} + +impl Default for Interaction { + fn default() -> Interaction { + Interaction::Idle + } +} diff --git a/core/src/point.rs b/core/src/point.rs index 2b5ad154..3714aa2f 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -1,7 +1,7 @@ use crate::Vector; /// A 2D point. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Point { /// The X coordinate. pub x: f32, @@ -67,3 +67,11 @@ impl std::ops::Sub for Point { } } } + +impl std::ops::Sub for Point { + type Output = Vector; + + fn sub(self, point: Point) -> Vector { + Vector::new(self.x - point.x, self.y - point.y) + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index aead6e9a..8bc89a44 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::{Point, Size, Vector}; /// A rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -17,6 +17,35 @@ pub struct Rectangle { } impl Rectangle { + /// Creates a new [`Rectangle`] with its top-left corner in the given + /// [`Point`] and with the provided [`Size`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + /// [`Point`]: struct.Point.html + /// [`Size`]: struct.Size.html + pub fn new(top_left: Point, size: Size) -> Self { + Self { + x: top_left.x, + y: top_left.y, + width: size.width, + height: size.height, + } + } + + /// Creates a new [`Rectangle`] with its top-left corner at the origin + /// and with the provided [`Size`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + /// [`Size`]: struct.Size.html + pub fn with_size(size: Size) -> Self { + Self { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + } + } + /// Returns the [`Point`] at the center of the [`Rectangle`]. /// /// [`Point`]: struct.Point.html @@ -43,6 +72,21 @@ impl Rectangle { self.y + self.height / 2.0 } + /// Returns the position of the top left corner of the [`Rectangle`]. + /// + /// [`Rectangle`]: struct.Rectangle.html + pub fn position(&self) -> Point { + Point::new(self.x, self.y) + } + + /// Returns the [`Size`] of the [`Rectangle`]. + /// + /// [`Size`]: struct.Size.html + /// [`Rectangle`]: struct.Rectangle.html + pub fn size(&self) -> Size { + Size::new(self.width, self.height) + } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. /// /// [`Point`]: struct.Point.html @@ -112,8 +156,23 @@ impl From> for Rectangle { Rectangle { x: rectangle.x as u32, y: rectangle.y as u32, - width: rectangle.width.ceil() as u32, - height: rectangle.height.ceil() as u32, + width: (rectangle.width + 0.5).round() as u32, + height: (rectangle.height + 0.5).round() as u32, + } + } +} + +impl std::ops::Add> for Rectangle +where + T: std::ops::Add, +{ + type Output = Rectangle; + + fn add(self, translation: Vector) -> Self { + Rectangle { + x: self.x + translation.x, + y: self.y + translation.y, + ..self } } } diff --git a/core/src/size.rs b/core/src/size.rs index 4276f05f..a02299e8 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -15,6 +15,11 @@ impl Size { /// [`Size`]: struct.Size.html pub const ZERO: Size = Size::new(0., 0.); + /// A [`Size`] with a width and height of 1 unit. + /// + /// [`Size`]: struct.Size.html + pub const UNIT: Size = Size::new(1., 1.); + /// A [`Size`] with infinite width and height. /// /// [`Size`]: struct.Size.html diff --git a/core/src/vector.rs b/core/src/vector.rs index a75053a0..def3f8c0 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -43,6 +43,17 @@ where } } +impl std::ops::Mul for Vector +where + T: std::ops::Mul + Copy, +{ + type Output = Self; + + fn mul(self, scale: T) -> Self { + Self::new(self.x * scale, self.y * scale) + } +} + impl Default for Vector where T: Default, diff --git a/examples/README.md b/examples/README.md index f67a0dd2..8e1b781f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T [TodoMVC]: http://todomvc.com/ +## [Game of Life](game_of_life) +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file. + + + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway + ## [Styling](styling) An example showcasing custom styling with a light and dark theme. @@ -69,7 +90,7 @@ cargo run --package styling ## Extras A bunch of simpler examples exist: -- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`]. +- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget. - [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time. - [`color_palette`](color_palette), a color palette generator based on a user-defined root color. - [`counter`](counter), the classic counter example explained in the [`README`](../README.md). diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml index b13a0aa5..a88975a7 100644 --- a/examples/bezier_tool/Cargo.toml +++ b/examples/bezier_tool/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_wgpu = { path = "../../wgpu" } -lyon = "0.15" +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md index 933f2120..ebbb12cc 100644 --- a/examples/bezier_tool/README.md +++ b/examples/bezier_tool/README.md @@ -1,6 +1,6 @@ ## Bézier tool -A Paint-like tool for drawing Bézier curves using [`lyon`]. +A Paint-like tool for drawing Bézier curves using the `Canvas` widget. The __[`main`]__ file contains all the code of the example. @@ -16,4 +16,3 @@ cargo run --package bezier_tool ``` [`main`]: src/main.rs -[`lyon`]: https://github.com/nical/lyon diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index fcb7733c..fe41e1b2 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,291 +1,6 @@ -//! This example showcases a simple native custom widget that renders arbitrary -//! path with `lyon`. -mod bezier { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. - use iced_native::{ - input, layout, Clipboard, Color, Element, Event, Font, Hasher, - HorizontalAlignment, Layout, Length, MouseCursor, Point, Rectangle, - Size, Vector, VerticalAlignment, Widget, - }; - use iced_wgpu::{ - triangle::{Mesh2D, Vertex2D}, - Defaults, Primitive, Renderer, - }; - use lyon::tessellation::{ - basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions, - StrokeTessellator, VertexBuffers, - }; - - pub struct Bezier<'a, Message> { - state: &'a mut State, - curves: &'a [Curve], - // [from, to, ctrl] - on_click: Box 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::Mesh2D { - origin: Point::new(bounds.x, bounds.y), - buffers: Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - }; - - ( - Primitive::Clip { - bounds, - offset: Vector::new(0, 0), - content: Box::new( - if self.curves.is_empty() - && self.state.pending.is_none() - { - let instructions = Primitive::Text { - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - color: Color { - a: defaults.text.color.a * 0.7, - ..defaults.text.color - }, - content: String::from( - "Click to create bezier curves!", - ), - font: Font::Default, - size: 30.0, - horizontal_alignment: - HorizontalAlignment::Center, - vertical_alignment: VerticalAlignment::Center, - }; - - Primitive::Group { - primitives: vec![mesh, instructions], - } - } else { - mesh - }, - ), - }, - MouseCursor::OutOfBounds, - ) - } - - fn hash_layout(&self, _state: &mut Hasher) {} - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - messages: &mut Vec, - _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, + button, Align, Button, Column, Element, Length, Sandbox, Settings, Text, }; pub fn main() { @@ -323,6 +38,7 @@ impl Sandbox for Example { match message { Message::AddCurve(curve) => { self.curves.push(curve); + self.bezier.request_redraw(); } Message::Clear => { self.bezier = bezier::State::default(); @@ -332,7 +48,7 @@ impl Sandbox for Example { } fn view(&mut self) -> Element { - let content = Column::new() + Column::new() .padding(20) .spacing(20) .align_items(Align::Center) @@ -341,22 +57,177 @@ impl Sandbox for Example { .width(Length::Shrink) .size(50), ) - .push(Bezier::new( - &mut self.bezier, - self.curves.as_slice(), - Message::AddCurve, - )) + .push(self.bezier.view(&self.curves).map(Message::AddCurve)) .push( Button::new(&mut self.button_state, Text::new("Clear")) .padding(8) .on_press(Message::Clear), - ); - - Container::new(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() + ) .into() } } + +mod bezier { + use iced::{ + canvas::{self, Canvas, Cursor, Event, Frame, Geometry, Path, Stroke}, + mouse, Element, Length, Point, Rectangle, + }; + + #[derive(Default)] + pub struct State { + pending: Option, + 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::Program for Bezier<'a> { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + let cursor_position = cursor.position_in(&bounds)?; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + match self.state.pending { + None => { + self.state.pending = Some(Pending::One { + from: cursor_position, + }); + None + } + Some(Pending::One { from }) => { + self.state.pending = Some(Pending::Two { + from, + to: cursor_position, + }); + + None + } + Some(Pending::Two { from, to }) => { + self.state.pending = None; + + Some(Curve { + from, + to, + control: cursor_position, + }) + } + } + } + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + let content = + self.state.cache.draw(bounds.size(), |frame: &mut Frame| { + Curve::draw_all(self.curves, frame); + + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default(), + ); + }); + + if let Some(pending) = &self.state.pending { + let pending_curve = pending.draw(bounds, cursor); + + vec![content, pending_curve] + } else { + vec![content] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + if cursor.is_over(&bounds) { + mouse::Interaction::Crosshair + } else { + mouse::Interaction::default() + } + } + } + + #[derive(Debug, Clone, Copy)] + pub struct Curve { + from: Point, + to: Point, + control: Point, + } + + impl Curve { + fn draw_all(curves: &[Curve], frame: &mut Frame) { + let curves = Path::new(|p| { + for curve in curves { + p.move_to(curve.from); + p.quadratic_curve_to(curve.control, curve.to); + } + }); + + frame.stroke(&curves, Stroke::default().with_width(2.0)); + } + } + + #[derive(Debug, Clone, Copy)] + enum Pending { + One { from: Point }, + Two { from: Point, to: Point }, + } + + impl Pending { + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { + let mut frame = Frame::new(bounds.size()); + + if let Some(cursor_position) = cursor.position_in(&bounds) { + match *self { + Pending::One { from } => { + let line = Path::line(from, cursor_position); + frame.stroke(&line, Stroke::default().with_width(2.0)); + } + Pending::Two { from, to } => { + let curve = Curve { + from, + to, + control: cursor_position, + }; + + Curve::draw_all(&[curve], &mut frame); + } + }; + } + + frame.into_geometry() + } + } +} diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml index 308cbfbb..c6e32379 100644 --- a/examples/clock/Cargo.toml +++ b/examples/clock/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez "] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } chrono = "0.4" -async-std = { version = "1.0", features = ["unstable"] } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 827379fa..9c583c78 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - canvas, executor, Application, Canvas, Color, Command, Container, Element, - Length, Point, Settings, Subscription, Vector, + canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke}, + executor, time, Application, Color, Command, Container, Element, Length, + Point, Rectangle, Settings, Subscription, Vector, }; pub fn main() { @@ -11,8 +12,8 @@ pub fn main() { } struct Clock { - now: LocalTime, - clock: canvas::layer::Cache, + 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(), @@ -42,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; @@ -55,14 +56,14 @@ 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 { - let canvas = Canvas::new() + let canvas = Canvas::new(self) .width(Length::Units(400)) - .height(Length::Units(400)) - .push(self.clock.with(&self.now)); + .height(Length::Units(400)); Container::new(canvas) .width(Length::Fill) @@ -74,69 +75,54 @@ impl Application for Clock { } } -#[derive(Debug, PartialEq, Eq)] -struct LocalTime { - hour: u32, - minute: u32, - second: u32, -} - -impl From> for LocalTime { - fn from(date_time: chrono::DateTime) -> LocalTime { +impl canvas::Program for Clock { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { use chrono::Timelike; - LocalTime { - hour: date_time.hour(), - minute: date_time.minute(), - second: date_time.second(), - } - } -} + let clock = self.clock.draw(bounds.size(), |frame| { + let center = frame.center(); + let radius = frame.width().min(frame.height()) / 2.0; -impl canvas::Drawable for LocalTime { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::Path; + let background = Path::circle(center, radius); + frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 2.0; + let short_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); - let clock = Path::circle(center, radius); - frame.fill(&clock, Color::from_rgb8(0x12, 0x93, 0xD8)); + let long_hand = + Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); - let short_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); + let thin_stroke = Stroke { + width: radius / 100.0, + color: Color::WHITE, + line_cap: LineCap::Round, + ..Stroke::default() + }; - let long_hand = - Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius)); + let wide_stroke = Stroke { + width: thin_stroke.width * 3.0, + ..thin_stroke + }; - let thin_stroke = canvas::Stroke { - width: radius / 100.0, - color: Color::WHITE, - line_cap: canvas::LineCap::Round, - ..canvas::Stroke::default() - }; + frame.translate(Vector::new(center.x, center.y)); - let wide_stroke = canvas::Stroke { - width: thin_stroke.width * 3.0, - ..thin_stroke - }; + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.hour(), 12)); + frame.stroke(&short_hand, wide_stroke); + }); - frame.translate(Vector::new(center.x, center.y)); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.minute(), 60)); + frame.stroke(&long_hand, wide_stroke); + }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.hour, 12)); - frame.stroke(&short_hand, wide_stroke); + frame.with_save(|frame| { + frame.rotate(hand_rotation(self.now.second(), 60)); + frame.stroke(&long_hand, thin_stroke); + }) }); - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.minute, 60)); - frame.stroke(&long_hand, wide_stroke); - }); - - frame.with_save(|frame| { - frame.rotate(hand_rotation(self.second, 60)); - frame.stroke(&long_hand, thin_stroke); - }); + vec![clock] } } @@ -145,40 +131,3 @@ fn hand_rotation(n: u32, total: u32) -> f32 { 2.0 * std::f32::consts::PI * turns } - -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription> { - 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/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)) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0a570745..f096fb54 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -10,8 +10,8 @@ mod circle { // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. use iced_native::{ - layout, Background, Color, Element, Hasher, Layout, Length, - MouseCursor, Point, Size, Widget, + layout, mouse, Background, Color, Element, Hasher, Layout, Length, + Point, Size, Widget, }; use iced_wgpu::{Defaults, Primitive, Renderer}; @@ -57,7 +57,7 @@ mod circle { _defaults: &Defaults, layout: Layout<'_>, _cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { ( Primitive::Quad { bounds: layout.bounds(), @@ -66,7 +66,7 @@ mod circle { border_width: 0, border_color: Color::TRANSPARENT, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml new file mode 100644 index 00000000..b9bb7f2a --- /dev/null +++ b/examples/game_of_life/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game_of_life" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } +tokio = { version = "0.2", features = ["blocking"] } +itertools = "0.9" +rustc-hash = "1.1" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md new file mode 100644 index 00000000..1aeb1455 --- /dev/null +++ b/examples/game_of_life/README.md @@ -0,0 +1,22 @@ +## Game of Life + +An interactive version of the [Game of Life], invented by [John Horton Conway]. + +It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support. + +The __[`main`]__ file contains the relevant code of the example. + + + +You can run it with `cargo run`: +``` +cargo run --package game_of_life +``` + +[`main`]: src/main.rs +[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life +[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs new file mode 100644 index 00000000..080d55c0 --- /dev/null +++ b/examples/game_of_life/src/main.rs @@ -0,0 +1,803 @@ +//! This example showcases an interactive version of the Game of Life, invented +//! by John Conway. It leverages a `Canvas` together with other widgets. +mod style; + +use grid::Grid; +use iced::{ + button::{self, Button}, + executor, + slider::{self, Slider}, + time, Align, Application, Checkbox, Column, Command, Container, Element, + Length, Row, Settings, Subscription, Text, +}; +use std::time::{Duration, Instant}; + +pub fn main() { + GameOfLife::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +#[derive(Default)] +struct GameOfLife { + grid: Grid, + controls: Controls, + is_playing: bool, + queued_ticks: usize, + speed: usize, + next_speed: Option, +} + +#[derive(Debug, Clone)] +enum Message { + Grid(grid::Message), + Tick(Instant), + TogglePlayback, + ToggleGrid(bool), + Next, + Clear, + SpeedChanged(f32), +} + +impl Application for GameOfLife { + type Message = Message; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + ( + 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.queued_ticks = (self.queued_ticks + 1).min(self.speed); + + if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(speed) = self.next_speed.take() { + self.speed = speed; + } + + self.queued_ticks = 0; + + return Command::perform(task, Message::Grid); + } + } + Message::TogglePlayback => { + self.is_playing = !self.is_playing; + } + Message::ToggleGrid(show_grid_lines) => { + self.grid.toggle_lines(show_grid_lines); + } + Message::Clear => { + self.grid.clear(); + } + Message::SpeedChanged(speed) => { + if self.is_playing { + self.next_speed = Some(speed.round() as usize); + } else { + self.speed = speed.round() as usize; + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + if self.is_playing { + time::every(Duration::from_millis(1000 / self.speed as u64)) + .map(Message::Tick) + } else { + Subscription::none() + } + } + + fn view(&mut self) -> Element { + let selected_speed = self.next_speed.unwrap_or(self.speed); + let controls = self.controls.view( + self.is_playing, + self.grid.are_lines_visible(), + selected_speed, + ); + + let content = Column::new() + .push(self.grid.view().map(Message::Grid)) + .push(controls); + + Container::new(content) + .width(Length::Fill) + .height(Length::Fill) + .style(style::Container) + .into() + } +} + +mod grid { + use iced::{ + canvas::{ + self, Cache, Canvas, Cursor, Event, Frame, Geometry, Path, Text, + }, + mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle, + Size, Vector, VerticalAlignment, + }; + use rustc_hash::{FxHashMap, FxHashSet}; + use std::future::Future; + use std::ops::RangeInclusive; + use std::time::{Duration, Instant}; + + pub struct Grid { + state: State, + interaction: Interaction, + life_cache: Cache, + grid_cache: Cache, + translation: Vector, + scaling: f32, + show_lines: bool, + last_tick_duration: Duration, + last_queued_ticks: usize, + version: usize, + } + + #[derive(Debug, Clone)] + pub enum Message { + Populate(Cell), + Unpopulate(Cell), + Ticked { + result: Result, + tick_duration: Duration, + version: usize, + }, + } + + #[derive(Debug, Clone)] + pub enum TickError { + JoinFailed, + } + + impl Default for Grid { + fn default() -> Self { + Self { + state: State::default(), + interaction: Interaction::None, + life_cache: Cache::default(), + grid_cache: Cache::default(), + translation: Vector::default(), + scaling: 1.0, + show_lines: true, + last_tick_duration: Duration::default(), + last_queued_ticks: 0, + version: 0, + } + } + } + + impl Grid { + const MIN_SCALING: f32 = 0.1; + const MAX_SCALING: f32 = 2.0; + + pub fn tick( + &mut self, + amount: usize, + ) -> Option> { + let version = self.version; + let tick = self.state.tick(amount)?; + + self.last_queued_ticks = amount; + + Some(async move { + let start = Instant::now(); + let result = tick.await; + let tick_duration = start.elapsed() / amount as u32; + + Message::Ticked { + result, + version, + tick_duration, + } + }) + } + + pub fn update(&mut self, message: Message) { + match message { + Message::Populate(cell) => { + self.state.populate(cell); + self.life_cache.clear(); + } + Message::Unpopulate(cell) => { + self.state.unpopulate(&cell); + self.life_cache.clear(); + } + Message::Ticked { + result: Ok(life), + version, + tick_duration, + } if version == self.version => { + self.state.update(life); + self.life_cache.clear(); + + self.last_tick_duration = tick_duration; + } + Message::Ticked { + result: Err(error), .. + } => { + dbg!(error); + } + Message::Ticked { .. } => {} + } + } + + pub fn view<'a>(&'a mut self) -> Element<'a, Message> { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } + + pub fn clear(&mut self) { + self.state = State::default(); + self.version += 1; + + self.life_cache.clear(); + } + + pub fn toggle_lines(&mut self, enabled: bool) { + self.show_lines = enabled; + } + + pub fn are_lines_visible(&self) -> bool { + self.show_lines + } + + fn visible_region(&self, size: Size) -> Region { + let width = size.width / self.scaling; + let height = size.height / self.scaling; + + Region { + x: -self.translation.x - width / 2.0, + y: -self.translation.y - height / 2.0, + width, + height, + } + } + + fn project(&self, position: Point, size: Size) -> Point { + let region = self.visible_region(size); + + Point::new( + position.x / self.scaling + region.x, + position.y / self.scaling + region.y, + ) + } + } + + impl<'a> canvas::Program for Grid { + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + self.interaction = Interaction::None; + } + + let cursor_position = cursor.position_in(&bounds)?; + let cell = Cell::at(self.project(cursor_position, bounds.size())); + let is_populated = self.state.contains(&cell); + + let (populate, unpopulate) = if is_populated { + (None, Some(Message::Unpopulate(cell))) + } else { + (Some(Message::Populate(cell)), None) + }; + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(button) => match button { + mouse::Button::Left => { + self.interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing + }; + + populate.or(unpopulate) + } + mouse::Button::Right => { + self.interaction = Interaction::Panning { + translation: self.translation, + start: cursor_position, + }; + + None + } + _ => None, + }, + mouse::Event::CursorMoved { .. } => { + match self.interaction { + Interaction::Drawing => populate, + Interaction::Erasing => unpopulate, + Interaction::Panning { translation, start } => { + self.translation = translation + + (cursor_position - start) + * (1.0 / self.scaling); + + self.life_cache.clear(); + self.grid_cache.clear(); + + None + } + _ => None, + } + } + mouse::Event::WheelScrolled { delta } => match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + if y < 0.0 && self.scaling > Self::MIN_SCALING + || y > 0.0 && self.scaling < Self::MAX_SCALING + { + let old_scaling = self.scaling; + + self.scaling = (self.scaling + * (1.0 + y / 30.0)) + .max(Self::MIN_SCALING) + .min(Self::MAX_SCALING); + + if let Some(cursor_to_center) = + cursor.position_from(bounds.center()) + { + let factor = self.scaling - old_scaling; + + self.translation = self.translation + - Vector::new( + cursor_to_center.x * factor + / (old_scaling * old_scaling), + cursor_to_center.y * factor + / (old_scaling * old_scaling), + ); + } + + self.life_cache.clear(); + self.grid_cache.clear(); + } + + None + } + }, + _ => None, + }, + } + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); + + let life = self.life_cache.draw(bounds.size(), |frame| { + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + + for cell in region.cull(self.state.cells()) { + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color::WHITE, + ); + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.size()); + + let hovered_cell = + cursor.position_in(&bounds).map(|position| { + Cell::at(self.project(position, frame.size())) + }); + + if let Some(cell) = hovered_cell { + frame.with_save(|frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + frame.fill_rectangle( + Point::new(cell.j as f32, cell.i as f32), + Size::UNIT, + Color { + a: 0.5, + ..Color::BLACK + }, + ); + }); + } + + let text = Text { + color: Color::WHITE, + size: 14.0, + position: Point::new(frame.width(), frame.height()), + horizontal_alignment: HorizontalAlignment::Right, + vertical_alignment: VerticalAlignment::Bottom, + ..Text::default() + }; + + if let Some(cell) = hovered_cell { + frame.fill_text(Text { + content: format!("({}, {})", cell.j, cell.i), + position: text.position - Vector::new(0.0, 16.0), + ..text + }); + } + + let cell_count = self.state.cell_count(); + + frame.fill_text(Text { + content: format!( + "{} cell{} @ {:?} ({})", + cell_count, + if cell_count == 1 { "" } else { "s" }, + self.last_tick_duration, + self.last_queued_ticks + ), + ..text + }); + + frame.into_geometry() + }; + + if self.scaling < 0.2 || !self.show_lines { + vec![life, overlay] + } else { + let grid = self.grid_cache.draw(bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); + + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } + + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); + + vec![life, grid, overlay] + } + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + match self.interaction { + Interaction::Drawing => mouse::Interaction::Crosshair, + Interaction::Erasing => mouse::Interaction::Crosshair, + Interaction::Panning { .. } => mouse::Interaction::Grabbing, + Interaction::None if cursor.is_over(&bounds) => { + mouse::Interaction::Crosshair + } + _ => mouse::Interaction::default(), + } + } + } + + #[derive(Default)] + struct State { + life: Life, + births: FxHashSet, + is_ticking: bool, + } + + impl State { + fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + fn cells(&self) -> impl Iterator { + self.life.iter().chain(self.births.iter()) + } + + fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + fn tick( + &mut self, + amount: usize, + ) -> Option>> { + 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, + } + + impl Life { + fn len(&self) -> usize { + self.cells.len() + } + + fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator { + self.cells.iter() + } + } + + impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct Cell { + i: isize, + j: isize, + } + + impl Cell { + const SIZE: usize = 20; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } + } + + pub struct Region { + x: f32, + y: f32, + width: f32, + height: f32, + } + + impl Region { + fn rows(&self) -> RangeInclusive { + let first_row = (self.y / Cell::SIZE as f32).floor() as isize; + + let visible_rows = + (self.height / Cell::SIZE as f32).ceil() as isize; + + first_row..=first_row + visible_rows + } + + fn columns(&self) -> RangeInclusive { + let first_column = (self.x / Cell::SIZE as f32).floor() as isize; + + let visible_columns = + (self.width / Cell::SIZE as f32).ceil() as isize; + + first_column..=first_column + visible_columns + } + + fn cull<'a>( + &self, + cells: impl Iterator, + ) -> impl Iterator { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |cell| { + rows.contains(&cell.i) && columns.contains(&cell.j) + }) + } + } + + enum Interaction { + None, + Drawing, + Erasing, + Panning { translation: Vector, start: Point }, + } +} + +#[derive(Default)] +struct Controls { + toggle_button: button::State, + next_button: button::State, + clear_button: button::State, + speed_slider: slider::State, +} + +impl Controls { + fn view<'a>( + &'a mut self, + is_playing: bool, + is_grid_enabled: bool, + speed: usize, + ) -> Element<'a, Message> { + let playback_controls = Row::new() + .spacing(10) + .push( + Button::new( + &mut self.toggle_button, + Text::new(if is_playing { "Pause" } else { "Play" }), + ) + .on_press(Message::TogglePlayback) + .style(style::Button), + ) + .push( + Button::new(&mut self.next_button, Text::new("Next")) + .on_press(Message::Next) + .style(style::Button), + ); + + let speed_controls = Row::new() + .width(Length::Fill) + .align_items(Align::Center) + .spacing(10) + .push( + Slider::new( + &mut self.speed_slider, + 1.0..=1000.0, + speed as f32, + Message::SpeedChanged, + ) + .style(style::Slider), + ) + .push(Text::new(format!("x{}", speed)).size(16)); + + Row::new() + .padding(10) + .spacing(20) + .align_items(Align::Center) + .push(playback_controls) + .push(speed_controls) + .push( + Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid) + .size(16) + .spacing(5) + .text_size(16), + ) + .push( + Button::new(&mut self.clear_button, Text::new("Clear")) + .on_press(Message::Clear) + .style(style::Clear), + ) + .into() + } +} diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs new file mode 100644 index 00000000..d59569f2 --- /dev/null +++ b/examples/game_of_life/src/style.rs @@ -0,0 +1,134 @@ +use iced::{button, container, slider, Background, Color}; + +const ACTIVE: Color = Color::from_rgb( + 0x72 as f32 / 255.0, + 0x89 as f32 / 255.0, + 0xDA as f32 / 255.0, +); + +const DESTRUCTIVE: Color = Color::from_rgb( + 0xC0 as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +const HOVERED: Color = Color::from_rgb( + 0x67 as f32 / 255.0, + 0x7B as f32 / 255.0, + 0xC4 as f32 / 255.0, +); + +pub struct Container; + +impl container::StyleSheet for Container { + fn style(&self) -> container::Style { + container::Style { + background: Some(Background::Color(Color::from_rgb8( + 0x36, 0x39, 0x3F, + ))), + text_color: Some(Color::WHITE), + ..container::Style::default() + } + } +} + +pub struct Button; + +impl button::StyleSheet for Button { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(ACTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(HOVERED)), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Clear; + +impl button::StyleSheet for Clear { + fn active(&self) -> button::Style { + button::Style { + background: Some(Background::Color(DESTRUCTIVE)), + border_radius: 3, + text_color: Color::WHITE, + ..button::Style::default() + } + } + + fn hovered(&self) -> button::Style { + button::Style { + background: Some(Background::Color(Color { + a: 0.5, + ..DESTRUCTIVE + })), + text_color: Color::WHITE, + ..self.active() + } + } + + fn pressed(&self) -> button::Style { + button::Style { + border_width: 1, + border_color: Color::WHITE, + ..self.hovered() + } + } +} + +pub struct Slider; + +impl slider::StyleSheet for Slider { + fn active(&self) -> slider::Style { + slider::Style { + rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }), + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 9 }, + color: ACTIVE, + border_width: 0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: HOVERED, + ..active.handle + }, + ..active + } + } + + fn dragging(&self) -> slider::Style { + let active = self.active(); + + slider::Style { + handle: slider::Handle { + color: Color::from_rgb(0.85, 0.85, 0.85), + ..active.handle + }, + ..active + } + } +} diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 13a687ab..aabe6b21 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -11,7 +11,7 @@ mod rainbow { // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, + layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, Widget, }; use iced_wgpu::{ @@ -54,7 +54,7 @@ mod rainbow { _defaults: &Defaults, layout: Layout<'_>, cursor_position: Point, - ) -> (Primitive, MouseCursor) { + ) -> (Primitive, mouse::Interaction) { let b = layout.bounds(); // R O Y G B I V @@ -85,60 +85,63 @@ mod rainbow { let posn_l = [0.0, b.height / 2.0]; ( - Primitive::Mesh2D { - origin: Point::new(b.x, b.y), - buffers: Mesh2D { - vertices: vec![ - Vertex2D { - position: posn_center, - color: [1.0, 1.0, 1.0, 1.0], - }, - Vertex2D { - position: posn_tl, - color: color_r, - }, - Vertex2D { - position: posn_t, - color: color_o, - }, - Vertex2D { - position: posn_tr, - color: color_y, - }, - Vertex2D { - position: posn_r, - color: color_g, - }, - Vertex2D { - position: posn_br, - color: color_gb, - }, - Vertex2D { - position: posn_b, - color: color_b, - }, - Vertex2D { - position: posn_bl, - color: color_i, - }, - Vertex2D { - position: posn_l, - color: color_v, - }, - ], - indices: vec![ - 0, 1, 2, // TL - 0, 2, 3, // T - 0, 3, 4, // TR - 0, 4, 5, // R - 0, 5, 6, // BR - 0, 6, 7, // B - 0, 7, 8, // BL - 0, 8, 1, // L - ], - }, + Primitive::Translate { + translation: Vector::new(b.x, b.y), + content: Box::new(Primitive::Mesh2D { + size: b.size(), + buffers: Mesh2D { + vertices: vec![ + Vertex2D { + position: posn_center, + color: [1.0, 1.0, 1.0, 1.0], + }, + Vertex2D { + position: posn_tl, + color: color_r, + }, + Vertex2D { + position: posn_t, + color: color_o, + }, + Vertex2D { + position: posn_tr, + color: color_y, + }, + Vertex2D { + position: posn_r, + color: color_g, + }, + Vertex2D { + position: posn_br, + color: color_gb, + }, + Vertex2D { + position: posn_b, + color: color_b, + }, + Vertex2D { + position: posn_bl, + color: color_i, + }, + Vertex2D { + position: posn_l, + color: color_v, + }, + ], + indices: vec![ + 0, 1, 2, // TL + 0, 2, 3, // T + 0, 3, 4, // TR + 0, 4, 5, // R + 0, 5, 6, // BR + 0, 6, 7, // B + 0, 7, 8, // BL + 0, 8, 1, // L + ], + }, + }), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7203d4b6..92d2fa8d 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -8,7 +8,7 @@ use iced_wgpu::{ wgpu, window::SwapChain, Primitive, Renderer, Settings, Target, }; use iced_winit::{ - futures, winit, Cache, Clipboard, MouseCursor, Size, UserInterface, + futures, mouse, winit, Cache, Clipboard, Size, UserInterface, }; use winit::{ @@ -63,7 +63,7 @@ pub fn main() { let mut events = Vec::new(); let mut cache = Some(Cache::default()); let mut renderer = Renderer::new(&mut device, Settings::default()); - let mut output = (Primitive::None, MouseCursor::OutOfBounds); + let mut output = (Primitive::None, mouse::Interaction::default()); let clipboard = Clipboard::new(&window); // Initialize scene and GUI controls @@ -189,7 +189,7 @@ pub fn main() { scene.draw(&mut encoder, &frame.view); // And then iced on top - let mouse_cursor = renderer.draw( + let mouse_interaction = renderer.draw( &mut device, &mut encoder, Target { @@ -205,9 +205,11 @@ pub fn main() { queue.submit(&[encoder.finish()]); // And update the mouse cursor - window.set_cursor_icon(iced_winit::conversion::mouse_cursor( - mouse_cursor, - )); + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + mouse_interaction, + ), + ); } _ => {} } diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml index c88cda50..44ced729 100644 --- a/examples/solar_system/Cargo.toml +++ b/examples/solar_system/Cargo.toml @@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez "] edition = "2018" publish = false -[features] -canvas = [] - [dependencies] -iced = { path = "../..", features = ["canvas", "async-std", "debug"] } -iced_native = { path = "../../native" } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["canvas", "tokio", "debug"] } rand = "0.7" diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index bcd1dc71..98bd3b21 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -7,8 +7,9 @@ //! //! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system use iced::{ - canvas, executor, window, Application, Canvas, Color, Command, Container, - Element, Length, Point, Settings, Size, Subscription, Vector, + canvas::{self, Cursor, Path, Stroke}, + executor, time, window, Application, Canvas, Color, Command, Element, + Length, Point, Rectangle, Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -22,7 +23,6 @@ pub fn main() { struct SolarSystem { state: State, - solar_system: canvas::layer::Cache, } #[derive(Debug, Clone, Copy)] @@ -39,7 +39,6 @@ impl Application for SolarSystem { ( SolarSystem { state: State::new(), - solar_system: Default::default(), }, Command::none(), ) @@ -53,7 +52,6 @@ impl Application for SolarSystem { match message { Message::Tick(instant) => { self.state.update(instant); - self.solar_system.clear(); } } @@ -66,24 +64,20 @@ impl Application for SolarSystem { } fn view(&mut self) -> Element { - let canvas = Canvas::new() + Canvas::new(&mut self.state) .width(Length::Fill) .height(Length::Fill) - .push(self.solar_system.with(&self.state)); - - Container::new(canvas) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() .into() } } #[derive(Debug)] struct State { + space_cache: canvas::Cache, + system_cache: canvas::Cache, + cursor_position: Point, start: Instant, - current: Instant, + now: Instant, stars: Vec<(Point, f32)>, } @@ -99,137 +93,122 @@ impl State { let (width, height) = window::Settings::default().size; State { + space_cache: Default::default(), + system_cache: Default::default(), + cursor_position: Point::ORIGIN, start: now, - current: now, - stars: { - use rand::Rng; - - let mut rng = rand::thread_rng(); - - (0..100) - .map(|_| { - ( - Point::new( - rng.gen_range(0.0, width as f32), - rng.gen_range(0.0, height as f32), - ), - rng.gen_range(0.5, 1.0), - ) - }) - .collect() - }, + now, + stars: Self::generate_stars(width, height), } } pub fn update(&mut self, now: Instant) { - self.current = now; + self.now = now; + self.system_cache.clear(); + } + + fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + use rand::Rng; + + let mut rng = rand::thread_rng(); + + (0..100) + .map(|_| { + ( + Point::new( + rng.gen_range( + -(width as f32) / 2.0, + width as f32 / 2.0, + ), + rng.gen_range( + -(height as f32) / 2.0, + height as f32 / 2.0, + ), + ), + rng.gen_range(0.5, 1.0), + ) + }) + .collect() } } -impl canvas::Drawable for State { - fn draw(&self, frame: &mut canvas::Frame) { - use canvas::{Path, Stroke}; +impl canvas::Program for State { + fn draw( + &self, + bounds: Rectangle, + _cursor: Cursor, + ) -> Vec { use std::f32::consts::PI; - let center = frame.center(); + let background = self.space_cache.draw(bounds.size(), |frame| { + let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - let space = Path::rectangle(Point::new(0.0, 0.0), frame.size()); - - let stars = Path::new(|path| { - for (p, size) in &self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } - }); - - let sun = Path::circle(center, Self::SUN_RADIUS); - let orbit = Path::circle(center, Self::ORBIT_RADIUS); - - frame.fill(&space, Color::BLACK); - frame.fill(&stars, Color::WHITE); - frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C)); - frame.stroke( - &orbit, - Stroke { - width: 1.0, - color: Color::from_rgba8(0, 153, 255, 0.1), - ..Stroke::default() - }, - ); - - let elapsed = self.current - self.start; - let elapsed_seconds = elapsed.as_secs() as f32; - let elapsed_millis = 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.translate(Vector::new(Self::ORBIT_RADIUS, 0.0)); - - let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS); - let shadow = Path::rectangle( - Point::new(0.0, -Self::EARTH_RADIUS), - Size::new(Self::EARTH_RADIUS * 4.0, Self::EARTH_RADIUS * 2.0), - ); - - frame.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.translate(Vector::new(0.0, Self::MOON_DISTANCE)); - - let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS); - frame.fill(&moon, Color::WHITE); + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } }); - frame.fill( - &shadow, - Color { - a: 0.7, - ..Color::BLACK + frame.fill(&space, Color::BLACK); + + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); + }); + + let system = self.system_cache.draw(bounds.size(), |frame| { + 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() }, ); + + 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)); + + 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.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); + }); + + frame.fill( + &shadow, + Color { + a: 0.7, + ..Color::BLACK + }, + ); + }); }); - } -} - -mod time { - use iced::futures; - use std::time::Instant; - - pub fn every(duration: std::time::Duration) -> iced::Subscription { - 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() - } + + vec![background, system] } } diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml index 1dae3b83..075aa111 100644 --- a/examples/stopwatch/Cargo.toml +++ b/examples/stopwatch/Cargo.toml @@ -6,7 +6,4 @@ edition = "2018" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } -iced_futures = { path = "../../futures", features = ["async-std"] } -async-std = { version = "1.0", features = ["unstable"] } +iced = { path = "../..", features = ["tokio"] } diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 5a54ed2b..9de6d39e 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,6 +1,7 @@ use iced::{ - button, Align, Application, Button, Column, Command, Container, Element, - HorizontalAlignment, Length, Row, Settings, Subscription, Text, + button, executor, time, Align, Application, Button, Column, Command, + Container, Element, HorizontalAlignment, Length, Row, Settings, + Subscription, Text, }; use std::time::{Duration, Instant}; @@ -28,7 +29,7 @@ enum Message { } impl Application for Stopwatch { - type Executor = iced_futures::executor::AsyncStd; + type Executor = executor::Default; type Message = Message; type Flags = (); @@ -143,43 +144,6 @@ impl Application for Stopwatch { } } -mod time { - use iced::futures; - - pub fn every( - duration: std::time::Duration, - ) -> iced::Subscription { - 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}; diff --git a/futures/Cargo.toml b/futures/Cargo.toml index f0e2a104..275d0391 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -22,11 +22,12 @@ version = "0.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "0.2" optional = true -features = ["rt-core", "rt-threaded"] +features = ["rt-core", "rt-threaded", "time", "stream"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std] version = "1.0" optional = true +features = ["unstable"] [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" diff --git a/futures/src/lib.rs b/futures/src/lib.rs index 966a9cdc..46fc59fc 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -14,6 +14,13 @@ mod runtime; pub mod executor; pub mod subscription; +#[cfg(all( + any(feature = "tokio", feature = "async-std"), + not(target_arch = "wasm32") +))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] +pub mod time; + pub use command::Command; pub use executor::Executor; pub use runtime::Runtime; diff --git a/futures/src/time.rs b/futures/src/time.rs new file mode 100644 index 00000000..e87b4a83 --- /dev/null +++ b/futures/src/time.rs @@ -0,0 +1,70 @@ +//! Listen and react to time. +use crate::subscription::{self, Subscription}; + +/// Returns a [`Subscription`] that produces messages at a set interval. +/// +/// The first message is produced after a `duration`, and then continues to +/// produce more messages every `duration` after that. +/// +/// [`Subscription`]: ../subscription/struct.Subscription.html +pub fn every( + duration: std::time::Duration, +) -> Subscription { + Subscription::from_recipe(Every(duration)) +} + +struct Every(std::time::Duration); + +#[cfg(feature = "async-std")] +impl 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, E>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + async_std::stream::interval(self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } +} + +#[cfg(all(feature = "tokio", not(feature = "async-std")))] +impl 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, E>, + ) -> futures::stream::BoxStream<'static, Self::Output> { + use futures::stream::StreamExt; + + let start = tokio::time::Instant::now() + self.0; + + tokio::time::interval_at(start, self.0) + .map(|_| std::time::Instant::now()) + .boxed() + } +} diff --git a/native/src/event.rs b/native/src/event.rs index b2550ead..606a71d6 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -1,7 +1,4 @@ -use crate::{ - input::{keyboard, mouse}, - window, -}; +use crate::{keyboard, mouse, window}; /// A user interface event. /// diff --git a/native/src/input.rs b/native/src/input.rs deleted file mode 100644 index 097fa730..00000000 --- a/native/src/input.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Map your system events into input events that the runtime can understand. -pub mod keyboard; -pub mod mouse; - -mod button_state; - -pub use button_state::ButtonState; diff --git a/native/src/input/button_state.rs b/native/src/input/button_state.rs deleted file mode 100644 index 988043ba..00000000 --- a/native/src/input/button_state.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// The state of a button. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -pub enum ButtonState { - /// The button is pressed. - Pressed, - - /// The button is __not__ pressed. - Released, -} diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs deleted file mode 100644 index 928bf492..00000000 --- a/native/src/input/keyboard.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Build keyboard events. -mod event; - -pub use event::Event; -pub use iced_core::keyboard::{KeyCode, ModifiersState}; diff --git a/native/src/keyboard.rs b/native/src/keyboard.rs new file mode 100644 index 00000000..012538e3 --- /dev/null +++ b/native/src/keyboard.rs @@ -0,0 +1,2 @@ +//! Track keyboard events. +pub use iced_core::keyboard::*; diff --git a/native/src/lib.rs b/native/src/lib.rs index 89612391..9882803f 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -39,8 +39,9 @@ #![deny(unused_results)] #![forbid(unsafe_code)] #![forbid(rust_2018_idioms)] -pub mod input; +pub mod keyboard; pub mod layout; +pub mod mouse; pub mod renderer; pub mod subscription; pub mod widget; @@ -50,7 +51,6 @@ mod clipboard; mod element; mod event; mod hasher; -mod mouse_cursor; mod runtime; mod user_interface; @@ -68,7 +68,6 @@ pub use element::Element; pub use event::Event; pub use hasher::Hasher; pub use layout::Layout; -pub use mouse_cursor::MouseCursor; pub use renderer::Renderer; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/native/src/mouse.rs b/native/src/mouse.rs new file mode 100644 index 00000000..9ee406cf --- /dev/null +++ b/native/src/mouse.rs @@ -0,0 +1,6 @@ +//! Track mouse events. + +pub mod click; + +pub use click::Click; +pub use iced_core::mouse::*; diff --git a/native/src/input/mouse/click.rs b/native/src/mouse/click.rs similarity index 100% rename from native/src/input/mouse/click.rs rename to native/src/mouse/click.rs diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs deleted file mode 100644 index 0dad3edc..00000000 --- a/native/src/mouse_cursor.rs +++ /dev/null @@ -1,36 +0,0 @@ -/// The state of the mouse cursor. -#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] -pub enum MouseCursor { - /// The cursor is out of the bounds of the user interface. - OutOfBounds, - - /// The cursor is over a non-interactive widget. - Idle, - - /// The cursor is over a clickable widget. - Pointer, - - /// The cursor is over a busy widget. - Working, - - /// The cursor is over a grabbable widget. - Grab, - - /// The cursor is grabbing a widget. - Grabbing, - - /// The cursor is over a text widget. - Text, - - /// The cursor is resizing a widget horizontally. - ResizingHorizontally, - - /// The cursor is resizing a widget vertically. - ResizingVertically, -} - -impl Default for MouseCursor { - fn default() -> MouseCursor { - MouseCursor::OutOfBounds - } -} diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 5d9221e9..48cd6111 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,6 +1,4 @@ -use crate::{ - input::mouse, layout, Clipboard, Element, Event, Layout, Point, Size, -}; +use crate::{layout, mouse, Clipboard, Element, Event, Layout, Point, Size}; use std::hash::Hasher; diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 3cf4f780..c932da2b 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -5,8 +5,7 @@ //! [`Button`]: struct.Button.html //! [`State`]: struct.State.html use crate::{ - input::{mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Widget, }; use std::hash::Hash; @@ -185,28 +184,24 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if self.on_press.is_some() { + let bounds = layout.bounds(); + + self.state.is_pressed = bounds.contains(cursor_position); + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { if let Some(on_press) = self.on_press.clone() { let bounds = layout.bounds(); - match state { - ButtonState::Pressed => { - self.state.is_pressed = - bounds.contains(cursor_position); - } - ButtonState::Released => { - let is_clicked = self.state.is_pressed - && bounds.contains(cursor_position); + let is_clicked = self.state.is_pressed + && bounds.contains(cursor_position); - self.state.is_pressed = false; + self.state.is_pressed = false; - if is_clicked { - messages.push(on_press); - } - } + if is_clicked { + messages.push(on_press); } } } diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 4d167df7..5fb13290 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -2,8 +2,7 @@ use std::hash::Hash; use crate::{ - input::{mouse, ButtonState}, - layout, row, text, Align, Clipboard, Element, Event, Hasher, + layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -152,10 +151,7 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index f84775ed..c398a30b 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -22,9 +22,8 @@ pub use split::Split; pub use state::{Focus, State}; use crate::{ - input::{keyboard, mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size, - Widget, + keyboard, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, + Point, Size, Widget, }; /// A collection of panes distributed using either vertical or horizontal splits @@ -405,11 +404,8 @@ where clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => match state { - ButtonState::Pressed => { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { let mut clicked_region = self.elements.iter().zip(layout.children()).filter( |(_, layout)| { @@ -438,7 +434,7 @@ where self.state.unfocus(); } } - ButtonState::Released => { + mouse::Event::ButtonReleased(mouse::Button::Left) => { if let Some(pane) = self.state.picked_pane() { self.state.focus(&pane); @@ -465,97 +461,110 @@ where } } } - }, - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Right, - state: ButtonState::Pressed, - }) if self.on_resize.is_some() - && self.state.picked_pane().is_none() - && self.pressed_modifiers.matches(self.modifier_keys) => - { - let bounds = layout.bounds(); + mouse::Event::ButtonPressed(mouse::Button::Right) + if self.on_resize.is_some() + && self.state.picked_pane().is_none() + && self + .pressed_modifiers + .matches(self.modifier_keys) => + { + let bounds = layout.bounds(); - if bounds.contains(cursor_position) { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); + if bounds.contains(cursor_position) { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); - let splits = self.state.splits( - f32::from(self.spacing), - Size::new(bounds.width, bounds.height), - ); + let splits = self.state.splits( + f32::from(self.spacing), + Size::new(bounds.width, bounds.height), + ); - let mut sorted_splits: Vec<_> = splits - .iter() - .filter(|(_, (axis, rectangle, _))| match axis { - Axis::Horizontal => { - relative_cursor.x > rectangle.x - && relative_cursor.x - < rectangle.x + rectangle.width - } - Axis::Vertical => { - relative_cursor.y > rectangle.y - && relative_cursor.y - < rectangle.y + rectangle.height - } - }) - .collect(); - - sorted_splits.sort_by_key( - |(_, (axis, rectangle, ratio))| { - let distance = match axis { - Axis::Horizontal => (relative_cursor.y - - (rectangle.y + rectangle.height * ratio)) - .abs(), - Axis::Vertical => (relative_cursor.x - - (rectangle.x + rectangle.width * ratio)) - .abs(), - }; - - distance.round() as u32 - }, - ); - - if let Some((split, (axis, _, _))) = sorted_splits.first() { - self.state.pick_split(split, *axis); - self.trigger_resize(layout, cursor_position, messages); - } - } - } - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Right, - state: ButtonState::Released, - }) if self.state.picked_split().is_some() => { - self.state.drop_split(); - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - self.trigger_resize(layout, cursor_position, messages); - } - Event::Keyboard(keyboard::Event::Input { - modifiers, - key_code, - state, - }) => { - if let Some(on_key_press) = &self.on_key_press { - // TODO: Discard when event is captured - if state == ButtonState::Pressed { - if let Some(_) = self.state.active_pane() { - if modifiers.matches(self.modifier_keys) { - if let Some(message) = - on_key_press(KeyPressEvent { - key_code, - modifiers, - }) - { - messages.push(message); + let mut sorted_splits: Vec<_> = splits + .iter() + .filter(|(_, (axis, rectangle, _))| match axis { + Axis::Horizontal => { + relative_cursor.x > rectangle.x + && relative_cursor.x + < rectangle.x + rectangle.width } - } + Axis::Vertical => { + relative_cursor.y > rectangle.y + && relative_cursor.y + < rectangle.y + rectangle.height + } + }) + .collect(); + + sorted_splits.sort_by_key( + |(_, (axis, rectangle, ratio))| { + let distance = match axis { + Axis::Horizontal => (relative_cursor.y + - (rectangle.y + + rectangle.height * ratio)) + .abs(), + Axis::Vertical => (relative_cursor.x + - (rectangle.x + + rectangle.width * ratio)) + .abs(), + }; + + distance.round() as u32 + }, + ); + + if let Some((split, (axis, _, _))) = + sorted_splits.first() + { + self.state.pick_split(split, *axis); + self.trigger_resize( + layout, + cursor_position, + messages, + ); } } } + mouse::Event::ButtonPressed(mouse::Button::Right) + if self.state.picked_split().is_some() => + { + self.state.drop_split(); + } + mouse::Event::CursorMoved { .. } => { + self.trigger_resize(layout, cursor_position, messages); + } + _ => {} + }, + Event::Keyboard(keyboard_event) => { + match keyboard_event { + keyboard::Event::KeyPressed { + modifiers, + key_code, + } => { + if let Some(on_key_press) = &self.on_key_press { + // TODO: Discard when event is captured + if let Some(_) = self.state.active_pane() { + if modifiers.matches(self.modifier_keys) { + if let Some(message) = + on_key_press(KeyPressEvent { + key_code, + modifiers, + }) + { + messages.push(message); + } + } + } + } - *self.pressed_modifiers = modifiers; + *self.pressed_modifiers = modifiers; + } + keyboard::Event::KeyReleased { modifiers, .. } => { + *self.pressed_modifiers = modifiers; + } + _ => {} + } } _ => {} } diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index 0a8b8419..ed2813b8 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,5 +1,5 @@ use crate::{ - input::keyboard, + keyboard, pane_grid::{node::Node, Axis, Direction, Pane, Split}, Hasher, Point, Rectangle, Size, }; diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index a13d86a0..ab5bcf32 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -1,7 +1,6 @@ //! Create choices using radio buttons. use crate::{ - input::{mouse, ButtonState}, - layout, row, text, Align, Clipboard, Element, Event, Hasher, + layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher, HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget, }; @@ -123,10 +122,7 @@ where _clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if layout.bounds().contains(cursor_position) { messages.push(self.on_click.clone()); } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 393095a4..3c8e5e5b 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,9 +1,7 @@ //! Navigate an endless amount of content with a scrollbar. use crate::{ - column, - input::{mouse, ButtonState}, - layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length, - Point, Rectangle, Size, Widget, + column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, + Layout, Length, Point, Rectangle, Size, Widget, }; use std::{f32, hash::Hash, u32}; @@ -188,10 +186,9 @@ where if self.state.is_scroller_grabbed() { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) => { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) => { self.state.scroller_grabbed_at = None; } Event::Mouse(mouse::Event::CursorMoved { .. }) => { @@ -212,10 +209,9 @@ where } } else if is_mouse_over_scrollbar { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => { if let Some(scrollbar) = scrollbar { if let Some(scroller_grabbed_at) = scrollbar.grab_scroller(cursor_position) diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 1feb7825..8cdfc3de 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -5,8 +5,7 @@ //! [`Slider`]: struct.Slider.html //! [`State`]: struct.State.html use crate::{ - input::{mouse, ButtonState}, - layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, + layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; @@ -164,25 +163,23 @@ where }; match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state, - }) => match state { - ButtonState::Pressed => { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { if layout.bounds().contains(cursor_position) { change(); self.state.is_dragging = true; } } - ButtonState::Released => { + mouse::Event::ButtonReleased(mouse::Button::Left) => { self.state.is_dragging = false; } - }, - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - if self.state.is_dragging { - change(); + mouse::Event::CursorMoved { .. } => { + if self.state.is_dragging { + change(); + } } - } + _ => {} + }, _ => {} } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 7d1a7415..1cdbe007 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -15,13 +15,10 @@ pub use value::Value; use editor::Editor; use crate::{ - input::{ - keyboard, - mouse::{self, click}, - ButtonState, - }, - layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, - Rectangle, Size, Widget, + keyboard, layout, + mouse::{self, click}, + Clipboard, Element, Event, Font, Hasher, Layout, Length, Point, Rectangle, + Size, Widget, }; use std::u32; @@ -212,10 +209,7 @@ where clipboard: Option<&dyn Clipboard>, ) { match event { - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Pressed, - }) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let is_clicked = layout.bounds().contains(cursor_position); if is_clicked { @@ -280,10 +274,7 @@ where self.state.is_dragging = is_clicked; self.state.is_focused = is_clicked; } - Event::Mouse(mouse::Event::Input { - button: mouse::Button::Left, - state: ButtonState::Released, - }) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { self.state.is_dragging = false; } Event::Mouse(mouse::Event::CursorMoved { x, .. }) => { @@ -327,9 +318,8 @@ where let message = (self.on_change)(editor.contents()); messages.push(message); } - Event::Keyboard(keyboard::Event::Input { + Event::Keyboard(keyboard::Event::KeyPressed { key_code, - state: ButtonState::Pressed, modifiers, }) if self.state.is_focused => match key_code { keyboard::KeyCode::Enter => { @@ -473,10 +463,8 @@ where } _ => {} }, - Event::Keyboard(keyboard::Event::Input { - key_code, - state: ButtonState::Released, - .. + Event::Keyboard(keyboard::Event::KeyReleased { + key_code, .. }) => match key_code { keyboard::KeyCode::V => { self.state.is_pasting = None; @@ -749,7 +737,7 @@ fn find_cursor_position( } mod platform { - use crate::input::keyboard; + use crate::keyboard; pub fn is_jump_modifier_pressed( modifiers: keyboard::ModifiersState, diff --git a/native/src/window/backend.rs b/native/src/window/backend.rs index 3bc691cd..892d4bb9 100644 --- a/native/src/window/backend.rs +++ b/native/src/window/backend.rs @@ -1,4 +1,4 @@ -use crate::MouseCursor; +use crate::mouse; use raw_window_handle::HasRawWindowHandle; @@ -51,5 +51,5 @@ pub trait Backend: Sized { output: &::Output, scale_factor: f64, overlay: &[T], - ) -> MouseCursor; + ) -> mouse::Interaction; } diff --git a/src/keyboard.rs b/src/keyboard.rs index 181dd974..0b3e894d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,6 +1,2 @@ //! Listen and react to keyboard events. -#[cfg(not(target_arch = "wasm32"))] -pub use iced_winit::input::keyboard::{KeyCode, ModifiersState}; - -#[cfg(target_arch = "wasm32")] -pub use iced_web::keyboard::{KeyCode, ModifiersState}; +pub use crate::runtime::keyboard::{Event, KeyCode, ModifiersState}; diff --git a/src/lib.rs b/src/lib.rs index 4f66cc73..2c08268b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,10 +185,18 @@ mod sandbox; pub mod executor; pub mod keyboard; +pub mod mouse; pub mod settings; pub mod widget; pub mod window; +#[cfg(all( + any(feature = "tokio", feature = "async-std"), + not(target_arch = "wasm32") +))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))] +pub mod time; + #[doc(no_inline)] pub use widget::*; @@ -206,5 +214,5 @@ use iced_web as runtime; pub use runtime::{ futures, Align, Background, Color, Command, Font, HorizontalAlignment, - Length, Point, Size, Subscription, Vector, VerticalAlignment, + Length, Point, Rectangle, Size, Subscription, Vector, VerticalAlignment, }; diff --git a/src/mouse.rs b/src/mouse.rs new file mode 100644 index 00000000..d61ed09a --- /dev/null +++ b/src/mouse.rs @@ -0,0 +1,2 @@ +//! Listen and react to mouse events. +pub use crate::runtime::mouse::{Button, Event, Interaction, ScrollDelta}; diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 00000000..cd442461 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,14 @@ +//! Listen and react to time. +use crate::Subscription; + +/// Returns a [`Subscription`] that produces messages at a set interval. +/// +/// The first message is produced after a `duration`, and then continues to +/// produce more messages every `duration` after that. +/// +/// [`Subscription`]: ../subscription/struct.Subscription.html +pub fn every( + duration: std::time::Duration, +) -> Subscription { + iced_futures::time::every(duration) +} diff --git a/web/src/lib.rs b/web/src/lib.rs index 395c0a25..53b54b7e 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -74,8 +74,8 @@ pub use dodrio; pub use element::Element; pub use hasher::Hasher; pub use iced_core::{ - keyboard, Align, Background, Color, Font, HorizontalAlignment, Length, - Point, Size, Vector, VerticalAlignment, + keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment, + Length, Point, Rectangle, Size, Vector, VerticalAlignment, }; pub use iced_futures::{executor, futures, Command}; pub use subscription::Subscription; diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 46d9e624..e73227ef 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,5 +1,5 @@ use iced_native::{ - image, svg, Background, Color, Font, HorizontalAlignment, Point, Rectangle, + image, svg, Background, Color, Font, HorizontalAlignment, Rectangle, Size, Vector, VerticalAlignment, }; @@ -70,12 +70,22 @@ pub enum Primitive { /// The content of the clip content: Box, }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, /// A low-level primitive to render a mesh of triangles. /// /// It can be used to render many kinds of geometry freely. Mesh2D { - /// The top-left coordinate of the mesh - origin: Point, + /// The size of the drawable region of the mesh. + /// + /// Any geometry that falls out of this region will be clipped. + size: Size, /// The vertex and index buffers of the mesh buffers: triangle::Mesh2D, @@ -85,9 +95,6 @@ pub enum Primitive { /// This can be useful if you are implementing a widget where primitive /// generation is expensive. Cached { - /// The origin of the coordinate system of the cached primitives - origin: Point, - /// The cached primitive cache: Arc, }, diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs index ca9364c1..71b4af38 100644 --- a/wgpu/src/renderer.rs +++ b/wgpu/src/renderer.rs @@ -7,8 +7,7 @@ use crate::{ use crate::image::{self, Image}; use iced_native::{ - layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, - Widget, + layout, mouse, Background, Color, Layout, Point, Rectangle, Vector, Widget, }; mod widget; @@ -29,7 +28,7 @@ pub struct Renderer { struct Layer<'a> { bounds: Rectangle, quads: Vec, - meshes: Vec<(Point, &'a triangle::Mesh2D)>, + meshes: Vec<(Vector, Rectangle, &'a triangle::Mesh2D)>, text: Vec>, #[cfg(any(feature = "image", feature = "svg"))] @@ -48,6 +47,12 @@ impl<'a> Layer<'a> { images: Vec::new(), } } + + pub fn intersection(&self, rectangle: Rectangle) -> Option> { + let layer_bounds: Rectangle = self.bounds.into(); + + layer_bounds.intersection(&rectangle).map(Into::into) + } } impl Renderer { @@ -88,10 +93,10 @@ impl Renderer { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: Target<'_>, - (primitive, mouse_cursor): &(Primitive, MouseCursor), + (primitive, mouse_interaction): &(Primitive, mouse::Interaction), scale_factor: f64, overlay: &[T], - ) -> MouseCursor { + ) -> mouse::Interaction { log::debug!("Drawing"); let (width, height) = target.viewport.dimensions(); @@ -126,7 +131,7 @@ impl Renderer { #[cfg(any(feature = "image", feature = "svg"))] self.image_pipeline.trim_cache(); - *mouse_cursor + *mouse_interaction } fn draw_primitive<'a>( @@ -214,10 +219,20 @@ impl Renderer { border_color: border_color.into_linear(), }); } - Primitive::Mesh2D { origin, buffers } => { + Primitive::Mesh2D { size, buffers } => { let layer = layers.last_mut().unwrap(); - layer.meshes.push((*origin + translation, buffers)); + // Only draw visible content + if let Some(clip_bounds) = layer.intersection(Rectangle::new( + Point::new(translation.x, translation.y), + *size, + )) { + layer.meshes.push(( + translation, + clip_bounds.into(), + buffers, + )); + } } Primitive::Clip { bounds, @@ -226,16 +241,10 @@ impl Renderer { } => { let layer = layers.last_mut().unwrap(); - let layer_bounds: Rectangle = layer.bounds.into(); - - let clip = Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..*bounds - }; - // Only draw visible content - if let Some(clip_bounds) = layer_bounds.intersection(&clip) { + if let Some(clip_bounds) = + layer.intersection(*bounds + translation) + { let clip_layer = Layer::new(clip_bounds.into()); let new_layer = Layer::new(layer.bounds); @@ -249,15 +258,21 @@ impl Renderer { layers.push(new_layer); } } - - Primitive::Cached { origin, cache } => { + Primitive::Translate { + translation: new_translation, + content, + } => { self.draw_primitive( - translation + Vector::new(origin.x, origin.y), - &cache, + translation + *new_translation, + &content, layers, ); } + Primitive::Cached { cache } => { + self.draw_primitive(translation, &cache, layers); + } + #[cfg(feature = "image")] Primitive::Image { handle, bounds } => { let layer = layers.last_mut().unwrap(); @@ -362,8 +377,8 @@ impl Renderer { target_width, target_height, scaled, + scale_factor, &layer.meshes, - bounds, ); } @@ -437,7 +452,7 @@ impl Renderer { } impl iced_native::Renderer for Renderer { - type Output = (Primitive, MouseCursor); + type Output = (Primitive, mouse::Interaction); type Defaults = Defaults; fn layout<'a, Message>( diff --git a/wgpu/src/renderer/widget/button.rs b/wgpu/src/renderer/widget/button.rs index 359b4866..eb225038 100644 --- a/wgpu/src/renderer/widget/button.rs +++ b/wgpu/src/renderer/widget/button.rs @@ -1,6 +1,6 @@ use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; use iced_native::{ - Background, Color, Element, Layout, MouseCursor, Point, Rectangle, Vector, + mouse, Background, Color, Element, Layout, Point, Rectangle, Vector, }; impl iced_native::button::Renderer for Renderer { @@ -84,9 +84,9 @@ impl iced_native::button::Renderer for Renderer { content }, if is_mouse_over { - MouseCursor::Pointer + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/wgpu/src/renderer/widget/checkbox.rs b/wgpu/src/renderer/widget/checkbox.rs index c0f1bf21..0340bf62 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/wgpu/src/renderer/widget/checkbox.rs @@ -1,6 +1,6 @@ use crate::{checkbox::StyleSheet, Primitive, Renderer}; use iced_native::{ - checkbox, HorizontalAlignment, MouseCursor, Rectangle, VerticalAlignment, + checkbox, mouse, HorizontalAlignment, Rectangle, VerticalAlignment, }; impl checkbox::Renderer for Renderer { @@ -54,9 +54,9 @@ impl checkbox::Renderer for Renderer { }, }, if is_mouse_over { - MouseCursor::Pointer + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/wgpu/src/renderer/widget/column.rs b/wgpu/src/renderer/widget/column.rs index 95a7463a..b853276d 100644 --- a/wgpu/src/renderer/widget/column.rs +++ b/wgpu/src/renderer/widget/column.rs @@ -1,5 +1,5 @@ use crate::{Primitive, Renderer}; -use iced_native::{column, Element, Layout, MouseCursor, Point}; +use iced_native::{column, mouse, Element, Layout, Point}; impl column::Renderer for Renderer { fn draw( @@ -9,7 +9,7 @@ impl column::Renderer for Renderer { layout: Layout<'_>, cursor_position: Point, ) -> Self::Output { - let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut mouse_interaction = mouse::Interaction::default(); ( Primitive::Group { @@ -17,18 +17,18 @@ impl column::Renderer for Renderer { .iter() .zip(layout.children()) .map(|(child, layout)| { - let (primitive, new_mouse_cursor) = + let (primitive, new_mouse_interaction) = child.draw(self, defaults, layout, cursor_position); - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; } primitive }) .collect(), }, - mouse_cursor, + mouse_interaction, ) } } diff --git a/wgpu/src/renderer/widget/container.rs b/wgpu/src/renderer/widget/container.rs index dda7dc8a..30cc3f07 100644 --- a/wgpu/src/renderer/widget/container.rs +++ b/wgpu/src/renderer/widget/container.rs @@ -21,7 +21,7 @@ impl iced_native::container::Renderer for Renderer { }, }; - let (content, mouse_cursor) = + let (content, mouse_interaction) = content.draw(self, &defaults, content_layout, cursor_position); if style.background.is_some() || style.border_width > 0 { @@ -39,10 +39,10 @@ impl iced_native::container::Renderer for Renderer { Primitive::Group { primitives: vec![quad, content], }, - mouse_cursor, + mouse_interaction, ) } else { - (content, mouse_cursor) + (content, mouse_interaction) } } } diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs index 70dc5d97..c4c04984 100644 --- a/wgpu/src/renderer/widget/image.rs +++ b/wgpu/src/renderer/widget/image.rs @@ -1,5 +1,5 @@ use crate::{Primitive, Renderer}; -use iced_native::{image, Layout, MouseCursor}; +use iced_native::{image, mouse, Layout}; impl image::Renderer for Renderer { fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { @@ -16,7 +16,7 @@ impl image::Renderer for Renderer { handle, bounds: layout.bounds(), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/pane_grid.rs b/wgpu/src/renderer/widget/pane_grid.rs index 2d201fec..2253e4af 100644 --- a/wgpu/src/renderer/widget/pane_grid.rs +++ b/wgpu/src/renderer/widget/pane_grid.rs @@ -1,7 +1,8 @@ use crate::{Primitive, Renderer}; use iced_native::{ + mouse, pane_grid::{self, Axis, Pane}, - Element, Layout, MouseCursor, Point, Rectangle, Vector, + Element, Layout, Point, Rectangle, Vector, }; impl pane_grid::Renderer for Renderer { @@ -22,7 +23,7 @@ impl pane_grid::Renderer for Renderer { cursor_position }; - let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut mouse_interaction = mouse::Interaction::default(); let mut dragged_pane = None; let mut panes: Vec<_> = content @@ -30,11 +31,11 @@ impl pane_grid::Renderer for Renderer { .zip(layout.children()) .enumerate() .map(|(i, ((id, pane), layout))| { - let (primitive, new_mouse_cursor) = + let (primitive, new_mouse_interaction) = pane.draw(self, defaults, layout, pane_cursor_position); - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; } if Some(*id) == dragging { @@ -59,12 +60,12 @@ impl pane_grid::Renderer for Renderer { height: bounds.height + 0.5, }, offset: Vector::new(0, 0), - content: Box::new(Primitive::Cached { - origin: Point::new( + content: Box::new(Primitive::Translate { + translation: Vector::new( cursor_position.x - bounds.x - bounds.width / 2.0, cursor_position.y - bounds.y - bounds.height / 2.0, ), - cache: std::sync::Arc::new(pane), + content: Box::new(pane), }), }; @@ -78,14 +79,14 @@ impl pane_grid::Renderer for Renderer { ( Primitive::Group { primitives }, if dragging.is_some() { - MouseCursor::Grabbing + mouse::Interaction::Grabbing } else if let Some(axis) = resizing { match axis { - Axis::Horizontal => MouseCursor::ResizingVertically, - Axis::Vertical => MouseCursor::ResizingHorizontally, + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, } } else { - mouse_cursor + mouse_interaction }, ) } diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/wgpu/src/renderer/widget/progress_bar.rs index 34e33276..2baeeb14 100644 --- a/wgpu/src/renderer/widget/progress_bar.rs +++ b/wgpu/src/renderer/widget/progress_bar.rs @@ -1,5 +1,5 @@ use crate::{progress_bar::StyleSheet, Primitive, Renderer}; -use iced_native::{progress_bar, Color, MouseCursor, Rectangle}; +use iced_native::{mouse, progress_bar, Color, Rectangle}; impl progress_bar::Renderer for Renderer { type Style = Box; @@ -48,7 +48,7 @@ impl progress_bar::Renderer for Renderer { } else { background }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/radio.rs b/wgpu/src/renderer/widget/radio.rs index 564f066b..2f1461db 100644 --- a/wgpu/src/renderer/widget/radio.rs +++ b/wgpu/src/renderer/widget/radio.rs @@ -1,5 +1,5 @@ use crate::{radio::StyleSheet, Primitive, Renderer}; -use iced_native::{radio, Background, Color, MouseCursor, Rectangle}; +use iced_native::{mouse, radio, Background, Color, Rectangle}; const SIZE: f32 = 28.0; const DOT_SIZE: f32 = SIZE / 2.0; @@ -55,9 +55,9 @@ impl radio::Renderer for Renderer { }, }, if is_mouse_over { - MouseCursor::Pointer + mouse::Interaction::Pointer } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/wgpu/src/renderer/widget/row.rs b/wgpu/src/renderer/widget/row.rs index bd9f1a04..d0b7ef09 100644 --- a/wgpu/src/renderer/widget/row.rs +++ b/wgpu/src/renderer/widget/row.rs @@ -1,5 +1,5 @@ use crate::{Primitive, Renderer}; -use iced_native::{row, Element, Layout, MouseCursor, Point}; +use iced_native::{mouse, row, Element, Layout, Point}; impl row::Renderer for Renderer { fn draw( @@ -9,7 +9,7 @@ impl row::Renderer for Renderer { layout: Layout<'_>, cursor_position: Point, ) -> Self::Output { - let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut mouse_interaction = mouse::Interaction::default(); ( Primitive::Group { @@ -17,18 +17,18 @@ impl row::Renderer for Renderer { .iter() .zip(layout.children()) .map(|(child, layout)| { - let (primitive, new_mouse_cursor) = + let (primitive, new_mouse_interaction) = child.draw(self, defaults, layout, cursor_position); - if new_mouse_cursor > mouse_cursor { - mouse_cursor = new_mouse_cursor; + if new_mouse_interaction > mouse_interaction { + mouse_interaction = new_mouse_interaction; } primitive }) .collect(), }, - mouse_cursor, + mouse_interaction, ) } } diff --git a/wgpu/src/renderer/widget/scrollable.rs b/wgpu/src/renderer/widget/scrollable.rs index 732523e3..8a400b82 100644 --- a/wgpu/src/renderer/widget/scrollable.rs +++ b/wgpu/src/renderer/widget/scrollable.rs @@ -1,7 +1,5 @@ use crate::{Primitive, Renderer}; -use iced_native::{ - scrollable, Background, Color, MouseCursor, Rectangle, Vector, -}; +use iced_native::{mouse, scrollable, Background, Color, Rectangle, Vector}; const SCROLLBAR_WIDTH: u16 = 10; const SCROLLBAR_MARGIN: u16 = 2; @@ -56,7 +54,7 @@ impl scrollable::Renderer for Renderer { scrollbar: Option, offset: u32, style_sheet: &Self::Style, - (content, mouse_cursor): Self::Output, + (content, mouse_interaction): Self::Output, ) -> Self::Output { ( if let Some(scrollbar) = scrollbar { @@ -118,9 +116,9 @@ impl scrollable::Renderer for Renderer { content }, if is_mouse_over_scrollbar || state.is_scroller_grabbed() { - MouseCursor::Idle + mouse::Interaction::Idle } else { - mouse_cursor + mouse_interaction }, ) } diff --git a/wgpu/src/renderer/widget/slider.rs b/wgpu/src/renderer/widget/slider.rs index c8ebd0da..220feace 100644 --- a/wgpu/src/renderer/widget/slider.rs +++ b/wgpu/src/renderer/widget/slider.rs @@ -2,7 +2,7 @@ use crate::{ slider::{HandleShape, StyleSheet}, Primitive, Renderer, }; -use iced_native::{slider, Background, Color, MouseCursor, Point, Rectangle}; +use iced_native::{mouse, slider, Background, Color, Point, Rectangle}; const HANDLE_HEIGHT: f32 = 22.0; @@ -95,11 +95,11 @@ impl slider::Renderer for Renderer { primitives: vec![rail_top, rail_bottom, handle], }, if is_dragging { - MouseCursor::Grabbing + mouse::Interaction::Grabbing } else if is_mouse_over { - MouseCursor::Grab + mouse::Interaction::Grab } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs index 28e05437..225f7e6c 100644 --- a/wgpu/src/renderer/widget/space.rs +++ b/wgpu/src/renderer/widget/space.rs @@ -1,8 +1,8 @@ use crate::{Primitive, Renderer}; -use iced_native::{space, MouseCursor, Rectangle}; +use iced_native::{mouse, space, Rectangle}; impl space::Renderer for Renderer { fn draw(&mut self, _bounds: Rectangle) -> Self::Output { - (Primitive::None, MouseCursor::OutOfBounds) + (Primitive::None, mouse::Interaction::default()) } } diff --git a/wgpu/src/renderer/widget/svg.rs b/wgpu/src/renderer/widget/svg.rs index 67bc3fe1..f6d6d0ba 100644 --- a/wgpu/src/renderer/widget/svg.rs +++ b/wgpu/src/renderer/widget/svg.rs @@ -1,5 +1,5 @@ use crate::{Primitive, Renderer}; -use iced_native::{svg, Layout, MouseCursor}; +use iced_native::{mouse, svg, Layout}; impl svg::Renderer for Renderer { fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { @@ -16,7 +16,7 @@ impl svg::Renderer for Renderer { handle, bounds: layout.bounds(), }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/text.rs b/wgpu/src/renderer/widget/text.rs index f27cc430..4605ed06 100644 --- a/wgpu/src/renderer/widget/text.rs +++ b/wgpu/src/renderer/widget/text.rs @@ -1,6 +1,6 @@ use crate::{Primitive, Renderer}; use iced_native::{ - text, Color, Font, HorizontalAlignment, MouseCursor, Rectangle, Size, + mouse, text, Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment, }; @@ -55,7 +55,7 @@ impl text::Renderer for Renderer { horizontal_alignment, vertical_alignment, }, - MouseCursor::OutOfBounds, + mouse::Interaction::default(), ) } } diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 6f72db68..57be6692 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -1,9 +1,10 @@ use crate::{text_input::StyleSheet, Primitive, Renderer}; use iced_native::{ + mouse, text_input::{self, cursor}, - Background, Color, Font, HorizontalAlignment, MouseCursor, Point, - Rectangle, Size, Vector, VerticalAlignment, + Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size, + Vector, VerticalAlignment, }; use std::f32; @@ -232,9 +233,9 @@ impl text_input::Renderer for Renderer { primitives: vec![input, contents], }, if is_mouse_over { - MouseCursor::Text + mouse::Interaction::Text } else { - MouseCursor::OutOfBounds + mouse::Interaction::default() }, ) } diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 86c74fcd..3e68a269 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,14 +1,14 @@ //! Draw meshes of triangles. use crate::{settings, Transformation}; -use iced_native::{Point, Rectangle}; +use iced_native::{Rectangle, Vector}; use std::mem; use zerocopy::AsBytes; mod msaa; const UNIFORM_BUFFER_SIZE: usize = 100; -const VERTEX_BUFFER_SIZE: usize = 100_000; -const INDEX_BUFFER_SIZE: usize = 100_000; +const VERTEX_BUFFER_SIZE: usize = 10_000; +const INDEX_BUFFER_SIZE: usize = 10_000; #[derive(Debug)] pub(crate) struct Pipeline { @@ -201,15 +201,15 @@ impl Pipeline { target_width: u32, target_height: u32, transformation: Transformation, - meshes: &[(Point, &Mesh2D)], - bounds: Rectangle, + scale_factor: f32, + meshes: &[(Vector, Rectangle, &Mesh2D)], ) { // This looks a bit crazy, but we are just counting how many vertices // and indices we will need to handle. // TODO: Improve readability let (total_vertices, total_indices) = meshes .iter() - .map(|(_, mesh)| (mesh.vertices.len(), mesh.indices.len())) + .map(|(_, _, mesh)| (mesh.vertices.len(), mesh.indices.len())) .fold((0, 0), |(total_v, total_i), (v, i)| { (total_v + v, total_i + i) }); @@ -230,12 +230,10 @@ impl Pipeline { let mut last_index = 0; // We upload everything upfront - for (origin, mesh) in meshes { - let transform = Uniforms { - transform: (transformation - * Transformation::translate(origin.x, origin.y)) - .into(), - }; + for (origin, _, mesh) in meshes { + let transform = (transformation + * Transformation::translate(origin.x, origin.y)) + .into(); let vertex_buffer = device.create_buffer_with_data( mesh.vertices.as_bytes(), @@ -318,16 +316,19 @@ impl Pipeline { }); render_pass.set_pipeline(&self.pipeline); - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); for (i, (vertex_offset, index_offset, indices)) in offsets.into_iter().enumerate() { + let bounds = meshes[i].1 * scale_factor; + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + render_pass.set_bind_group( 0, &self.constants, @@ -361,12 +362,28 @@ impl Pipeline { #[derive(Debug, Clone, Copy, AsBytes)] struct Uniforms { transform: [f32; 16], + // We need to align this to 256 bytes to please `wgpu`... + // TODO: Be smarter and stop wasting memory! + _padding_a: [f32; 32], + _padding_b: [f32; 16], } impl Default for Uniforms { fn default() -> Self { Self { transform: *Transformation::identity().as_ref(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], + } + } +} + +impl From for Uniforms { + fn from(transformation: Transformation) -> Uniforms { + Self { + transform: transformation.into(), + _padding_a: [0.0; 32], + _padding_b: [0.0; 16], } } } diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs index 325f90ce..2fc10ea0 100644 --- a/wgpu/src/widget/canvas.rs +++ b/wgpu/src/widget/canvas.rs @@ -9,35 +9,38 @@ use crate::{Defaults, Primitive, Renderer}; use iced_native::{ - layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget, + layout, mouse, Clipboard, Element, Hasher, Layout, Length, Point, Size, + Vector, Widget, }; use std::hash::Hash; +use std::marker::PhantomData; -pub mod layer; pub mod path; -mod drawable; +mod cache; +mod cursor; +mod event; mod fill; mod frame; +mod geometry; +mod program; mod stroke; mod text; -pub use drawable::Drawable; +pub use cache::Cache; +pub use cursor::Cursor; +pub use event::Event; pub use fill::Fill; pub use frame::Frame; -pub use layer::Layer; +pub use geometry::Geometry; pub use path::Path; +pub use program::Program; pub use stroke::{LineCap, LineJoin, Stroke}; pub use text::Text; /// A widget capable of drawing 2D graphics. /// -/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the -/// painter's algorithm. In other words, layers will be drawn on top of each -/// other in the same order they are pushed into the [`Canvas`]. -/// /// [`Canvas`]: struct.Canvas.html -/// [`Layer`]: layer/trait.Layer.html /// /// # Examples /// The repository has a couple of [examples] showcasing how to use a @@ -45,12 +48,15 @@ pub use text::Text; /// /// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock /// and its hands to display the current time. +/// - [`game_of_life`], an interactive version of the Game of Life, invented by +/// John Conway. /// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget /// and showcasing how to compose different transforms. /// -/// [examples]: https://github.com/hecrj/iced/tree/0.1/examples -/// [`clock`]: https://github.com/hecrj/iced/tree/0.1/examples/clock -/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.1/examples/solar_system +/// [examples]: https://github.com/hecrj/iced/tree/master/examples +/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock +/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life +/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system /// /// ## Drawing a simple circle /// If you want to get a quick overview, here's how we can draw a simple circle: @@ -58,10 +64,10 @@ pub use text::Text; /// ```no_run /// # mod iced { /// # pub use iced_wgpu::canvas; -/// # pub use iced_native::Color; +/// # pub use iced_native::{Color, Rectangle}; /// # } -/// use iced::canvas::{self, layer, Canvas, Drawable, Fill, Frame, Path}; -/// use iced::Color; +/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle}; /// /// // First, we define the data we need for drawing /// #[derive(Debug)] @@ -69,43 +75,46 @@ pub use text::Text; /// radius: f32, /// } /// -/// // Then, we implement the `Drawable` trait -/// impl Drawable for Circle { -/// fn draw(&self, frame: &mut Frame) { +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// /// // We create a `Path` representing a simple circle -/// let circle = Path::new(|p| p.circle(frame.center(), self.radius)); +/// let circle = Path::circle(frame.center(), self.radius); /// /// // And fill it with some color /// frame.fill(&circle, Fill::Color(Color::BLACK)); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] /// } /// } /// -/// // We can use a `Cache` to avoid unnecessary re-tessellation -/// let cache: layer::Cache = layer::Cache::new(); -/// -/// // Finally, we simply provide the data to our `Cache` and push the resulting -/// // layer into a `Canvas` -/// let canvas = Canvas::new() -/// .push(cache.with(&Circle { radius: 50.0 })); +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); /// ``` #[derive(Debug)] -pub struct Canvas<'a> { +pub struct Canvas> { width: Length, height: Length, - layers: Vec>, + program: P, + phantom: PhantomData, } -impl<'a> Canvas<'a> { +impl> Canvas { const DEFAULT_SIZE: u16 = 100; - /// Creates a new [`Canvas`] with no layers. + /// Creates a new [`Canvas`]. /// /// [`Canvas`]: struct.Canvas.html - pub fn new() -> Self { + pub fn new(program: P) -> Self { Canvas { width: Length::Units(Self::DEFAULT_SIZE), height: Length::Units(Self::DEFAULT_SIZE), - layers: Vec::new(), + program, + phantom: PhantomData, } } @@ -124,20 +133,11 @@ impl<'a> Canvas<'a> { self.height = height; self } - - /// Adds a [`Layer`] to the [`Canvas`]. - /// - /// It will be drawn on top of previous layers. - /// - /// [`Layer`]: layer/trait.Layer.html - /// [`Canvas`]: struct.Canvas.html - pub fn push(mut self, layer: impl Layer + 'a) -> Self { - self.layers.push(Box::new(layer)); - self - } } -impl<'a, Message> Widget for Canvas<'a> { +impl> Widget + for Canvas +{ fn width(&self) -> Length { self.width } @@ -157,45 +157,77 @@ impl<'a, Message> Widget for Canvas<'a> { layout::Node::new(size) } + fn on_event( + &mut self, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + messages: &mut Vec, + _renderer: &Renderer, + _clipboard: Option<&dyn Clipboard>, + ) { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + if let Some(message) = + self.program.update(canvas_event, bounds, cursor) + { + messages.push(message); + } + } + } + fn draw( &self, _renderer: &mut Renderer, _defaults: &Defaults, layout: Layout<'_>, - _cursor_position: Point, - ) -> (Primitive, MouseCursor) { + cursor_position: Point, + ) -> (Primitive, mouse::Interaction) { let bounds = layout.bounds(); - let origin = Point::new(bounds.x, bounds.y); - let size = Size::new(bounds.width, bounds.height); + let translation = Vector::new(bounds.x, bounds.y); + let cursor = Cursor::from_window_position(cursor_position); ( - Primitive::Group { - primitives: self - .layers - .iter() - .map(|layer| Primitive::Cached { - origin, - cache: layer.draw(size), - }) - .collect(), + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { + primitives: self + .program + .draw(bounds, cursor) + .into_iter() + .map(Geometry::into_primitive) + .collect(), + }), }, - MouseCursor::Idle, + self.program.mouse_interaction(bounds, cursor), ) } fn hash_layout(&self, state: &mut Hasher) { - std::any::TypeId::of::>().hash(state); + struct Marker; + std::any::TypeId::of::().hash(state); self.width.hash(state); self.height.hash(state); } } -impl<'a, Message> From> for Element<'a, Message, Renderer> +impl<'a, Message, P: Program + 'a> From> + for Element<'a, Message, Renderer> where Message: 'static, { - fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> { + fn from(canvas: Canvas) -> Element<'a, Message, Renderer> { Element::new(canvas) } } diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs new file mode 100644 index 00000000..4b28d164 --- /dev/null +++ b/wgpu/src/widget/canvas/cache.rs @@ -0,0 +1,108 @@ +use crate::{ + canvas::{Frame, Geometry}, + Primitive, +}; + +use iced_native::Size; +use std::{cell::RefCell, sync::Arc}; + +enum State { + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Default for State { + fn default() -> Self { + State::Empty + } +} +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +/// +/// [`Layer`]: ../trait.Layer.html +/// [`Cache`]: struct.Cache.html +/// [`Geometry`]: struct.Geometry.html +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + /// + /// [`Cache`]: struct.Cache.html + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + /// + /// [`Cache`]: struct.Cache.html + pub fn clear(&mut self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + /// + /// [`Cache`]: struct.Cache.html + pub fn draw(&self, bounds: Size, draw_fn: impl Fn(&mut Frame)) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry::from_primitive(Primitive::Cached { + cache: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.into_primitive()) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry::from_primitive(Primitive::Cached { cache: primitive }) + } +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Empty => write!(f, "Empty"), + State::Filled { primitive, bounds } => f + .debug_struct("Filled") + .field("primitive", primitive) + .field("bounds", bounds) + .finish(), + } + } +} diff --git a/wgpu/src/widget/canvas/cursor.rs b/wgpu/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..456760ea --- /dev/null +++ b/wgpu/src/widget/canvas/cursor.rs @@ -0,0 +1,72 @@ +use iced_native::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + /// + /// [`Cursor`]: enum.Cursor.html + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/wgpu/src/widget/canvas/drawable.rs b/wgpu/src/widget/canvas/drawable.rs deleted file mode 100644 index 6c74071c..00000000 --- a/wgpu/src/widget/canvas/drawable.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::canvas::Frame; - -/// A type that can be drawn on a [`Frame`]. -/// -/// [`Frame`]: struct.Frame.html -pub trait Drawable { - /// Draws the [`Drawable`] on the given [`Frame`]. - /// - /// [`Drawable`]: trait.Drawable.html - /// [`Frame`]: struct.Frame.html - fn draw(&self, frame: &mut Frame); -} diff --git a/wgpu/src/widget/canvas/event.rs b/wgpu/src/widget/canvas/event.rs new file mode 100644 index 00000000..ad11f51e --- /dev/null +++ b/wgpu/src/widget/canvas/event.rs @@ -0,0 +1,10 @@ +use iced_native::mouse; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: struct.Event.html +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs index de4717f1..5262ab4e 100644 --- a/wgpu/src/widget/canvas/frame.rs +++ b/wgpu/src/widget/canvas/frame.rs @@ -1,7 +1,7 @@ use iced_native::{Point, Rectangle, Size, Vector}; use crate::{ - canvas::{Fill, Path, Stroke, Text}, + canvas::{Fill, Geometry, Path, Stroke, Text}, triangle, Primitive, }; @@ -10,8 +10,7 @@ use crate::{ /// [`Canvas`]: struct.Canvas.html #[derive(Debug)] pub struct Frame { - width: f32, - height: f32, + size: Size, buffers: lyon::tessellation::VertexBuffers, primitives: Vec, transforms: Transforms, @@ -36,10 +35,9 @@ impl Frame { /// top-left corner of its bounds. /// /// [`Frame`]: struct.Frame.html - pub fn new(width: f32, height: f32) -> Frame { + pub fn new(size: Size) -> Frame { Frame { - width, - height, + size, buffers: lyon::tessellation::VertexBuffers::new(), primitives: Vec::new(), transforms: Transforms { @@ -57,7 +55,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn width(&self) -> f32 { - self.width + self.size.width } /// Returns the width of the [`Frame`]. @@ -65,7 +63,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn height(&self) -> f32 { - self.height + self.size.height } /// Returns the dimensions of the [`Frame`]. @@ -73,7 +71,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn size(&self) -> Size { - Size::new(self.width, self.height) + self.size } /// Returns the coordinate of the center of the [`Frame`]. @@ -81,7 +79,7 @@ impl Frame { /// [`Frame`]: struct.Frame.html #[inline] pub fn center(&self) -> Point { - Point::new(self.width / 2.0, self.height / 2.0) + Point::new(self.size.width / 2.0, self.size.height / 2.0) } /// Draws the given [`Path`] on the [`Frame`] by filling it with the @@ -122,6 +120,43 @@ impl Frame { let _ = result.expect("Tessellate path"); } + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + /// + /// [`Frame`]: struct.Frame.html + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + use lyon::tessellation::{BuffersBuilder, FillOptions}; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(match fill.into() { + Fill::Color(color) => color.into_linear(), + }), + ); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let _ = lyon::tessellation::basic_shapes::fill_rectangle( + &lyon::math::Rect::new(top_left, size.into()), + &FillOptions::default(), + &mut buffers, + ) + .expect("Fill rectangle"); + } + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the /// provided style. /// @@ -262,13 +297,14 @@ impl Frame { self.transforms.current.is_identity = false; } - /// Produces the primitive representing everything drawn on the [`Frame`]. + /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. /// /// [`Frame`]: struct.Frame.html - pub fn into_primitive(mut self) -> Primitive { + /// [`Geometry`]: struct.Geometry.html + pub fn into_geometry(mut self) -> Geometry { if !self.buffers.indices.is_empty() { self.primitives.push(Primitive::Mesh2D { - origin: Point::ORIGIN, + size: self.size, buffers: triangle::Mesh2D { vertices: self.buffers.vertices, indices: self.buffers.indices, @@ -276,14 +312,28 @@ impl Frame { }); } - Primitive::Group { + Geometry::from_primitive(Primitive::Group { primitives: self.primitives, - } + }) } } struct FillVertex([f32; 4]); +impl lyon::tessellation::BasicVertexConstructor + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + impl lyon::tessellation::FillVertexConstructor for FillVertex { diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..4cadee39 --- /dev/null +++ b/wgpu/src/widget/canvas/geometry.rs @@ -0,0 +1,34 @@ +use crate::Primitive; + +/// A bunch of shapes that can be drawn. +/// +/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a +/// [`Cache`]. +/// +/// [`Geometry`]: struct.Geometry.html +/// [`Frame`]: struct.Frame.html +/// [`Cache`]: struct.Cache.html +#[derive(Debug, Clone)] +pub struct Geometry(Primitive); + +impl Geometry { + pub(crate) fn from_primitive(primitive: Primitive) -> Self { + Self(primitive) + } + + /// Turns the [`Geometry`] into a [`Primitive`]. + /// + /// This can be useful if you are building a custom widget. + /// + /// [`Geometry`]: struct.Geometry.html + /// [`Primitive`]: ../enum.Primitive.html + pub fn into_primitive(self) -> Primitive { + self.0 + } +} + +impl From for Primitive { + fn from(geometry: Geometry) -> Primitive { + geometry.0 + } +} diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs deleted file mode 100644 index a46b7fb1..00000000 --- a/wgpu/src/widget/canvas/layer.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Produce, store, and reuse geometry. -mod cache; - -pub use cache::Cache; - -use crate::Primitive; -use iced_native::Size; - -use std::sync::Arc; - -/// A layer that can be presented at a [`Canvas`]. -/// -/// [`Canvas`]: ../struct.Canvas.html -pub trait Layer: std::fmt::Debug { - /// Draws the [`Layer`] in the given bounds and produces a [`Primitive`] as - /// a result. - /// - /// The [`Layer`] may choose to store the produced [`Primitive`] locally and - /// only recompute it when the bounds change, its contents change, or is - /// otherwise explicitly cleared by other means. - /// - /// [`Layer`]: trait.Layer.html - /// [`Primitive`]: ../../../enum.Primitive.html - fn draw(&self, bounds: Size) -> Arc; -} diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs deleted file mode 100644 index 4f8c2bec..00000000 --- a/wgpu/src/widget/canvas/layer/cache.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::{ - canvas::{Drawable, Frame, Layer}, - Primitive, -}; - -use iced_native::Size; -use std::{cell::RefCell, marker::PhantomData, sync::Arc}; - -enum State { - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -impl Default for State { - fn default() -> Self { - State::Empty - } -} -/// A simple cache that stores generated geometry to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -/// -/// [`Layer`]: ../trait.Layer.html -/// [`Cache`]: struct.Cache.html -#[derive(Debug)] -pub struct Cache { - input: PhantomData, - state: RefCell, -} - -impl Default for Cache -where - T: Drawable, -{ - fn default() -> Self { - Self { - input: PhantomData, - state: Default::default(), - } - } -} - -impl Cache -where - T: Drawable + std::fmt::Debug, -{ - /// Creates a new empty [`Cache`]. - /// - /// [`Cache`]: struct.Cache.html - pub fn new() -> Self { - Cache { - input: PhantomData, - state: Default::default(), - } - } - - /// Clears the cache, forcing a redraw the next time it is used. - /// - /// [`Cached`]: struct.Cached.html - pub fn clear(&mut self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Binds the [`Cache`] with some data, producing a [`Layer`] that can be - /// added to a [`Canvas`]. - /// - /// [`Cache`]: struct.Cache.html - /// [`Layer`]: ../trait.Layer.html - /// [`Canvas`]: ../../struct.Canvas.html - pub fn with<'a>(&'a self, input: &'a T) -> impl Layer + 'a { - Bind { - cache: self, - input: input, - } - } -} - -#[derive(Debug)] -struct Bind<'a, T: Drawable> { - cache: &'a Cache, - input: &'a T, -} - -impl<'a, T> Layer for Bind<'a, T> -where - T: Drawable + std::fmt::Debug, -{ - fn draw(&self, current_bounds: Size) -> Arc { - use std::ops::Deref; - - if let State::Filled { bounds, primitive } = - self.cache.state.borrow().deref() - { - if *bounds == current_bounds { - return primitive.clone(); - } - } - - let mut frame = Frame::new(current_bounds.width, current_bounds.height); - self.input.draw(&mut frame); - - let primitive = Arc::new(frame.into_primitive()); - - *self.cache.state.borrow_mut() = State::Filled { - bounds: current_bounds, - primitive: primitive.clone(), - }; - - primitive - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/wgpu/src/widget/canvas/program.rs b/wgpu/src/widget/canvas/program.rs new file mode 100644 index 00000000..725d9d72 --- /dev/null +++ b/wgpu/src/widget/canvas/program.rs @@ -0,0 +1,85 @@ +use crate::canvas::{Cursor, Event, Geometry}; +use iced_native::{mouse, Rectangle}; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Program`]: trait.Program.html +pub trait Program { + /// Updates the state of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + /// [`Event`]: enum.Event.html + fn update( + &mut self, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> Option { + None + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Program`]: trait.Program.html + /// [`Geometry`]: struct.Geometry.html + /// [`Frame`]: struct.Frame.html + /// [`Cache`]: struct.Cache.html + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Program`]: trait.Program.html + /// [`Canvas`]: struct.Canvas.html + fn mouse_interaction( + &self, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &mut T +where + T: Program, +{ + fn update( + &mut self, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> Option { + T::update(self, event, bounds, cursor) + } + + fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec { + T::draw(self, bounds, cursor) + } + + fn mouse_interaction( + &self, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, bounds, cursor) + } +} diff --git a/wgpu/src/widget/canvas/stroke.rs b/wgpu/src/widget/canvas/stroke.rs index 46d669c4..5b6fc56a 100644 --- a/wgpu/src/widget/canvas/stroke.rs +++ b/wgpu/src/widget/canvas/stroke.rs @@ -14,6 +14,38 @@ pub struct Stroke { pub line_join: LineJoin, } +impl Stroke { + /// Sets the color of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_color(self, color: Color) -> Stroke { + Stroke { color, ..self } + } + + /// Sets the width of the [`Stroke`]. + /// + /// [`Stroke`]: struct.Stroke.html + pub fn with_width(self, width: f32) -> Stroke { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + /// + /// [`LineCap`]: enum.LineCap.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_cap(self, line_cap: LineCap) -> Stroke { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + /// + /// [`LineJoin`]: enum.LineJoin.html + /// [`Stroke`]: struct.Stroke.html + pub fn with_line_join(self, line_join: LineJoin) -> Stroke { + Stroke { line_join, ..self } + } +} + impl Default for Stroke { fn default() -> Stroke { Stroke { diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs index e1b77700..7e40ae0c 100644 --- a/wgpu/src/window/backend.rs +++ b/wgpu/src/window/backend.rs @@ -1,6 +1,6 @@ use crate::{window::SwapChain, Renderer, Settings, Target}; -use iced_native::{futures, MouseCursor}; +use iced_native::{futures, mouse}; use raw_window_handle::HasRawWindowHandle; /// A window graphics backend for iced powered by `wgpu`. @@ -78,7 +78,7 @@ impl iced_native::window::Backend for Backend { output: &::Output, scale_factor: f64, overlay: &[T], - ) -> MouseCursor { + ) -> mouse::Interaction { let (frame, viewport) = swap_chain.next_frame().expect("Next frame"); let mut encoder = self.device.create_command_encoder( @@ -101,7 +101,7 @@ impl iced_native::window::Backend for Backend { depth_stencil_attachment: None, }); - let mouse_cursor = renderer.draw( + let mouse_interaction = renderer.draw( &mut self.device, &mut encoder, Target { @@ -115,6 +115,6 @@ impl iced_native::window::Backend for Backend { self.queue.submit(&[encoder.finish()]); - mouse_cursor + mouse_interaction } } diff --git a/winit/src/application.rs b/winit/src/application.rs index b974711c..f6bc8fcc 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,6 @@ use crate::{ - conversion, size::Size, window, Cache, Clipboard, Command, Debug, Element, - Executor, Mode, MouseCursor, Proxy, Runtime, Settings, Subscription, + conversion, mouse, size::Size, window, Cache, Clipboard, Command, Debug, + Element, Executor, Mode, Proxy, Runtime, Settings, Subscription, UserInterface, }; @@ -205,7 +205,7 @@ pub trait Application: Sized { let mut cache = Some(user_interface.into_cache()); let mut events = Vec::new(); - let mut mouse_cursor = MouseCursor::OutOfBounds; + let mut mouse_interaction = mouse::Interaction::default(); let mut modifiers = winit::event::ModifiersState::default(); debug.startup_finished(); @@ -328,7 +328,7 @@ pub trait Application: Sized { resized = false; } - let new_mouse_cursor = backend.draw( + let new_mouse_interaction = backend.draw( &mut renderer, &mut swap_chain, &primitive, @@ -338,12 +338,12 @@ pub trait Application: Sized { debug.render_finished(); - if new_mouse_cursor != mouse_cursor { - window.set_cursor_icon(conversion::mouse_cursor( - new_mouse_cursor, + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, )); - mouse_cursor = new_mouse_cursor; + mouse_interaction = new_mouse_interaction; } // TODO: Handle animations! diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index eaa26ace..b887db6e 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -3,11 +3,8 @@ //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native use crate::{ - input::{ - keyboard::{self, KeyCode, ModifiersState}, - mouse, ButtonState, - }, - window, Event, Mode, MouseCursor, + keyboard::{self, KeyCode, ModifiersState}, + mouse, window, Event, Mode, }; /// Converts a winit window event into an iced event. @@ -36,9 +33,15 @@ pub fn window_event( })) } WindowEvent::MouseInput { button, state, .. } => { - Some(Event::Mouse(mouse::Event::Input { - button: mouse_button(*button), - state: button_state(*state), + let button = mouse_button(*button); + + Some(Event::Mouse(match state { + winit::event::ElementState::Pressed => { + mouse::Event::ButtonPressed(button) + } + winit::event::ElementState::Released => { + mouse::Event::ButtonReleased(button) + } })) } WindowEvent::MouseWheel { delta, .. } => match delta { @@ -70,10 +73,24 @@ pub fn window_event( .. }, .. - } => Some(Event::Keyboard(keyboard::Event::Input { - key_code: key_code(*virtual_keycode), - state: button_state(*state), - modifiers: modifiers_state(modifiers), + } => Some(Event::Keyboard({ + let key_code = key_code(*virtual_keycode); + let modifiers = modifiers_state(modifiers); + + match state { + winit::event::ElementState::Pressed => { + keyboard::Event::KeyPressed { + key_code, + modifiers, + } + } + winit::event::ElementState::Released => { + keyboard::Event::KeyReleased { + key_code, + modifiers, + } + } + } })), WindowEvent::HoveredFile(path) => { Some(Event::Window(window::Event::FileHovered(path.clone()))) @@ -108,19 +125,23 @@ pub fn fullscreen( /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon { - match mouse_cursor { - MouseCursor::OutOfBounds => winit::window::CursorIcon::Default, - MouseCursor::Idle => winit::window::CursorIcon::Default, - MouseCursor::Pointer => winit::window::CursorIcon::Hand, - MouseCursor::Working => winit::window::CursorIcon::Progress, - MouseCursor::Grab => winit::window::CursorIcon::Grab, - MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing, - MouseCursor::Text => winit::window::CursorIcon::Text, - MouseCursor::ResizingHorizontally => { +pub fn mouse_interaction( + interaction: mouse::Interaction, +) -> winit::window::CursorIcon { + use mouse::Interaction; + + match interaction { + Interaction::Idle => winit::window::CursorIcon::Default, + Interaction::Pointer => winit::window::CursorIcon::Hand, + Interaction::Working => winit::window::CursorIcon::Progress, + Interaction::Grab => winit::window::CursorIcon::Grab, + Interaction::Grabbing => winit::window::CursorIcon::Grabbing, + Interaction::Crosshair => winit::window::CursorIcon::Crosshair, + Interaction::Text => winit::window::CursorIcon::Text, + Interaction::ResizingHorizontally => { winit::window::CursorIcon::EwResize } - MouseCursor::ResizingVertically => winit::window::CursorIcon::NsResize, + Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, } } @@ -137,17 +158,6 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { } } -/// Converts an `ElementState` from [`winit`] to an [`iced_native`] button state. -/// -/// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native -pub fn button_state(element_state: winit::event::ElementState) -> ButtonState { - match element_state { - winit::event::ElementState::Pressed => ButtonState::Pressed, - winit::event::ElementState::Released => ButtonState::Released, - } -} - /// Converts some `ModifiersState` from [`winit`] to an [`iced_native`] /// modifiers state. ///