Implement `QRCode` widget

This commit is contained in:
Héctor Ramón Jiménez 2020-11-20 10:13:58 +01:00
parent 209056e1cd
commit 3296be845c
11 changed files with 358 additions and 2 deletions

View File

@ -21,13 +21,17 @@ image = ["iced_wgpu/image"]
svg = ["iced_wgpu/svg"] svg = ["iced_wgpu/svg"]
# Enables the `Canvas` widget # Enables the `Canvas` widget
canvas = ["iced_wgpu/canvas"] 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"] default_system_font = ["iced_wgpu/default_system_font"]
# Enables the `iced_glow` renderer. Overrides `iced_wgpu` # Enables the `iced_glow` renderer. Overrides `iced_wgpu`
glow = ["iced_glow", "iced_glutin"] glow = ["iced_glow", "iced_glutin"]
# Enables the `Canvas` widget for `iced_glow` # Enables the `Canvas` widget for `iced_glow`
glow_canvas = ["iced_glow/canvas"] 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"] glow_default_system_font = ["iced_glow/default_system_font"]
# Enables a debug view in native platforms (press F12) # Enables a debug view in native platforms (press F12)
debug = ["iced_winit/debug"] debug = ["iced_winit/debug"]
@ -67,6 +71,7 @@ members = [
"examples/pick_list", "examples/pick_list",
"examples/pokedex", "examples/pokedex",
"examples/progress_bar", "examples/progress_bar",
"examples/qr_code",
"examples/scrollable", "examples/scrollable",
"examples/solar_system", "examples/solar_system",
"examples/stopwatch", "examples/stopwatch",

View File

@ -9,6 +9,7 @@ repository = "https://github.com/hecrj/iced"
[features] [features]
canvas = ["iced_graphics/canvas"] canvas = ["iced_graphics/canvas"]
qr_code = ["iced_graphics/qr_code"]
default_system_font = ["iced_graphics/font-source"] default_system_font = ["iced_graphics/font-source"]
# Not supported yet! # Not supported yet!
image = [] image = []

View File

@ -52,6 +52,14 @@ pub mod canvas;
#[doc(no_inline)] #[doc(no_inline)]
pub use canvas::Canvas; 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}; pub use iced_native::{Image, Space};
/// A container that distributes its contents vertically. /// A container that distributes its contents vertically.

View File

@ -0,0 +1,2 @@
//! Encode and display information in a QR code.
pub use iced_graphics::qr_code::*;

View File

@ -6,6 +6,7 @@ edition = "2018"
[features] [features]
canvas = ["lyon"] canvas = ["lyon"]
qr_code = ["qrcode", "canvas"]
font-source = ["font-kit"] font-source = ["font-kit"]
font-fallback = [] font-fallback = []
font-icons = [] font-icons = []
@ -32,6 +33,10 @@ path = "../style"
version = "0.16" version = "0.16"
optional = true optional = true
[dependencies.qrcode]
version = "0.12"
optional = true
[dependencies.font-kit] [dependencies.font-kit]
version = "0.8" version = "0.8"
optional = true optional = true

View File

@ -63,3 +63,11 @@ pub mod canvas;
#[cfg(feature = "canvas")] #[cfg(feature = "canvas")]
#[doc(no_inline)] #[doc(no_inline)]
pub use canvas::Canvas; 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;

View File

@ -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,
}
}
}

View File

@ -30,6 +30,13 @@ mod platform {
)] )]
pub use crate::renderer::widget::canvas; 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")))] #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
pub mod image { pub mod image {
//! Display images in your user interface. //! Display images in your user interface.
@ -53,6 +60,10 @@ mod platform {
#[cfg(any(feature = "canvas", feature = "glow_canvas"))] #[cfg(any(feature = "canvas", feature = "glow_canvas"))]
#[doc(no_inline)] #[doc(no_inline)]
pub use canvas::Canvas; 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")] #[cfg(target_arch = "wasm32")]

View File

@ -10,6 +10,7 @@ repository = "https://github.com/hecrj/iced"
[features] [features]
svg = ["resvg"] svg = ["resvg"]
canvas = ["iced_graphics/canvas"] canvas = ["iced_graphics/canvas"]
qr_code = ["iced_graphics/qr_code"]
default_system_font = ["iced_graphics/font-source"] default_system_font = ["iced_graphics/font-source"]
[dependencies] [dependencies]

View File

@ -52,6 +52,14 @@ pub mod canvas;
#[doc(no_inline)] #[doc(no_inline)]
pub use canvas::Canvas; 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; pub use iced_native::Space;
/// A container that distributes its contents vertically. /// A container that distributes its contents vertically.

View File

@ -0,0 +1,2 @@
//! Encode and display information in a QR code.
pub use iced_graphics::qr_code::*;