summaryrefslogtreecommitdiffstats
path: root/renderer/src
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-01 21:34:26 +0100
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-03-01 21:34:26 +0100
commit5fd5d1cdf8e5354788dc40729c4565ef377d3bba (patch)
tree0921efc7dc13a3050e03482147a791f85515f1f2 /renderer/src
parent3f6e28fa9b1b8d911f765c9efb5249a9e0c942d5 (diff)
downloadiced-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.rs4
-rw-r--r--renderer/src/lib.rs14
-rw-r--r--renderer/src/widget.rs12
-rw-r--r--renderer/src/widget/canvas.rs177
-rw-r--r--renderer/src/widget/canvas/cache.rs85
-rw-r--r--renderer/src/widget/qr_code.rs301
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,
+ }
+ }
+}