diff options
Diffstat (limited to 'graphics/src/widget')
| -rw-r--r-- | graphics/src/widget/qr_code.rs | 305 | 
1 files changed, 305 insertions, 0 deletions
| 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<Message, Renderer<B>> for QRCode<'a> +where +    B: Backend, +{ +    fn width(&self) -> Length { +        Length::Shrink +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        _renderer: &Renderer<B>, +        _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<B>, +        _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<Element<'a, Message, Renderer<B>>> for QRCode<'a> +where +    B: Backend, +{ +    fn into(self) -> Element<'a, Message, Renderer<B>> { +        Element::new(self) +    } +} + +/// 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, +        } +    } +} | 
