Implement Canvas support for iced_tiny_skia

This commit is contained in:
Héctor Ramón Jiménez 2023-03-01 21:34:26 +01:00
parent 3f6e28fa9b
commit 5fd5d1cdf8
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
65 changed files with 1354 additions and 570 deletions

View file

@ -6,12 +6,13 @@ edition = "2021"
[features]
image = ["iced_wgpu/image", "iced_tiny_skia/image"]
svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"]
canvas = ["iced_graphics/canvas"]
qr_code = ["iced_graphics/qr_code"]
canvas = ["iced_wgpu/canvas", "iced_tiny_skia/canvas"]
qr_code = ["canvas", "qrcode"]
tracing = ["iced_wgpu/tracing"]
[dependencies]
raw-window-handle = "0.5"
thiserror = "1"
[dependencies.iced_native]
version = "0.9"
@ -30,3 +31,8 @@ iced_wgpu = { version = "0.9", path = "../wgpu" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] }
[dependencies.qrcode]
version = "0.12"
optional = true
default-features = false

View file

@ -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(),

View file

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

View file

@ -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;

View file

@ -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()))
}
}

View file

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

View file

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