diff --git a/Cargo.toml b/Cargo.toml index e201a4b6..b829c702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,17 @@ image = ["iced_wgpu/image"] svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget canvas = ["iced_wgpu/canvas"] -# Enables using system fonts. +# Enables the `QRCode` widget +qr_code = ["iced_wgpu/qr_code"] +# Enables using system fonts default_system_font = ["iced_wgpu/default_system_font"] # Enables the `iced_glow` renderer. Overrides `iced_wgpu` glow = ["iced_glow", "iced_glutin"] # Enables the `Canvas` widget for `iced_glow` glow_canvas = ["iced_glow/canvas"] -# Enables using system fonts for `iced_glow`. +# Enables the `QRCode` widget for `iced_glow` +glow_qr_code = ["iced_glow/qr_code"] +# Enables using system fonts for `iced_glow` glow_default_system_font = ["iced_glow/default_system_font"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] @@ -67,6 +71,7 @@ members = [ "examples/pick_list", "examples/pokedex", "examples/progress_bar", + "examples/qr_code", "examples/scrollable", "examples/solar_system", "examples/stopwatch", diff --git a/glow/Cargo.toml b/glow/Cargo.toml index 8c15be04..0178f9f7 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/hecrj/iced" [features] canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] default_system_font = ["iced_graphics/font-source"] # Not supported yet! image = [] diff --git a/glow/src/widget.rs b/glow/src/widget.rs index 0e33909d..b5c84c56 100644 --- a/glow/src/widget.rs +++ b/glow/src/widget.rs @@ -52,6 +52,14 @@ pub mod canvas; #[doc(no_inline)] pub use canvas::Canvas; +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + pub use iced_native::{Image, Space}; /// A container that distributes its contents vertically. diff --git a/glow/src/widget/qr_code.rs b/glow/src/widget/qr_code.rs new file mode 100644 index 00000000..7b1c2408 --- /dev/null +++ b/glow/src/widget/qr_code.rs @@ -0,0 +1,2 @@ +//! Encode and display information in a QR code. +pub use iced_graphics::qr_code::*; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 1511f8b0..4ce50e5a 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [features] canvas = ["lyon"] +qr_code = ["qrcode", "canvas"] font-source = ["font-kit"] font-fallback = [] font-icons = [] @@ -32,6 +33,10 @@ path = "../style" version = "0.16" optional = true +[dependencies.qrcode] +version = "0.12" +optional = true + [dependencies.font-kit] version = "0.8" optional = true diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs index f87b558a..159ca91b 100644 --- a/graphics/src/widget.rs +++ b/graphics/src/widget.rs @@ -63,3 +63,11 @@ pub mod canvas; #[cfg(feature = "canvas")] #[doc(no_inline)] pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs new file mode 100644 index 00000000..b3a01dd7 --- /dev/null +++ b/graphics/src/widget/qr_code.rs @@ -0,0 +1,305 @@ +//! Encode and display information in a QR code. +use crate::canvas; +use crate::{Backend, Defaults, Primitive, Renderer, Vector}; + +use iced_native::{ + layout, mouse, Color, Element, Hasher, Layout, Length, Point, Rectangle, + Size, 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, B> Widget> for QRCode<'a> +where + B: Backend, +{ + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _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( + f32::from(side_length), + f32::from(side_length), + )) + } + + fn hash_layout(&self, state: &mut Hasher) { + use std::hash::Hash; + + self.state.contents.hash(state); + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) -> (Primitive, mouse::Interaction) { + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = self.state.cache.draw(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, + ); + }); + }); + + ( + Primitive::Translate { + translation: Vector::new(bounds.x, bounds.y), + content: Box::new(geometry.into_primitive()), + }, + mouse::Interaction::default(), + ) + } +} + +impl<'a, Message, B> Into>> for QRCode<'a> +where + B: Backend, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec, + 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 { + 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 { + 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 { + 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 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 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 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, + } + } +} diff --git a/src/widget.rs b/src/widget.rs index e8fff9cc..fdef89d6 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -30,6 +30,13 @@ mod platform { )] pub use crate::renderer::widget::canvas; + #[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "qr_code", feature = "glow_qr_code"))) + )] + pub use crate::renderer::widget::qr_code; + #[cfg_attr(docsrs, doc(cfg(feature = "image")))] pub mod image { //! Display images in your user interface. @@ -53,6 +60,10 @@ mod platform { #[cfg(any(feature = "canvas", feature = "glow_canvas"))] #[doc(no_inline)] pub use canvas::Canvas; + + #[cfg(any(feature = "qr_code", feature = "glow_qr_code"))] + #[doc(no_inline)] + pub use qr_code::QRCode; } #[cfg(target_arch = "wasm32")] diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 01fd720b..4d9f9ada 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/hecrj/iced" [features] svg = ["resvg"] canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] default_system_font = ["iced_graphics/font-source"] [dependencies] diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs index 1dae26f5..177ae1b6 100644 --- a/wgpu/src/widget.rs +++ b/wgpu/src/widget.rs @@ -52,6 +52,14 @@ pub mod canvas; #[doc(no_inline)] pub use canvas::Canvas; +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + pub use iced_native::Space; /// A container that distributes its contents vertically. diff --git a/wgpu/src/widget/qr_code.rs b/wgpu/src/widget/qr_code.rs new file mode 100644 index 00000000..7b1c2408 --- /dev/null +++ b/wgpu/src/widget/qr_code.rs @@ -0,0 +1,2 @@ +//! Encode and display information in a QR code. +pub use iced_graphics::qr_code::*;