diff options
author | 2023-03-03 04:57:55 +0100 | |
---|---|---|
committer | 2023-03-03 04:57:55 +0100 | |
commit | 6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f (patch) | |
tree | 7a9d57f52e3bee9f4d910c89178dc3e2917957a1 /src | |
parent | d13d19ba3569560edd67f20b48f37548d10ceee9 (diff) | |
download | iced-6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f.tar.gz iced-6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f.tar.bz2 iced-6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f.zip |
Move `Canvas` and `QRCode` to `iced` crate
Rename `canvas` modules to `geometry` in graphics subcrates
Diffstat (limited to 'src')
-rw-r--r-- | src/widget.rs | 8 | ||||
-rw-r--r-- | src/widget/canvas.rs | 238 | ||||
-rw-r--r-- | src/widget/canvas/cursor.rs | 64 | ||||
-rw-r--r-- | src/widget/canvas/event.rs | 21 | ||||
-rw-r--r-- | src/widget/canvas/program.rs | 108 | ||||
-rw-r--r-- | src/widget/qr_code.rs | 300 |
6 files changed, 735 insertions, 4 deletions
diff --git a/src/widget.rs b/src/widget.rs index f3a66101..19434e84 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -165,14 +165,14 @@ pub use vertical_slider::VerticalSlider; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub use iced_renderer::widget::canvas; +pub mod canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] /// Creates a new [`Canvas`]. -pub fn canvas<P, Message, Renderer>(program: P) -> Canvas<Message, Renderer, P> +pub fn canvas<P, Message, Renderer>(program: P) -> Canvas<P, Message, Renderer> where - Renderer: canvas::Renderer, + Renderer: iced_renderer::geometry::Renderer, P: canvas::Program<Message, Renderer>, { Canvas::new(program) @@ -193,7 +193,7 @@ pub mod image { #[cfg(feature = "qr_code")] #[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub use iced_renderer::widget::qr_code; +pub mod qr_code; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs new file mode 100644 index 00000000..bc5995c6 --- /dev/null +++ b/src/widget/canvas.rs @@ -0,0 +1,238 @@ +//! Draw 2D graphics for your users. +pub mod event; + +mod cursor; +mod program; + +pub use cursor::Cursor; +pub use event::Event; +pub use program::Program; + +pub use iced_renderer::geometry::*; + +use crate::{Length, Point, Rectangle, Size, Vector}; + +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; +use iced_native::{Clipboard, Element, Shell, Widget}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme, Renderer}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas<P, Message, Renderer = crate::Renderer> +where + Renderer: iced_renderer::geometry::Renderer, + P: Program<Message, Renderer>, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData<Message>, + theme_: PhantomData<Renderer>, +} + +impl<P, Message, Renderer> Canvas<P, Message, Renderer> +where + Renderer: iced_renderer::geometry::Renderer, + P: Program<Message, Renderer>, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } +} + +impl<P, Message, Renderer> Widget<Message, Renderer> + for Canvas<P, Message, Renderer> +where + Renderer: iced_renderer::geometry::Renderer, + P: Program<Message, Renderer>, +{ + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<P::State>>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + iced_native::Event::Touch(touch_event) => { + Some(Event::Touch(touch_event)) + } + iced_native::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::<P::State>(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, P, Message, Renderer> From<Canvas<P, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + iced_renderer::geometry::Renderer, + P: Program<Message, Renderer> + 'a, +{ + fn from( + canvas: Canvas<P, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/src/widget/canvas/cursor.rs b/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..ef6a7771 --- /dev/null +++ b/src/widget/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::{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. + pub fn position(&self) -> Option<Point> { + 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`. + pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> { + 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. + pub fn position_from(&self, origin: Point) -> Option<Point> { + 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. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/src/widget/canvas/event.rs b/src/widget/canvas/event.rs new file mode 100644 index 00000000..7c733a4d --- /dev/null +++ b/src/widget/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use iced_native::keyboard; +use iced_native::mouse; +use iced_native::touch; + +pub use iced_native::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/src/widget/canvas/program.rs b/src/widget/canvas/program.rs new file mode 100644 index 00000000..fd15663a --- /dev/null +++ b/src/widget/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::Cursor; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program<Message, Renderer = crate::Renderer> +where + Renderer: iced_renderer::geometry::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::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. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, 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`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Renderer::Geometry>; + + /// 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`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl<Message, Renderer, T> Program<Message, Renderer> for &T +where + Renderer: iced_renderer::geometry::Renderer, + T: Program<Message, Renderer>, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option<Message>) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec<Renderer::Geometry> { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/src/widget/qr_code.rs b/src/widget/qr_code.rs new file mode 100644 index 00000000..66442d5d --- /dev/null +++ b/src/widget/qr_code.rs @@ -0,0 +1,300 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_native::layout; +use iced_native::renderer; +use iced_native::widget::Tree; +use iced_native::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer<Theme>, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer<Theme>, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(geometry.0); + }); + } +} + +impl<'a, Message, Theme> From<QRCode<'a>> + for Element<'a, Message, Renderer<Theme>> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec<qrcode::Color>, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result<Self, Error> { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From<Version> for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From<ErrorCorrection> for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From<qrcode::types::QrError> for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} |