diff options
author | 2023-03-01 21:34:26 +0100 | |
---|---|---|
committer | 2023-03-01 21:34:26 +0100 | |
commit | 5fd5d1cdf8e5354788dc40729c4565ef377d3bba (patch) | |
tree | 0921efc7dc13a3050e03482147a791f85515f1f2 /renderer/src | |
parent | 3f6e28fa9b1b8d911f765c9efb5249a9e0c942d5 (diff) | |
download | iced-5fd5d1cdf8e5354788dc40729c4565ef377d3bba.tar.gz iced-5fd5d1cdf8e5354788dc40729c4565ef377d3bba.tar.bz2 iced-5fd5d1cdf8e5354788dc40729c4565ef377d3bba.zip |
Implement `Canvas` support for `iced_tiny_skia`
Diffstat (limited to 'renderer/src')
-rw-r--r-- | renderer/src/backend.rs | 4 | ||||
-rw-r--r-- | renderer/src/lib.rs | 14 | ||||
-rw-r--r-- | renderer/src/widget.rs | 12 | ||||
-rw-r--r-- | renderer/src/widget/canvas.rs | 177 | ||||
-rw-r--r-- | renderer/src/widget/canvas/cache.rs | 85 | ||||
-rw-r--r-- | renderer/src/widget/qr_code.rs | 301 |
6 files changed, 588 insertions, 5 deletions
diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index b0a409dc..6c0b4e5c 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -1,4 +1,4 @@ -use crate::{Font, Point, Size}; +use crate::{Font, Geometry, Point, Size}; use iced_graphics::backend; use iced_graphics::text; @@ -12,6 +12,8 @@ pub enum Backend { } impl iced_graphics::Backend for Backend { + type Geometry = Geometry; + fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index f9bfc373..d9c85e82 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -4,11 +4,14 @@ pub mod window; mod backend; mod settings; +pub use iced_graphics::primitive; + pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Point, Size, Viewport, + Antialiasing, Color, Error, Font, Point, Rectangle, Size, Vector, Viewport, }; /// The default graphics renderer for [`iced`]. @@ -16,3 +19,12 @@ pub use iced_graphics::{ /// [`iced`]: https://github.com/iced-rs/iced pub type Renderer<Theme = iced_native::Theme> = iced_graphics::Renderer<Backend, Theme>; + +#[derive(Debug, Clone)] +pub struct Geometry(pub(crate) Primitive); + +impl From<Geometry> for Primitive { + fn from(geometry: Geometry) -> Self { + geometry.0 + } +} diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs index 417cc06f..6c0c2a83 100644 --- a/renderer/src/widget.rs +++ b/renderer/src/widget.rs @@ -1,5 +1,11 @@ -#[cfg(feature = "qr_code")] -pub use iced_graphics::widget::qr_code; +#[cfg(feature = "canvas")] +pub mod canvas; #[cfg(feature = "canvas")] -pub use iced_graphics::widget::canvas; +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs new file mode 100644 index 00000000..f40a1097 --- /dev/null +++ b/renderer/src/widget/canvas.rs @@ -0,0 +1,177 @@ +mod cache; + +pub use cache::Cache; + +pub use iced_native::widget::canvas::event::{self, Event}; +pub use iced_native::widget::canvas::fill::{self, Fill}; +pub use iced_native::widget::canvas::gradient::{self, Gradient}; +pub use iced_native::widget::canvas::path::{self, Path}; +pub use iced_native::widget::canvas::stroke::{self, Stroke}; +pub use iced_native::widget::canvas::{ + Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, +}; + +use crate::{Backend, Point, Rectangle, Size, Vector}; + +pub use crate::Geometry; + +pub enum Frame { + Wgpu(iced_wgpu::widget::canvas::Frame), + TinySkia(iced_tiny_skia::canvas::Frame), +} + +macro_rules! delegate { + ($frame:expr, $name:ident => $body:expr) => { + match $frame { + Self::Wgpu($name) => $body, + Self::TinySkia($name) => $body, + } + }; +} + +impl Frame { + pub fn new<Theme>(renderer: &crate::Renderer<Theme>, size: Size) -> Self { + match renderer.backend() { + Backend::Wgpu(_) => { + Frame::Wgpu(iced_wgpu::widget::canvas::Frame::new(size)) + } + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) + } + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + delegate!(self, frame => frame.width()) + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + delegate!(self, frame => frame.height()) + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + delegate!(self, frame => frame.size()) + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + delegate!(self, frame => frame.center()) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { + delegate!(self, frame => frame.fill(path, fill)); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into<Fill>, + ) { + delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { + delegate!(self, frame => frame.stroke(path, stroke)); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into<Text>) { + delegate!(self, frame => frame.fill_text(text)); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + delegate!(self, frame => frame.push_transform()); + + f(self); + + delegate!(self, frame => frame.pop_transform()); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = match self { + Self::Wgpu(_) => { + Self::Wgpu(iced_wgpu::widget::canvas::Frame::new(region.size())) + } + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::canvas::Frame::new(region.size()), + ), + }; + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + match (self, frame) { + (Self::Wgpu(target), Self::Wgpu(frame)) => { + target.clip(frame, translation); + } + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, translation); + } + _ => unreachable!(), + }; + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + delegate!(self, frame => frame.translate(translation)); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + delegate!(self, frame => frame.rotate(angle)); + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + delegate!(self, frame => frame.scale(scale)); + } + + pub fn into_geometry(self) -> Geometry { + Geometry(delegate!(self, frame => frame.into_primitive())) + } +} diff --git a/renderer/src/widget/canvas/cache.rs b/renderer/src/widget/canvas/cache.rs new file mode 100644 index 00000000..7d6b4811 --- /dev/null +++ b/renderer/src/widget/canvas/cache.rs @@ -0,0 +1,85 @@ +use crate::widget::canvas::{Frame, Geometry}; +use crate::{Primitive, Renderer, Size}; + +use std::cell::RefCell; +use std::sync::Arc; + +/// 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. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell<State>, +} + +#[derive(Debug, Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc<Primitive>, + }, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&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. + pub fn draw<Theme>( + &self, + renderer: &Renderer<Theme>, + bounds: Size, + draw_fn: impl FnOnce(&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(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.0) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry(Primitive::Cache { content: primitive }) + } +} diff --git a/renderer/src/widget/qr_code.rs b/renderer/src/widget/qr_code.rs new file mode 100644 index 00000000..aae4ec88 --- /dev/null +++ b/renderer/src/widget/qr_code.rs @@ -0,0 +1,301 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_graphics::renderer; + +use iced_native::layout; +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, + } + } +} |