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,6 +6,7 @@ edition = "2021"
[features]
image = []
svg = []
canvas = ["iced_native/canvas"]
[dependencies]
raw-window-handle = "0.5"

View file

@ -1,9 +1,9 @@
use crate::{Color, Font, Settings, Size, Viewport};
use crate::{Color, Font, Primitive, Settings, Size, Viewport};
use iced_graphics::alignment;
use iced_graphics::backend;
use iced_graphics::text;
use iced_graphics::{Background, Primitive, Rectangle, Vector};
use iced_graphics::{Background, Rectangle, Vector};
use std::borrow::Cow;
@ -81,7 +81,6 @@ impl Backend {
translation: Vector,
) {
match primitive {
Primitive::None => {}
Primitive::Quad {
bounds,
background,
@ -161,6 +160,38 @@ impl Backend {
Primitive::Svg { .. } => {
// TODO
}
Primitive::Fill {
path,
paint,
rule,
transform,
} => {
pixels.fill_path(
path,
paint,
*rule,
transform
.post_translate(translation.x, translation.y)
.post_scale(scale_factor, scale_factor),
clip_mask,
);
}
Primitive::Stroke {
path,
paint,
stroke,
transform,
} => {
pixels.stroke_path(
path,
paint,
stroke,
transform
.post_translate(translation.x, translation.y)
.post_scale(scale_factor, scale_factor),
clip_mask,
);
}
Primitive::Group { primitives } => {
for primitive in primitives {
self.draw_primitive(
@ -196,16 +227,19 @@ impl Backend {
translation,
);
}
Primitive::Cached { cache } => {
Primitive::Cache { content } => {
self.draw_primitive(
cache,
content,
pixels,
clip_mask,
scale_factor,
translation,
);
}
Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {}
Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {
// Not supported!
// TODO: Draw a placeholder (?) / Log it (?)
}
}
}
}
@ -386,6 +420,8 @@ fn rectangular_clip_mask(
}
impl iced_graphics::Backend for Backend {
type Geometry = ();
fn trim_measurements(&mut self) {
self.text_pipeline.trim_measurement_cache();
}

276
tiny_skia/src/canvas.rs Normal file
View file

@ -0,0 +1,276 @@
use crate::{Point, Primitive, Rectangle, Size, Vector};
use iced_native::widget::canvas::fill::{self, Fill};
use iced_native::widget::canvas::stroke::{self, Stroke};
use iced_native::widget::canvas::{Path, Style, Text};
use iced_native::Gradient;
pub struct Frame {
size: Size,
transform: tiny_skia::Transform,
stack: Vec<tiny_skia::Transform>,
primitives: Vec<Primitive>,
}
impl Frame {
pub fn new(size: Size) -> Self {
Self {
size,
transform: tiny_skia::Transform::identity(),
stack: Vec::new(),
primitives: Vec::new(),
}
}
pub fn width(&self) -> f32 {
self.size.width
}
pub fn height(&self) -> f32 {
self.size.height
}
pub fn size(&self) -> Size {
self.size
}
pub fn center(&self) -> Point {
Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let path = convert_path(path);
let fill = fill.into();
self.primitives.push(Primitive::Fill {
path,
paint: into_paint(fill.style),
rule: into_fill_rule(fill.rule),
transform: self.transform,
});
}
pub fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
fill: impl Into<Fill>,
) {
self.fill(&Path::rectangle(top_left, size), fill);
}
pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let path = convert_path(path);
let stroke = stroke.into();
let skia_stroke = into_stroke(&stroke);
self.primitives.push(Primitive::Stroke {
path,
paint: into_paint(stroke.style),
stroke: skia_stroke,
transform: self.transform,
});
}
pub fn fill_text(&mut self, text: impl Into<Text>) {
let text = text.into();
let position = if self.transform.is_identity() {
text.position
} else {
let mut transformed = [tiny_skia::Point {
x: text.position.x,
y: text.position.y,
}];
self.transform.map_points(&mut transformed);
Point::new(transformed[0].x, transformed[0].y)
};
// TODO: Use vectorial text instead of primitive
self.primitives.push(Primitive::Text {
content: text.content,
bounds: Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
},
color: text.color,
size: text.size,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
});
}
pub fn push_transform(&mut self) {
self.stack.push(self.transform);
}
pub fn pop_transform(&mut self) {
self.transform = self.stack.pop().expect("Pop transform");
}
pub fn clip(&mut self, _frame: Self, _translation: Vector) {}
pub fn translate(&mut self, translation: Vector) {
self.transform =
self.transform.pre_translate(translation.x, translation.y);
}
pub fn rotate(&mut self, angle: f32) {
self.transform = self
.transform
.pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees()));
}
pub fn scale(&mut self, scale: f32) {
self.transform = self.transform.pre_scale(scale, scale);
}
pub fn into_primitive(self) -> Primitive {
Primitive::Clip {
bounds: Rectangle::new(Point::ORIGIN, self.size),
content: Box::new(Primitive::Group {
primitives: self.primitives,
}),
}
}
}
fn convert_path(path: &Path) -> tiny_skia::Path {
use iced_native::widget::canvas::path::lyon_path;
let mut builder = tiny_skia::PathBuilder::new();
let mut last_point = Default::default();
for event in path.raw().iter() {
match event {
lyon_path::Event::Begin { at } => {
builder.move_to(at.x, at.y);
last_point = at;
}
lyon_path::Event::Line { from, to } => {
if last_point != from {
builder.move_to(from.x, from.y);
}
builder.line_to(to.x, to.y);
last_point = to;
}
lyon_path::Event::Quadratic { from, ctrl, to } => {
if last_point != from {
builder.move_to(from.x, from.y);
}
builder.quad_to(ctrl.x, ctrl.y, to.x, to.y);
last_point = to;
}
lyon_path::Event::Cubic {
from,
ctrl1,
ctrl2,
to,
} => {
if last_point != from {
builder.move_to(from.x, from.y);
}
builder
.cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y);
last_point = to;
}
lyon_path::Event::End { close, .. } => {
if close {
builder.close();
}
}
}
}
builder
.finish()
.expect("Convert lyon path to tiny_skia path")
}
pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> {
tiny_skia::Paint {
shader: match style {
Style::Solid(color) => tiny_skia::Shader::SolidColor(
tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
.expect("Create color"),
),
Style::Gradient(gradient) => match gradient {
Gradient::Linear(linear) => tiny_skia::LinearGradient::new(
tiny_skia::Point {
x: linear.start.x,
y: linear.start.y,
},
tiny_skia::Point {
x: linear.end.x,
y: linear.end.y,
},
linear
.color_stops
.into_iter()
.map(|stop| {
tiny_skia::GradientStop::new(
stop.offset,
tiny_skia::Color::from_rgba(
stop.color.b,
stop.color.g,
stop.color.r,
stop.color.a,
)
.expect("Create color"),
)
})
.collect(),
tiny_skia::SpreadMode::Pad,
tiny_skia::Transform::identity(),
)
.expect("Create linear gradient"),
},
},
anti_alias: true,
..Default::default()
}
}
pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule {
match rule {
fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd,
fill::Rule::NonZero => tiny_skia::FillRule::Winding,
}
}
pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke {
tiny_skia::Stroke {
width: stroke.width,
line_cap: match stroke.line_cap {
stroke::LineCap::Butt => tiny_skia::LineCap::Butt,
stroke::LineCap::Square => tiny_skia::LineCap::Square,
stroke::LineCap::Round => tiny_skia::LineCap::Round,
},
line_join: match stroke.line_join {
stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter,
stroke::LineJoin::Round => tiny_skia::LineJoin::Round,
stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
},
dash: if stroke.line_dash.segments.is_empty() {
None
} else {
tiny_skia::StrokeDash::new(
stroke.line_dash.segments.into(),
stroke.line_dash.offset as f32,
)
},
..Default::default()
}
}

View file

@ -4,10 +4,18 @@ mod backend;
mod settings;
mod text;
#[cfg(feature = "canvas")]
pub mod canvas;
pub use iced_graphics::primitive;
pub use backend::Backend;
pub use primitive::Primitive;
pub use settings::Settings;
pub use iced_graphics::{Color, Error, Font, Point, Size, Vector, Viewport};
pub use iced_graphics::{
Color, Error, Font, Point, Rectangle, Size, Vector, Viewport,
};
/// A [`tiny-skia`] graphics renderer for [`iced`].
///

View file

@ -0,0 +1,82 @@
use crate::{Rectangle, Vector};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum Primitive {
/// A group of primitives
Group {
/// The primitives of the group
primitives: Vec<Primitive>,
},
/// A clip primitive
Clip {
/// The bounds of the clip
bounds: Rectangle,
/// The content of the clip
content: Box<Primitive>,
},
/// A primitive that applies a translation
Translate {
/// The translation vector
translation: Vector,
/// The primitive to translate
content: Box<Primitive>,
},
/// A cached primitive.
///
/// This can be useful if you are implementing a widget where primitive
/// generation is expensive.
Cached {
/// The cached primitive
cache: Arc<Primitive>,
},
/// A basic primitive.
Basic(iced_graphics::Primitive),
}
impl iced_graphics::backend::Primitive for Primitive {
fn translate(self, translation: Vector) -> Self {
Self::Translate {
translation,
content: Box::new(self),
}
}
fn clip(self, bounds: Rectangle) -> Self {
Self::Clip {
bounds,
content: Box::new(self),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Recording(pub(crate) Vec<Primitive>);
impl iced_graphics::backend::Recording for Recording {
type Primitive = Primitive;
fn push(&mut self, primitive: Primitive) {
self.0.push(primitive);
}
fn push_basic(&mut self, basic: iced_graphics::Primitive) {
self.0.push(Primitive::Basic(basic));
}
fn group(self) -> Self::Primitive {
Primitive::Group { primitives: self.0 }
}
fn clear(&mut self) {
self.0.clear();
}
}
impl Recording {
pub fn primitives(&self) -> &[Primitive] {
&self.0
}
}

View file

@ -1,7 +1,6 @@
use crate::{Backend, Color, Error, Renderer, Settings, Viewport};
use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport};
use iced_graphics::window::compositor::{self, Information, SurfaceError};
use iced_graphics::Primitive;
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use std::marker::PhantomData;