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

@ -1,11 +1,10 @@
use crate::quad;
use crate::text;
use crate::triangle;
use crate::{Settings, Transformation};
use crate::{Layer, Primitive, Settings, Transformation};
use iced_graphics::backend;
use iced_graphics::layer::Layer;
use iced_graphics::{Color, Font, Primitive, Size, Viewport};
use iced_graphics::{Color, Font, Size, Viewport};
#[cfg(feature = "tracing")]
use tracing::info_span;
@ -330,6 +329,8 @@ impl Backend {
}
impl iced_graphics::Backend for Backend {
type Geometry = ();
fn trim_measurements(&mut self) {
self.text_pipeline.trim_measurement_cache()
}

View file

@ -6,10 +6,10 @@ use iced_graphics::image::raster;
#[cfg(feature = "svg")]
use iced_graphics::image::vector;
use crate::layer;
use crate::{Buffer, Transformation};
use atlas::Atlas;
use iced_graphics::layer;
use iced_native::{Rectangle, Size};
use std::cell::RefCell;

274
wgpu/src/layer.rs Normal file
View file

@ -0,0 +1,274 @@
//! Organize rendering primitives into a flattened list of layers.
mod image;
mod quad;
mod text;
pub mod mesh;
pub use image::Image;
pub use mesh::Mesh;
pub use quad::Quad;
pub use text::Text;
use crate::Primitive;
use iced_graphics::alignment;
use iced_graphics::{
Background, Color, Font, Point, Rectangle, Size, Vector, Viewport,
};
/// A group of primitives that should be clipped together.
#[derive(Debug)]
pub struct Layer<'a> {
/// The clipping bounds of the [`Layer`].
pub bounds: Rectangle,
/// The quads of the [`Layer`].
pub quads: Vec<Quad>,
/// The triangle meshes of the [`Layer`].
pub meshes: Vec<Mesh<'a>>,
/// The text of the [`Layer`].
pub text: Vec<Text<'a>>,
/// The images of the [`Layer`].
pub images: Vec<Image>,
}
impl<'a> Layer<'a> {
/// Creates a new [`Layer`] with the given clipping bounds.
pub fn new(bounds: Rectangle) -> Self {
Self {
bounds,
quads: Vec::new(),
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
}
}
/// Creates a new [`Layer`] for the provided overlay text.
///
/// This can be useful for displaying debug information.
pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self {
let mut overlay =
Layer::new(Rectangle::with_size(viewport.logical_size()));
for (i, line) in lines.iter().enumerate() {
let text = Text {
content: line.as_ref(),
bounds: Rectangle::new(
Point::new(11.0, 11.0 + 25.0 * i as f32),
Size::INFINITY,
),
color: Color::new(0.9, 0.9, 0.9, 1.0),
size: 20.0,
font: Font::Monospace,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
};
overlay.text.push(text);
overlay.text.push(Text {
bounds: text.bounds + Vector::new(-1.0, -1.0),
color: Color::BLACK,
..text
});
}
overlay
}
/// Distributes the given [`Primitive`] and generates a list of layers based
/// on its contents.
pub fn generate(
primitives: &'a [Primitive],
viewport: &Viewport,
) -> Vec<Self> {
let first_layer =
Layer::new(Rectangle::with_size(viewport.logical_size()));
let mut layers = vec![first_layer];
for primitive in primitives {
Self::process_primitive(
&mut layers,
Vector::new(0.0, 0.0),
primitive,
0,
);
}
layers
}
fn process_primitive(
layers: &mut Vec<Self>,
translation: Vector,
primitive: &'a Primitive,
current_layer: usize,
) {
match primitive {
Primitive::Text {
content,
bounds,
size,
color,
font,
horizontal_alignment,
vertical_alignment,
} => {
let layer = &mut layers[current_layer];
layer.text.push(Text {
content,
bounds: *bounds + translation,
size: *size,
color: *color,
font: *font,
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
});
}
Primitive::Quad {
bounds,
background,
border_radius,
border_width,
border_color,
} => {
let layer = &mut layers[current_layer];
// TODO: Move some of these computations to the GPU (?)
layer.quads.push(Quad {
position: [
bounds.x + translation.x,
bounds.y + translation.y,
],
size: [bounds.width, bounds.height],
color: match background {
Background::Color(color) => color.into_linear(),
},
border_radius: *border_radius,
border_width: *border_width,
border_color: border_color.into_linear(),
});
}
Primitive::Image { handle, bounds } => {
let layer = &mut layers[current_layer];
layer.images.push(Image::Raster {
handle: handle.clone(),
bounds: *bounds + translation,
});
}
Primitive::Svg {
handle,
color,
bounds,
} => {
let layer = &mut layers[current_layer];
layer.images.push(Image::Vector {
handle: handle.clone(),
color: *color,
bounds: *bounds + translation,
});
}
Primitive::SolidMesh { buffers, size } => {
let layer = &mut layers[current_layer];
let bounds = Rectangle::new(
Point::new(translation.x, translation.y),
*size,
);
// Only draw visible content
if let Some(clip_bounds) = layer.bounds.intersection(&bounds) {
layer.meshes.push(Mesh::Solid {
origin: Point::new(translation.x, translation.y),
buffers,
clip_bounds,
});
}
}
Primitive::GradientMesh {
buffers,
size,
gradient,
} => {
let layer = &mut layers[current_layer];
let bounds = Rectangle::new(
Point::new(translation.x, translation.y),
*size,
);
// Only draw visible content
if let Some(clip_bounds) = layer.bounds.intersection(&bounds) {
layer.meshes.push(Mesh::Gradient {
origin: Point::new(translation.x, translation.y),
buffers,
clip_bounds,
gradient,
});
}
}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
Self::process_primitive(
layers,
translation,
primitive,
current_layer,
)
}
}
Primitive::Clip { bounds, content } => {
let layer = &mut layers[current_layer];
let translated_bounds = *bounds + translation;
// Only draw visible content
if let Some(clip_bounds) =
layer.bounds.intersection(&translated_bounds)
{
let clip_layer = Layer::new(clip_bounds);
layers.push(clip_layer);
Self::process_primitive(
layers,
translation,
content,
layers.len() - 1,
);
}
}
Primitive::Translate {
translation: new_translation,
content,
} => {
Self::process_primitive(
layers,
translation + *new_translation,
content,
current_layer,
);
}
Primitive::Cache { content } => {
Self::process_primitive(
layers,
translation,
content,
current_layer,
);
}
Primitive::Fill { .. } | Primitive::Stroke { .. } => {
// Unsupported!
// TODO: Draw a placeholder (?)
}
}
}
}

27
wgpu/src/layer/image.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::{Color, Rectangle};
use iced_native::{image, svg};
/// A raster or vector image.
#[derive(Debug, Clone)]
pub enum Image {
/// A raster image.
Raster {
/// The handle of a raster image.
handle: image::Handle,
/// The bounds of the image.
bounds: Rectangle,
},
/// A vector image.
Vector {
/// The handle of a vector image.
handle: svg::Handle,
/// The [`Color`] filter
color: Option<Color>,
/// The bounds of the image.
bounds: Rectangle,
},
}

93
wgpu/src/layer/mesh.rs Normal file
View file

@ -0,0 +1,93 @@
//! A collection of triangle primitives.
use crate::primitive;
use crate::{Gradient, Point, Rectangle};
/// A mesh of triangles.
#[derive(Debug, Clone, Copy)]
pub enum Mesh<'a> {
/// A mesh of triangles with a solid color.
Solid {
/// The origin of the vertices of the [`Mesh`].
origin: Point,
/// The vertex and index buffers of the [`Mesh`].
buffers: &'a primitive::Mesh2D<primitive::ColoredVertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
},
/// A mesh of triangles with a gradient color.
Gradient {
/// The origin of the vertices of the [`Mesh`].
origin: Point,
/// The vertex and index buffers of the [`Mesh`].
buffers: &'a primitive::Mesh2D<primitive::Vertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
/// The gradient to apply to the [`Mesh`].
gradient: &'a Gradient,
},
}
impl Mesh<'_> {
/// Returns the origin of the [`Mesh`].
pub fn origin(&self) -> Point {
match self {
Self::Solid { origin, .. } | Self::Gradient { origin, .. } => {
*origin
}
}
}
/// Returns the indices of the [`Mesh`].
pub fn indices(&self) -> &[u32] {
match self {
Self::Solid { buffers, .. } => &buffers.indices,
Self::Gradient { buffers, .. } => &buffers.indices,
}
}
/// Returns the clip bounds of the [`Mesh`].
pub fn clip_bounds(&self) -> Rectangle<f32> {
match self {
Self::Solid { clip_bounds, .. }
| Self::Gradient { clip_bounds, .. } => *clip_bounds,
}
}
}
/// The result of counting the attributes of a set of meshes.
#[derive(Debug, Clone, Copy, Default)]
pub struct AttributeCount {
/// The total amount of solid vertices.
pub solid_vertices: usize,
/// The total amount of gradient vertices.
pub gradient_vertices: usize,
/// The total amount of indices.
pub indices: usize,
}
/// Returns the number of total vertices & total indices of all [`Mesh`]es.
pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount {
meshes
.iter()
.fold(AttributeCount::default(), |mut count, mesh| {
match mesh {
Mesh::Solid { buffers, .. } => {
count.solid_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
Mesh::Gradient { buffers, .. } => {
count.gradient_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
}
count
})
}

30
wgpu/src/layer/quad.rs Normal file
View file

@ -0,0 +1,30 @@
/// A colored rectangle with a border.
///
/// This type can be directly uploaded to GPU memory.
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct Quad {
/// The position of the [`Quad`].
pub position: [f32; 2],
/// The size of the [`Quad`].
pub size: [f32; 2],
/// The color of the [`Quad`], in __linear RGB__.
pub color: [f32; 4],
/// The border color of the [`Quad`], in __linear RGB__.
pub border_color: [f32; 4],
/// The border radius of the [`Quad`].
pub border_radius: [f32; 4],
/// The border width of the [`Quad`].
pub border_width: f32,
}
#[allow(unsafe_code)]
unsafe impl bytemuck::Zeroable for Quad {}
#[allow(unsafe_code)]
unsafe impl bytemuck::Pod for Quad {}

26
wgpu/src/layer/text.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::{alignment, Color, Font, Rectangle};
/// A paragraph of text.
#[derive(Debug, Clone, Copy)]
pub struct Text<'a> {
/// The content of the [`Text`].
pub content: &'a str,
/// The layout bounds of the [`Text`].
pub bounds: Rectangle,
/// The color of the [`Text`], in __linear RGB_.
pub color: Color,
/// The size of the [`Text`].
pub size: f32,
/// The font of the [`Text`].
pub font: Font,
/// The horizontal alignment of the [`Text`].
pub horizontal_alignment: alignment::Horizontal,
/// The vertical alignment of the [`Text`].
pub vertical_alignment: alignment::Vertical,
}

View file

@ -25,7 +25,7 @@
)]
#![deny(
missing_debug_implementations,
missing_docs,
//missing_docs,
unsafe_code,
unused_results,
clippy::extra_unused_lifetimes,
@ -38,7 +38,9 @@
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod layer;
pub mod settings;
pub mod widget;
pub mod window;
mod backend;
@ -47,16 +49,23 @@ mod quad;
mod text;
mod triangle;
pub use iced_graphics::primitive;
pub use iced_graphics::{
Antialiasing, Color, Error, Font, Primitive, Viewport,
Antialiasing, Color, Error, Font, Gradient, Point, Rectangle, Size, Vector,
Viewport,
};
pub use iced_native::alignment;
pub use iced_native::Theme;
pub use wgpu;
pub use backend::Backend;
pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
use crate::buffer::Buffer;
use buffer::Buffer;
use iced_graphics::Transformation;
#[cfg(any(feature = "image", feature = "svg"))]

View file

@ -1,5 +1,6 @@
use crate::layer;
use crate::{Buffer, Transformation};
use iced_graphics::layer;
use iced_native::Rectangle;
use bytemuck::{Pod, Zeroable};

View file

@ -1,6 +1,7 @@
use crate::layer::Text;
pub use iced_native::text::Hit;
use iced_graphics::layer::Text;
use iced_native::alignment;
use iced_native::{Color, Font, Rectangle, Size};

View file

@ -2,12 +2,12 @@
mod msaa;
use crate::buffer::r#static::Buffer;
use crate::layer::mesh::{self, Mesh};
use crate::settings;
use crate::Transformation;
use iced_graphics::layer::mesh::{self, Mesh};
use iced_graphics::triangle::ColoredVertex2D;
use iced_graphics::Size;
#[cfg(feature = "tracing")]
use tracing::info_span;
@ -468,6 +468,7 @@ mod solid {
use crate::settings;
use crate::triangle;
use encase::ShaderType;
use iced_graphics::primitive;
use iced_graphics::Transformation;
#[derive(Debug)]
@ -478,7 +479,7 @@ mod solid {
#[derive(Debug)]
pub struct Layer {
pub vertices: Buffer<triangle::ColoredVertex2D>,
pub vertices: Buffer<primitive::ColoredVertex2D>,
pub uniforms: dynamic::Buffer<Uniforms>,
pub constants: wgpu::BindGroup,
}
@ -596,7 +597,7 @@ mod solid {
entry_point: "vs_main",
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<
triangle::ColoredVertex2D,
primitive::ColoredVertex2D,
>()
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
@ -637,7 +638,7 @@ mod gradient {
use encase::ShaderType;
use glam::{IVec4, Vec4};
use iced_graphics::triangle::Vertex2D;
use iced_graphics::primitive;
#[derive(Debug)]
pub struct Pipeline {
@ -647,7 +648,7 @@ mod gradient {
#[derive(Debug)]
pub struct Layer {
pub vertices: Buffer<Vertex2D>,
pub vertices: Buffer<primitive::Vertex2D>,
pub uniforms: dynamic::Buffer<Uniforms>,
pub storage: dynamic::Buffer<Storage>,
pub constants: wgpu::BindGroup,
@ -810,34 +811,38 @@ mod gradient {
),
});
let pipeline = device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle::gradient pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex2D>()
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
),
}],
let pipeline =
device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle::gradient pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<
primitive::Vertex2D,
>(
)
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
),
}],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[triangle::fragment_target(format)],
}),
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[triangle::fragment_target(format)],
}),
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
},
);
);
Self {
pipeline,

9
wgpu/src/widget.rs Normal file
View file

@ -0,0 +1,9 @@
//! Use the graphical widgets supported out-of-the-box.
#[cfg(feature = "canvas")]
#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
pub mod canvas;
#[cfg(feature = "canvas")]
#[doc(no_inline)]
pub use canvas::Canvas;

16
wgpu/src/widget/canvas.rs Normal file
View file

@ -0,0 +1,16 @@
mod cache;
mod frame;
mod geometry;
pub use cache::Cache;
pub use frame::Frame;
pub use geometry::Geometry;
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,
};

View file

@ -0,0 +1,93 @@
use crate::widget::canvas::{Frame, Geometry};
use crate::Primitive;
use iced_native::Size;
use std::{cell::RefCell, sync::Arc};
#[derive(Default)]
enum State {
#[default]
Empty,
Filled {
bounds: Size,
primitive: Arc<Primitive>,
},
}
/// 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>,
}
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(
&self,
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::from_primitive(Primitive::Cache {
content: primitive.clone(),
});
}
}
let mut frame = Frame::new(bounds);
draw_fn(&mut frame);
let primitive = Arc::new(frame.into_primitive());
*self.state.borrow_mut() = State::Filled {
bounds,
primitive: primitive.clone(),
};
Geometry::from_primitive(Primitive::Cache { content: primitive })
}
}
impl std::fmt::Debug for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
State::Empty => write!(f, "Empty"),
State::Filled { primitive, bounds } => f
.debug_struct("Filled")
.field("primitive", primitive)
.field("bounds", bounds)
.finish(),
}
}
}

View file

@ -0,0 +1,609 @@
use crate::primitive::{self, Primitive};
use crate::widget::canvas::fill::{self, Fill};
use crate::widget::canvas::{
LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
};
use iced_native::{Gradient, Point, Rectangle, Size, Vector};
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
/// The frame of a [`Canvas`].
///
/// [`Canvas`]: crate::widget::Canvas
#[allow(missing_debug_implementations)]
pub struct Frame {
size: Size,
buffers: BufferStack,
primitives: Vec<Primitive>,
transforms: Transforms,
fill_tessellator: tessellation::FillTessellator,
stroke_tessellator: tessellation::StrokeTessellator,
}
enum Buffer {
Solid(tessellation::VertexBuffers<primitive::ColoredVertex2D, u32>),
Gradient(
tessellation::VertexBuffers<primitive::Vertex2D, u32>,
Gradient,
),
}
struct BufferStack {
stack: Vec<Buffer>,
}
impl BufferStack {
fn new() -> Self {
Self { stack: Vec::new() }
}
fn get_mut(&mut self, style: &Style) -> &mut Buffer {
match style {
Style::Solid(_) => match self.stack.last() {
Some(Buffer::Solid(_)) => {}
_ => {
self.stack.push(Buffer::Solid(
tessellation::VertexBuffers::new(),
));
}
},
Style::Gradient(gradient) => match self.stack.last() {
Some(Buffer::Gradient(_, last)) if gradient == last => {}
_ => {
self.stack.push(Buffer::Gradient(
tessellation::VertexBuffers::new(),
gradient.clone(),
));
}
},
}
self.stack.last_mut().unwrap()
}
fn get_fill<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::FillGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color.into_linear()),
))
}
(Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new(
tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder),
),
_ => unreachable!(),
}
}
fn get_stroke<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color.into_linear()),
))
}
(Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new(
tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder),
),
_ => unreachable!(),
}
}
}
#[derive(Debug)]
struct Transforms {
previous: Vec<Transform>,
current: Transform,
}
#[derive(Debug, Clone, Copy)]
struct Transform {
raw: lyon::math::Transform,
is_identity: bool,
}
impl Transform {
/// Transforms the given [Point] by the transformation matrix.
fn transform_point(&self, point: &mut Point) {
let transformed = self
.raw
.transform_point(euclid::Point2D::new(point.x, point.y));
point.x = transformed.x;
point.y = transformed.y;
}
fn transform_style(&self, style: Style) -> Style {
match style {
Style::Solid(color) => Style::Solid(color),
Style::Gradient(gradient) => {
Style::Gradient(self.transform_gradient(gradient))
}
}
}
fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
let (start, end) = match &mut gradient {
Gradient::Linear(linear) => (&mut linear.start, &mut linear.end),
};
self.transform_point(start);
self.transform_point(end);
gradient
}
}
impl Frame {
/// Creates a new empty [`Frame`] with the given dimensions.
///
/// The default coordinate system of a [`Frame`] has its origin at the
/// top-left corner of its bounds.
pub fn new(size: Size) -> Frame {
Frame {
size,
buffers: BufferStack::new(),
primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform {
raw: lyon::math::Transform::identity(),
is_identity: true,
},
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
/// Returns the width of the [`Frame`].
#[inline]
pub fn width(&self) -> f32 {
self.size.width
}
/// Returns the height of the [`Frame`].
#[inline]
pub fn height(&self) -> f32 {
self.size.height
}
/// Returns the dimensions of the [`Frame`].
#[inline]
pub fn size(&self) -> Size {
self.size
}
/// Returns the coordinate of the center of the [`Frame`].
#[inline]
pub fn center(&self) -> Point {
Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
/// 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>) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
if self.transforms.current.is_identity {
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.raw);
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Tessellate path.");
}
/// 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>,
) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let top_left =
self.transforms.current.raw.transform_point(
lyon::math::Point::new(top_left.x, top_left.y),
);
let size =
self.transforms.current.raw.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
self.fill_tessellator
.tessellate_rectangle(
&lyon::math::Box2D::new(top_left, top_left + size),
&options,
buffer.as_mut(),
)
.expect("Fill rectangle");
}
/// 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>>) {
let stroke = stroke.into();
let mut buffer = self
.buffers
.get_stroke(&self.transforms.current.transform_style(stroke.style));
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
options.start_cap = into_line_cap(stroke.line_cap);
options.end_cap = into_line_cap(stroke.line_cap);
options.line_join = into_line_join(stroke.line_join);
let path = if stroke.line_dash.segments.is_empty() {
Cow::Borrowed(path)
} else {
Cow::Owned(dashed(path, stroke.line_dash))
};
if self.transforms.current.is_identity {
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.raw);
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Stroke path");
}
/// 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>) {
let text = text.into();
let position = if self.transforms.current.is_identity {
text.position
} else {
let transformed = self.transforms.current.raw.transform_point(
lyon::math::Point::new(text.position.x, text.position.y),
);
Point::new(transformed.x, transformed.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,
});
}
/// 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)) {
self.push_transform();
f(self);
self.pop_transform();
}
pub fn push_transform(&mut self) {
self.transforms.previous.push(self.transforms.current);
}
pub fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
/// 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 = Frame::new(region.size());
f(&mut frame);
let translation = Vector::new(region.x, region.y);
self.clip(frame, translation);
}
pub fn clip(&mut self, frame: Frame, translation: Vector) {
let size = frame.size();
let primitives = frame.into_primitives();
let (text, meshes) = primitives
.into_iter()
.partition(|primitive| matches!(primitive, Primitive::Text { .. }));
self.primitives.push(Primitive::Group {
primitives: vec![
Primitive::Translate {
translation,
content: Box::new(Primitive::Group { primitives: meshes }),
},
Primitive::Translate {
translation,
content: Box::new(Primitive::Clip {
bounds: Rectangle::with_size(size),
content: Box::new(Primitive::Group {
primitives: text,
}),
}),
},
],
});
}
/// Applies a translation to the current transform of the [`Frame`].
#[inline]
pub fn translate(&mut self, translation: Vector) {
self.transforms.current.raw = self
.transforms
.current
.raw
.pre_translate(lyon::math::Vector::new(
translation.x,
translation.y,
));
self.transforms.current.is_identity = false;
}
/// Applies a rotation in radians to the current transform of the [`Frame`].
#[inline]
pub fn rotate(&mut self, angle: f32) {
self.transforms.current.raw = self
.transforms
.current
.raw
.pre_rotate(lyon::math::Angle::radians(angle));
self.transforms.current.is_identity = false;
}
/// Applies a scaling to the current transform of the [`Frame`].
#[inline]
pub fn scale(&mut self, scale: f32) {
self.transforms.current.raw =
self.transforms.current.raw.pre_scale(scale, scale);
self.transforms.current.is_identity = false;
}
/// Produces the [`Primitive`] representing everything drawn on the [`Frame`].
pub fn into_primitive(self) -> Primitive {
Primitive::Group {
primitives: self.into_primitives(),
}
}
fn into_primitives(mut self) -> Vec<Primitive> {
for buffer in self.buffers.stack {
match buffer {
Buffer::Solid(buffer) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::SolidMesh {
buffers: primitive::Mesh2D {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
})
}
}
Buffer::Gradient(buffer, gradient) => {
if !buffer.indices.is_empty() {
self.primitives.push(Primitive::GradientMesh {
buffers: primitive::Mesh2D {
vertices: buffer.vertices,
indices: buffer.indices,
},
size: self.size,
gradient,
})
}
}
}
}
self.primitives
}
}
struct Vertex2DBuilder;
impl tessellation::FillVertexConstructor<primitive::Vertex2D>
for Vertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
) -> primitive::Vertex2D {
let position = vertex.position();
primitive::Vertex2D {
position: [position.x, position.y],
}
}
}
impl tessellation::StrokeVertexConstructor<primitive::Vertex2D>
for Vertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
) -> primitive::Vertex2D {
let position = vertex.position();
primitive::Vertex2D {
position: [position.x, position.y],
}
}
}
struct TriangleVertex2DBuilder([f32; 4]);
impl tessellation::FillVertexConstructor<primitive::ColoredVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
) -> primitive::ColoredVertex2D {
let position = vertex.position();
primitive::ColoredVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
impl tessellation::StrokeVertexConstructor<primitive::ColoredVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
) -> primitive::ColoredVertex2D {
let position = vertex.position();
primitive::ColoredVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin {
match line_join {
LineJoin::Miter => lyon::tessellation::LineJoin::Miter,
LineJoin::Round => lyon::tessellation::LineJoin::Round,
LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel,
}
}
fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap {
match line_cap {
LineCap::Butt => lyon::tessellation::LineCap::Butt,
LineCap::Square => lyon::tessellation::LineCap::Square,
LineCap::Round => lyon::tessellation::LineCap::Round,
}
}
fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule {
match rule {
fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero,
fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
}
}
pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
use lyon::algorithms::walk::{
walk_along_path, RepeatedPattern, WalkerEvent,
};
use lyon::path::iterator::PathIterator;
Path::new(|builder| {
let segments_odd = (line_dash.segments.len() % 2 == 1)
.then(|| [line_dash.segments, line_dash.segments].concat());
let mut draw_line = false;
walk_along_path(
path.raw().iter().flattened(0.01),
0.0,
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
&mut RepeatedPattern {
callback: |event: WalkerEvent<'_>| {
let point = Point {
x: event.position.x,
y: event.position.y,
};
if draw_line {
builder.line_to(point);
} else {
builder.move_to(point);
}
draw_line = !draw_line;
true
},
index: line_dash.offset,
intervals: segments_odd
.as_deref()
.unwrap_or(line_dash.segments),
},
);
})
}

View file

@ -0,0 +1,24 @@
use crate::Primitive;
/// A bunch of shapes that can be drawn.
///
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
/// [`Cache`].
///
/// [`Frame`]: crate::widget::canvas::Frame
/// [`Cache`]: crate::widget::canvas::Cache
#[derive(Debug, Clone)]
pub struct Geometry(Primitive);
impl Geometry {
pub(crate) fn from_primitive(primitive: Primitive) -> Self {
Self(primitive)
}
/// Turns the [`Geometry`] into a [`Primitive`].
///
/// This can be useful if you are building a custom widget.
pub fn into_primitive(self) -> Primitive {
self.0
}
}