From 5100b5d0a1f654ec1254b7765ceadfb9091d6939 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 24 Feb 2023 23:24:48 +0100 Subject: Introduce `iced_renderer` subcrate featuring runtime renderer fallback --- wgpu/src/window.rs | 3 +- wgpu/src/window/compositor.rs | 139 ++++++++++++++++++++++++++---------------- 2 files changed, 88 insertions(+), 54 deletions(-) (limited to 'wgpu') diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index aac5fb9e..9545a14e 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,4 +1,5 @@ //! Display rendering results on windows. -mod compositor; +pub mod compositor; pub use compositor::Compositor; +pub use wgpu::Surface; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 365cb603..7406bfb8 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,8 +1,9 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; +//! Connect a window with a renderer. +use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; use futures::stream::{self, StreamExt}; -use iced_graphics::compositor; +use iced_graphics::window::compositor; use iced_native::futures; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; @@ -112,6 +113,77 @@ impl Compositor { } } +/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and +/// window. +pub fn new( + settings: Settings, + compatible_window: Option<&W>, +) -> Result<(Compositor, Backend), Error> { + let compositor = futures::executor::block_on(Compositor::request( + settings, + compatible_window, + )) + .ok_or(Error::GraphicsAdapterNotFound)?; + + let backend = compositor.create_backend(); + + Ok((compositor, backend)) +} + +/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +pub fn present>( + compositor: &mut Compositor, + backend: &mut Backend, + surface: &mut wgpu::Surface, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Result<(), compositor::SurfaceError> { + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + let view = &frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + backend.present( + &compositor.device, + &compositor.queue, + &mut encoder, + Some(background_color), + view, + primitives, + viewport, + overlay, + ); + + // Submit work + let _submission = compositor.queue.submit(Some(encoder.finish())); + frame.present(); + + Ok(()) + } + Err(error) => match error { + wgpu::SurfaceError::Timeout => { + Err(compositor::SurfaceError::Timeout) + } + wgpu::SurfaceError::Outdated => { + Err(compositor::SurfaceError::Outdated) + } + wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), + wgpu::SurfaceError::OutOfMemory => { + Err(compositor::SurfaceError::OutOfMemory) + } + }, + } +} + impl iced_graphics::window::Compositor for Compositor { type Settings = Settings; type Renderer = Renderer; @@ -121,13 +193,7 @@ impl iced_graphics::window::Compositor for Compositor { settings: Self::Settings, compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - let compositor = futures::executor::block_on(Self::request( - settings, - compatible_window, - )) - .ok_or(Error::GraphicsAdapterNotFound)?; - - let backend = compositor.create_backend(); + let (compositor, backend) = new(settings, compatible_window)?; Ok((compositor, Renderer::new(backend))) } @@ -178,49 +244,16 @@ impl iced_graphics::window::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = self.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu encoder"), - }, - ); - - let view = &frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - renderer.with_primitives(|backend, primitives| { - backend.present( - &self.device, - &self.queue, - &mut encoder, - Some(background_color), - view, - primitives, - viewport, - overlay, - ); - }); - - // Submit work - let _submission = self.queue.submit(Some(encoder.finish())); - frame.present(); - - Ok(()) - } - Err(error) => match error { - wgpu::SurfaceError::Timeout => { - Err(compositor::SurfaceError::Timeout) - } - wgpu::SurfaceError::Outdated => { - Err(compositor::SurfaceError::Outdated) - } - wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), - wgpu::SurfaceError::OutOfMemory => { - Err(compositor::SurfaceError::OutOfMemory) - } - }, - } + renderer.with_primitives(|backend, primitives| { + present( + self, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ) + }) } } -- cgit From 535d7a4d57e131e661587b36e41820dd6ccccc3e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 16:05:42 +0100 Subject: Implement basic presentation with `softbuffer` for `iced_tiny_skia` --- wgpu/src/window/compositor.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'wgpu') diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 7406bfb8..3a4a7123 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -201,11 +201,15 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> wgpu::Surface { #[allow(unsafe_code)] - unsafe { - self.instance.create_surface(window) - } + let mut surface = unsafe { self.instance.create_surface(window) }; + + self.configure_surface(&mut surface, width, height); + + surface } fn configure_surface( -- cgit From 5fd5d1cdf8e5354788dc40729c4565ef377d3bba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:34:26 +0100 Subject: Implement `Canvas` support for `iced_tiny_skia` --- wgpu/Cargo.toml | 15 +- wgpu/src/backend.rs | 7 +- wgpu/src/image.rs | 2 +- wgpu/src/layer.rs | 274 +++++++++++++++++ wgpu/src/layer/image.rs | 27 ++ wgpu/src/layer/mesh.rs | 93 ++++++ wgpu/src/layer/quad.rs | 30 ++ wgpu/src/layer/text.rs | 26 ++ wgpu/src/lib.rs | 15 +- wgpu/src/quad.rs | 3 +- wgpu/src/text.rs | 3 +- wgpu/src/triangle.rs | 71 +++-- wgpu/src/widget.rs | 9 + wgpu/src/widget/canvas.rs | 16 + wgpu/src/widget/canvas/cache.rs | 93 ++++++ wgpu/src/widget/canvas/frame.rs | 609 +++++++++++++++++++++++++++++++++++++ wgpu/src/widget/canvas/geometry.rs | 24 ++ 17 files changed, 1269 insertions(+), 48 deletions(-) create mode 100644 wgpu/src/layer.rs create mode 100644 wgpu/src/layer/image.rs create mode 100644 wgpu/src/layer/mesh.rs create mode 100644 wgpu/src/layer/quad.rs create mode 100644 wgpu/src/layer/text.rs create mode 100644 wgpu/src/widget.rs create mode 100644 wgpu/src/widget/canvas.rs create mode 100644 wgpu/src/widget/canvas/cache.rs create mode 100644 wgpu/src/widget/canvas/frame.rs create mode 100644 wgpu/src/widget/canvas/geometry.rs (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 632873a3..0bcef71c 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,8 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] +canvas = ["iced_graphics/canvas", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] @@ -62,10 +61,6 @@ version = "0.2" git = "https://github.com/hecrj/glyphon.git" rev = "65b481d758f50fd13fc21af2cc5ef62ddee64955" -[dependencies.tracing] -version = "0.1.6" -optional = true - [dependencies.encase] version = "0.3.0" features = ["glam"] @@ -73,6 +68,14 @@ features = ["glam"] [dependencies.glam] version = "0.21.3" +[dependencies.lyon] +version = "1.0" +optional = true + +[dependencies.tracing] +version = "0.1.6" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index e650d9a5..10dc5b4f 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -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() } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index db05d2ff..2159a3ec 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -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; diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs new file mode 100644 index 00000000..0840555a --- /dev/null +++ b/wgpu/src/layer.rs @@ -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, + + /// The triangle meshes of the [`Layer`]. + pub meshes: Vec>, + + /// The text of the [`Layer`]. + pub text: Vec>, + + /// The images of the [`Layer`]. + pub images: Vec, +} + +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], 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 { + 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, + 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 (?) + } + } + } +} diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs new file mode 100644 index 00000000..3eff2397 --- /dev/null +++ b/wgpu/src/layer/image.rs @@ -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, + + /// The bounds of the image. + bounds: Rectangle, + }, +} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs new file mode 100644 index 00000000..5c1e41ad --- /dev/null +++ b/wgpu/src/layer/mesh.rs @@ -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, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// 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, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + + /// 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 { + 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 + }) +} diff --git a/wgpu/src/layer/quad.rs b/wgpu/src/layer/quad.rs new file mode 100644 index 00000000..0d8bde9d --- /dev/null +++ b/wgpu/src/layer/quad.rs @@ -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 {} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs new file mode 100644 index 00000000..38d62616 --- /dev/null +++ b/wgpu/src/layer/text.rs @@ -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, +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9da40572..8be12602 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -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"))] diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 246cc5e1..8a568968 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,5 +1,6 @@ +use crate::layer; use crate::{Buffer, Transformation}; -use iced_graphics::layer; + use iced_native::Rectangle; use bytemuck::{Pod, Zeroable}; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index dea6ab18..0dc8a64c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -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}; diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 4b4fa16d..706e4282 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -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, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, 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, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub storage: dynamic::Buffer, 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::() - 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, diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs new file mode 100644 index 00000000..8d05041e --- /dev/null +++ b/wgpu/src/widget.rs @@ -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; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs new file mode 100644 index 00000000..41444fcf --- /dev/null +++ b/wgpu/src/widget/canvas.rs @@ -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, +}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs new file mode 100644 index 00000000..09b26b90 --- /dev/null +++ b/wgpu/src/widget/canvas/cache.rs @@ -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, + }, +} + +/// 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, +} + +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(), + } + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs new file mode 100644 index 00000000..987570ec --- /dev/null +++ b/wgpu/src/widget/canvas/frame.rs @@ -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, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +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 { + 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 { + 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, + 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) { + 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, + ) { + 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>) { + 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) { + 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 { + 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 + 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 + 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 + 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 + 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), + }, + ); + }) +} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..e8ac621d --- /dev/null +++ b/wgpu/src/widget/canvas/geometry.rs @@ -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 + } +} -- cgit From bbeaf10c04a922af5c1c3b898f0c4301d23feab0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 03:55:07 +0100 Subject: Mark `Primitive` as `non-exhaustive` in `iced_graphics` --- wgpu/src/layer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'wgpu') diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 0840555a..69fcf899 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -265,9 +265,8 @@ impl<'a> Layer<'a> { current_layer, ); } - Primitive::Fill { .. } | Primitive::Stroke { .. } => { + _ => { // Unsupported! - // TODO: Draw a placeholder (?) } } } -- cgit From d13d19ba3569560edd67f20b48f37548d10ceee9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:00:44 +0100 Subject: Rename `canvas::frame` to `canvas` in `iced_wgpu` --- wgpu/src/canvas.rs | 608 ++++++++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 5 +- wgpu/src/widget.rs | 9 - wgpu/src/widget/canvas.rs | 16 - wgpu/src/widget/canvas/cache.rs | 93 ------ wgpu/src/widget/canvas/frame.rs | 609 ------------------------------------- wgpu/src/widget/canvas/geometry.rs | 24 -- 7 files changed, 611 insertions(+), 753 deletions(-) create mode 100644 wgpu/src/canvas.rs delete mode 100644 wgpu/src/widget.rs delete mode 100644 wgpu/src/widget/canvas.rs delete mode 100644 wgpu/src/widget/canvas/cache.rs delete mode 100644 wgpu/src/widget/canvas/frame.rs delete mode 100644 wgpu/src/widget/canvas/geometry.rs (limited to 'wgpu') diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs new file mode 100644 index 00000000..e8d540c3 --- /dev/null +++ b/wgpu/src/canvas.rs @@ -0,0 +1,608 @@ +use iced_graphics::primitive::{self, Primitive}; +use iced_native::widget::canvas::fill::{self, Fill}; +use iced_native::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, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +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 { + 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 { + 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, + 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) { + 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, + ) { + 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>) { + 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) { + 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 { + 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 + 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 + 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 + 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 + 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), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 8be12602..31db16a8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -37,12 +37,13 @@ #![forbid(rust_2018_idioms)] #![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; +#[cfg(feature = "canvas")] +pub mod canvas; + mod backend; mod buffer; mod quad; diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs deleted file mode 100644 index 8d05041e..00000000 --- a/wgpu/src/widget.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! 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; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs deleted file mode 100644 index 41444fcf..00000000 --- a/wgpu/src/widget/canvas.rs +++ /dev/null @@ -1,16 +0,0 @@ -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, -}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs deleted file mode 100644 index 09b26b90..00000000 --- a/wgpu/src/widget/canvas/cache.rs +++ /dev/null @@ -1,93 +0,0 @@ -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, - }, -} - -/// 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, -} - -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(), - } - } -} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs deleted file mode 100644 index 987570ec..00000000 --- a/wgpu/src/widget/canvas/frame.rs +++ /dev/null @@ -1,609 +0,0 @@ -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, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -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 { - 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 { - 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, - 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) { - 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, - ) { - 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>) { - 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) { - 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 { - 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 - 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 - 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 - 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 - 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), - }, - ); - }) -} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs deleted file mode 100644 index e8ac621d..00000000 --- a/wgpu/src/widget/canvas/geometry.rs +++ /dev/null @@ -1,24 +0,0 @@ -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 - } -} -- cgit From 6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:57:55 +0100 Subject: Move `Canvas` and `QRCode` to `iced` crate Rename `canvas` modules to `geometry` in graphics subcrates --- wgpu/Cargo.toml | 2 +- wgpu/src/backend.rs | 2 - wgpu/src/canvas.rs | 608 --------------------------------------------------- wgpu/src/geometry.rs | 608 +++++++++++++++++++++++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 4 +- 5 files changed, 611 insertions(+), 613 deletions(-) delete mode 100644 wgpu/src/canvas.rs create mode 100644 wgpu/src/geometry.rs (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0bcef71c..2e39a9e7 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,7 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas", "lyon"] +geometry = ["iced_graphics/geometry", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 10dc5b4f..6f39a5fe 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -329,8 +329,6 @@ impl Backend { } impl iced_graphics::Backend for Backend { - type Geometry = (); - fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache() } diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs deleted file mode 100644 index e8d540c3..00000000 --- a/wgpu/src/canvas.rs +++ /dev/null @@ -1,608 +0,0 @@ -use iced_graphics::primitive::{self, Primitive}; -use iced_native::widget::canvas::fill::{self, Fill}; -use iced_native::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, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -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 { - 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 { - 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, - 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) { - 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, - ) { - 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>) { - 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) { - 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 { - 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 - 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 - 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 - 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 - 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), - }, - ); - }) -} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs new file mode 100644 index 00000000..11e8126f --- /dev/null +++ b/wgpu/src/geometry.rs @@ -0,0 +1,608 @@ +use iced_graphics::geometry::fill::{self, Fill}; +use iced_graphics::geometry::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; +use iced_graphics::primitive::{self, Primitive}; +use iced_graphics::{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, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +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 { + 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 { + 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, + 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) { + 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, + ) { + 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>) { + 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) { + 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 { + 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 + 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 + 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 + 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 + 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), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 31db16a8..4439b185 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -41,8 +41,8 @@ pub mod layer; pub mod settings; pub mod window; -#[cfg(feature = "canvas")] -pub mod canvas; +#[cfg(feature = "geometry")] +pub mod geometry; mod backend; mod buffer; -- cgit From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- wgpu/Cargo.toml | 4 ---- wgpu/src/backend.rs | 20 +++++++++----------- wgpu/src/geometry.rs | 8 ++++---- wgpu/src/image.rs | 21 +++++++++++---------- wgpu/src/image/atlas.rs | 4 ++-- wgpu/src/image/atlas/allocation.rs | 3 +-- wgpu/src/image/atlas/allocator.rs | 4 ++-- wgpu/src/image/atlas/entry.rs | 5 ++--- wgpu/src/layer.rs | 9 +++------ wgpu/src/layer/image.rs | 6 +++--- wgpu/src/layer/mesh.rs | 4 ++-- wgpu/src/layer/text.rs | 3 ++- wgpu/src/lib.rs | 15 +++------------ wgpu/src/quad.rs | 6 +++--- wgpu/src/settings.rs | 5 ++--- wgpu/src/text.rs | 14 ++++++-------- wgpu/src/triangle.rs | 24 +++++++++++------------- wgpu/src/triangle/msaa.rs | 4 ++-- wgpu/src/window/compositor.rs | 10 ++++++---- 19 files changed, 74 insertions(+), 95 deletions(-) (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 2e39a9e7..f3ea66dd 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -48,10 +48,6 @@ features = ["std"] version = "1.9" features = ["derive"] -[dependencies.iced_native] -version = "0.9" -path = "../native" - [dependencies.iced_graphics] version = "0.7" path = "../graphics" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 6f39a5fe..9c9a1b76 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,10 +1,11 @@ +use crate::core; +use crate::core::{Color, Font, Point, Size}; +use crate::graphics::backend; +use crate::graphics::{Primitive, Transformation, Viewport}; use crate::quad; use crate::text; use crate::triangle; -use crate::{Layer, Primitive, Settings, Transformation}; - -use iced_graphics::backend; -use iced_graphics::{Color, Font, Size, Viewport}; +use crate::{Layer, Settings}; #[cfg(feature = "tracing")] use tracing::info_span; @@ -363,9 +364,9 @@ impl backend::Text for Backend { size: f32, font: Font, bounds: Size, - point: iced_native::Point, + point: Point, nearest_only: bool, - ) -> Option { + ) -> Option { self.text_pipeline.hit_test( contents, size, @@ -383,17 +384,14 @@ impl backend::Text for Backend { #[cfg(feature = "image")] impl backend::Image for Backend { - fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { + fn dimensions(&self, handle: &core::image::Handle) -> Size { self.image_pipeline.dimensions(handle) } } #[cfg(feature = "svg")] impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &iced_native::svg::Handle, - ) -> Size { + fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size { self.image_pipeline.viewport_dimensions(handle) } } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 11e8126f..59ec31fe 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,9 +1,9 @@ -use iced_graphics::geometry::fill::{self, Fill}; -use iced_graphics::geometry::{ +use crate::core::{Gradient, Point, Rectangle, Size, Vector}; +use crate::graphics::geometry::fill::{self, Fill}; +use crate::graphics::geometry::{ LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; -use iced_graphics::primitive::{self, Primitive}; -use iced_graphics::{Gradient, Point, Rectangle, Size, Vector}; +use crate::graphics::primitive::{self, Primitive}; use lyon::geom::euclid; use lyon::tessellation; diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 2159a3ec..5eaa9a86 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -1,16 +1,17 @@ mod atlas; -#[cfg(feature = "image")] -use iced_graphics::image::raster; - -#[cfg(feature = "svg")] -use iced_graphics::image::vector; +use atlas::Atlas; +use crate::core::{Rectangle, Size}; +use crate::graphics::Transformation; use crate::layer; -use crate::{Buffer, Transformation}; -use atlas::Atlas; +use crate::Buffer; + +#[cfg(feature = "image")] +use crate::graphics::image::raster; -use iced_native::{Rectangle, Size}; +#[cfg(feature = "svg")] +use crate::graphics::image::vector; use std::cell::RefCell; use std::mem; @@ -18,10 +19,10 @@ use std::mem; use bytemuck::{Pod, Zeroable}; #[cfg(feature = "image")] -use iced_native::image; +use crate::core::image; #[cfg(feature = "svg")] -use iced_native::svg; +use crate::core::svg; #[cfg(feature = "tracing")] use tracing::info_span; diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 7df67abd..0a17ca33 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -12,8 +12,8 @@ use allocator::Allocator; pub const SIZE: u32 = 2048; -use iced_graphics::image; -use iced_graphics::Size; +use crate::core::Size; +use crate::graphics::image; use std::num::NonZeroU32; diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs index 43aba875..11289771 100644 --- a/wgpu/src/image/atlas/allocation.rs +++ b/wgpu/src/image/atlas/allocation.rs @@ -1,7 +1,6 @@ +use crate::core::Size; use crate::image::atlas::{self, allocator}; -use iced_graphics::Size; - #[derive(Debug)] pub enum Allocation { Partial { diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs index 03effdcb..204a5c26 100644 --- a/wgpu/src/image/atlas/allocator.rs +++ b/wgpu/src/image/atlas/allocator.rs @@ -46,10 +46,10 @@ impl Region { (rectangle.min.x as u32, rectangle.min.y as u32) } - pub fn size(&self) -> iced_graphics::Size { + pub fn size(&self) -> crate::core::Size { let size = self.allocation.rectangle.size(); - iced_graphics::Size::new(size.width as u32, size.height as u32) + crate::core::Size::new(size.width as u32, size.height as u32) } } diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs index 69c05a50..4b06bd95 100644 --- a/wgpu/src/image/atlas/entry.rs +++ b/wgpu/src/image/atlas/entry.rs @@ -1,8 +1,7 @@ +use crate::core::Size; +use crate::graphics::image; use crate::image::atlas; -use iced_graphics::image; -use iced_graphics::Size; - #[derive(Debug)] pub enum Entry { Contiguous(atlas::Allocation), diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 69fcf899..cb9d5e2f 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -10,12 +10,9 @@ 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, -}; +use crate::core::alignment; +use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector}; +use crate::graphics::{Primitive, Viewport}; /// A group of primitives that should be clipped together. #[derive(Debug)] diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs index 3eff2397..0de589f8 100644 --- a/wgpu/src/layer/image.rs +++ b/wgpu/src/layer/image.rs @@ -1,6 +1,6 @@ -use crate::{Color, Rectangle}; - -use iced_native::{image, svg}; +use crate::core::image; +use crate::core::svg; +use crate::core::{Color, Rectangle}; /// A raster or vector image. #[derive(Debug, Clone)] diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs index 5c1e41ad..9dd14391 100644 --- a/wgpu/src/layer/mesh.rs +++ b/wgpu/src/layer/mesh.rs @@ -1,6 +1,6 @@ //! A collection of triangle primitives. -use crate::primitive; -use crate::{Gradient, Point, Rectangle}; +use crate::core::{Gradient, Point, Rectangle}; +use crate::graphics::primitive; /// A mesh of triangles. #[derive(Debug, Clone, Copy)] diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index 38d62616..fdbdaafb 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,4 +1,5 @@ -use crate::{alignment, Color, Font, Rectangle}; +use crate::core::alignment; +use crate::core::{Color, Font, Rectangle}; /// A paragraph of text. #[derive(Debug, Clone, Copy)] diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 4439b185..473f3621 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -50,25 +50,17 @@ mod quad; mod text; mod triangle; -pub use iced_graphics::primitive; -pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Gradient, Point, Rectangle, Size, Vector, - Viewport, -}; -pub use iced_native::alignment; +pub use iced_graphics as graphics; +pub use iced_graphics::core; -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 buffer::Buffer; -use iced_graphics::Transformation; - #[cfg(any(feature = "image", feature = "svg"))] mod image; @@ -76,5 +68,4 @@ mod image; /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = - iced_graphics::Renderer; +pub type Renderer = iced_graphics::Renderer; diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 8a568968..b55216d7 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,7 +1,7 @@ +use crate::core::Rectangle; +use crate::graphics::Transformation; use crate::layer; -use crate::{Buffer, Transformation}; - -use iced_native::Rectangle; +use crate::Buffer; use bytemuck::{Pod, Zeroable}; use std::mem; diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index bd9cf473..7c0750ef 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,7 +1,6 @@ //! Configure a renderer. -pub use crate::Antialiasing; - -use crate::Font; +use crate::core::Font; +use crate::graphics::Antialiasing; /// The settings of a [`Backend`]. /// diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 0dc8a64c..d7a27f3a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,10 +1,8 @@ +use crate::core::alignment; +use crate::core::text::Hit; +use crate::core::{Color, Font, Point, Rectangle, Size}; use crate::layer::Text; -pub use iced_native::text::Hit; - -use iced_native::alignment; -use iced_native::{Color, Font, Rectangle, Size}; - use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::cell::RefCell; @@ -269,9 +267,9 @@ impl Pipeline { &self, content: &str, size: f32, - font: iced_native::Font, - bounds: iced_native::Size, - point: iced_native::Point, + font: Font, + bounds: Size, + point: Point, _nearest_only: bool, ) -> Option { self.system.as_ref().unwrap().with(|fields| { diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 706e4282..9fa521d7 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -2,11 +2,9 @@ mod msaa; use crate::buffer::r#static::Buffer; +use crate::core::{Gradient, Size}; +use crate::graphics::{Antialiasing, Transformation}; use crate::layer::mesh::{self, Mesh}; -use crate::settings; -use crate::Transformation; - -use iced_graphics::Size; #[cfg(feature = "tracing")] use tracing::info_span; @@ -137,7 +135,7 @@ impl Layer { gradient_vertex_offset += written_bytes; match gradient { - iced_graphics::Gradient::Linear(linear) => { + Gradient::Linear(linear) => { use glam::{IVec4, Vec4}; let start_offset = self.gradient.color_stop_offset; @@ -319,7 +317,7 @@ impl Pipeline { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, - antialiasing: Option, + antialiasing: Option, ) -> Pipeline { Pipeline { blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), @@ -453,7 +451,7 @@ fn primitive_state() -> wgpu::PrimitiveState { } fn multisample_state( - antialiasing: Option, + antialiasing: Option, ) -> wgpu::MultisampleState { wgpu::MultisampleState { count: antialiasing.map(|a| a.sample_count()).unwrap_or(1), @@ -465,11 +463,11 @@ fn multisample_state( mod solid { use crate::buffer::dynamic; use crate::buffer::r#static::Buffer; - use crate::settings; + use crate::graphics::primitive; + use crate::graphics::{Antialiasing, Transformation}; use crate::triangle; + use encase::ShaderType; - use iced_graphics::primitive; - use iced_graphics::Transformation; #[derive(Debug)] pub struct Pipeline { @@ -550,7 +548,7 @@ mod solid { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, - antialiasing: Option, + antialiasing: Option, ) -> Self { let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { @@ -633,7 +631,7 @@ mod solid { mod gradient { use crate::buffer::dynamic; use crate::buffer::r#static::Buffer; - use crate::settings; + use crate::graphics::Antialiasing; use crate::triangle; use encase::ShaderType; @@ -755,7 +753,7 @@ mod gradient { pub(super) fn new( device: &wgpu::Device, format: wgpu::TextureFormat, - antialiasing: Option, + antialiasing: Option, ) -> Self { let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index a3016ff8..7144125c 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -1,4 +1,4 @@ -use crate::settings; +use crate::graphics; #[derive(Debug)] pub struct Blit { @@ -14,7 +14,7 @@ impl Blit { pub fn new( device: &wgpu::Device, format: wgpu::TextureFormat, - antialiasing: settings::Antialiasing, + antialiasing: graphics::Antialiasing, ) -> Blit { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 3a4a7123..a67ac3c0 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,10 +1,12 @@ //! Connect a window with a renderer. -use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; +use crate::core::Color; +use crate::graphics; +use crate::graphics::compositor; +use crate::graphics::{Error, Primitive, Viewport}; +use crate::{Backend, Renderer, Settings}; use futures::stream::{self, StreamExt}; -use iced_graphics::window::compositor; -use iced_native::futures; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; @@ -184,7 +186,7 @@ pub fn present>( } } -impl iced_graphics::window::Compositor for Compositor { +impl graphics::Compositor for Compositor { type Settings = Settings; type Renderer = Renderer; type Surface = wgpu::Surface; -- cgit From 5fed065dc3aa3d2f9ff8d229cbffe003a89ba033 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:56:10 +0100 Subject: Update `glyphon` in `iced_wgpu` --- wgpu/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 4cdd6c68..50a81a91 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -55,7 +55,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "810bc979f9005e2bd343b72b980e57e46174283f" +rev = "edd23695ad53db5f89d455c3c130172fd107d6a2" [dependencies.encase] version = "0.3.0" -- cgit From 06bbcc310e6e759a0839df6ca391ea5e0f0ee609 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Mar 2023 06:40:20 +0100 Subject: Move `webgl` feature selection for `wgpu` into `iced_wgpu` --- wgpu/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 50a81a91..5601c0de 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -23,7 +23,6 @@ dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] geometry = ["iced_graphics/geometry", "lyon"] spirv = ["wgpu/spirv"] -webgl = ["wgpu/webgl"] [dependencies] wgpu = "0.14" @@ -36,6 +35,9 @@ once_cell = "1.0" rustc-hash = "1.1" ouroboros = "0.15" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wgpu = { version = "0.14", features = ["webgl"] } + [dependencies.twox-hash] version = "1.6" default-features = false -- cgit From 3a26baa564524b0f25c5cb180b592c8b004b68a9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Mar 2023 03:47:49 +0100 Subject: Remove `image` abstractions in `iced_graphics` --- wgpu/Cargo.toml | 20 ++--- wgpu/src/backend.rs | 2 +- wgpu/src/image.rs | 39 ++++----- wgpu/src/image/atlas.rs | 190 ++++++++++++++++++++---------------------- wgpu/src/image/atlas/entry.rs | 6 +- wgpu/src/image/raster.rs | 121 +++++++++++++++++++++++++++ wgpu/src/image/vector.rs | 181 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 420 insertions(+), 139 deletions(-) create mode 100644 wgpu/src/image/raster.rs create mode 100644 wgpu/src/image/vector.rs (limited to 'wgpu') diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 5601c0de..6a313d4f 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -8,21 +8,9 @@ license = "MIT AND OFL-1.1" repository = "https://github.com/iced-rs/iced" [features] -svg = ["iced_graphics/svg"] -image = ["iced_graphics/image"] -png = ["iced_graphics/png"] -jpeg = ["iced_graphics/jpeg"] -jpeg_rayon = ["iced_graphics/jpeg_rayon"] -gif = ["iced_graphics/gif"] -webp = ["iced_graphics/webp"] -pnm = ["iced_graphics/pnm"] -ico = ["iced_graphics/ico"] -bmp = ["iced_graphics/bmp"] -hdr = ["iced_graphics/hdr"] -dds = ["iced_graphics/dds"] -farbfeld = ["iced_graphics/farbfeld"] geometry = ["iced_graphics/geometry", "lyon"] -spirv = ["wgpu/spirv"] +image = ["iced_graphics/image"] +svg = ["resvg"] [dependencies] wgpu = "0.14" @@ -70,6 +58,10 @@ version = "0.21.3" version = "1.0" optional = true +[dependencies.resvg] +version = "0.29" +optional = true + [dependencies.tracing] version = "0.1.6" optional = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 9c9a1b76..88c58554 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -119,7 +119,7 @@ impl Backend { self.triangle_pipeline.end_frame(); #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.end_frame(device, queue, encoder); + self.image_pipeline.end_frame(); } fn prepare_text( diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 5eaa9a86..4163e338 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -1,5 +1,11 @@ mod atlas; +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + use atlas::Atlas; use crate::core::{Rectangle, Size}; @@ -7,12 +13,6 @@ use crate::graphics::Transformation; use crate::layer; use crate::Buffer; -#[cfg(feature = "image")] -use crate::graphics::image::raster; - -#[cfg(feature = "svg")] -use crate::graphics::image::vector; - use std::cell::RefCell; use std::mem; @@ -30,9 +30,9 @@ use tracing::info_span; #[derive(Debug)] pub struct Pipeline { #[cfg(feature = "image")] - raster_cache: RefCell>, + raster_cache: RefCell, #[cfg(feature = "svg")] - vector_cache: RefCell>, + vector_cache: RefCell, pipeline: wgpu::RenderPipeline, vertices: wgpu::Buffer, @@ -368,8 +368,10 @@ impl Pipeline { #[cfg(feature = "image")] layer::Image::Raster { handle, bounds } => { if let Some(atlas_entry) = raster_cache.upload( + device, + queue, + encoder, handle, - &mut (device, queue, encoder), &mut self.texture_atlas, ) { add_instances( @@ -392,11 +394,13 @@ impl Pipeline { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = vector_cache.upload( + device, + queue, + encoder, handle, *color, size, _scale, - &mut (device, queue, encoder), &mut self.texture_atlas, ) { add_instances( @@ -477,21 +481,12 @@ impl Pipeline { } } - pub fn end_frame( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - ) { + pub fn end_frame(&mut self) { #[cfg(feature = "image")] - self.raster_cache - .borrow_mut() - .trim(&mut self.texture_atlas, &mut (device, queue, encoder)); + self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); #[cfg(feature = "svg")] - self.vector_cache - .borrow_mut() - .trim(&mut self.texture_atlas, &mut (device, queue, encoder)); + self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); self.prepare_layer = 0; } diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 0a17ca33..c00b8cef 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -13,7 +13,6 @@ use allocator::Allocator; pub const SIZE: u32 = 2048; use crate::core::Size; -use crate::graphics::image; use std::num::NonZeroU32; @@ -64,6 +63,97 @@ impl Atlas { self.layers.len() } + pub fn upload( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + width: u32, + height: u32, + data: &[u8], + ) -> Option { + let entry = { + let current_size = self.layers.len(); + let entry = self.allocate(width, height)?; + + // We grow the internal texture after allocating if necessary + let new_layers = self.layers.len() - current_size; + self.grow(new_layers, device, encoder); + + entry + }; + + log::info!("Allocated atlas entry: {:?}", entry); + + // It is a webgpu requirement that: + // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 + // So we calculate padded_width by rounding width up to the next + // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padding = (align - (4 * width) % align) % align; + let padded_width = (4 * width + padding) as usize; + let padded_data_size = padded_width * height as usize; + + let mut padded_data = vec![0; padded_data_size]; + + for row in 0..height as usize { + let offset = row * padded_width; + + padded_data[offset..offset + 4 * width as usize].copy_from_slice( + &data[row * 4 * width as usize..(row + 1) * 4 * width as usize], + ) + } + + match &entry { + Entry::Contiguous(allocation) => { + self.upload_allocation( + &padded_data, + width, + height, + padding, + 0, + allocation, + queue, + ); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + let (x, y) = fragment.position; + let offset = (y * padded_width as u32 + 4 * x) as usize; + + self.upload_allocation( + &padded_data, + width, + height, + padding, + offset, + &fragment.allocation, + queue, + ); + } + } + } + + log::info!("Current atlas: {:?}", self); + + Some(entry) + } + + pub fn remove(&mut self, entry: &Entry) { + log::info!("Removing atlas entry: {:?}", entry); + + match entry { + Entry::Contiguous(allocation) => { + self.deallocate(allocation); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + self.deallocate(&fragment.allocation); + } + } + } + } + fn allocate(&mut self, width: u32, height: u32) -> Option { // Allocate one layer if texture fits perfectly if width == SIZE && height == SIZE { @@ -296,101 +386,3 @@ impl Atlas { }); } } - -impl image::Storage for Atlas { - type Entry = Entry; - type State<'a> = ( - &'a wgpu::Device, - &'a wgpu::Queue, - &'a mut wgpu::CommandEncoder, - ); - - fn upload( - &mut self, - width: u32, - height: u32, - data: &[u8], - (device, queue, encoder): &mut Self::State<'_>, - ) -> Option { - let entry = { - let current_size = self.layers.len(); - let entry = self.allocate(width, height)?; - - // We grow the internal texture after allocating if necessary - let new_layers = self.layers.len() - current_size; - self.grow(new_layers, device, encoder); - - entry - }; - - log::info!("Allocated atlas entry: {:?}", entry); - - // It is a webgpu requirement that: - // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0 - // So we calculate padded_width by rounding width up to the next - // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT. - let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let padding = (align - (4 * width) % align) % align; - let padded_width = (4 * width + padding) as usize; - let padded_data_size = padded_width * height as usize; - - let mut padded_data = vec![0; padded_data_size]; - - for row in 0..height as usize { - let offset = row * padded_width; - - padded_data[offset..offset + 4 * width as usize].copy_from_slice( - &data[row * 4 * width as usize..(row + 1) * 4 * width as usize], - ) - } - - match &entry { - Entry::Contiguous(allocation) => { - self.upload_allocation( - &padded_data, - width, - height, - padding, - 0, - allocation, - queue, - ); - } - Entry::Fragmented { fragments, .. } => { - for fragment in fragments { - let (x, y) = fragment.position; - let offset = (y * padded_width as u32 + 4 * x) as usize; - - self.upload_allocation( - &padded_data, - width, - height, - padding, - offset, - &fragment.allocation, - queue, - ); - } - } - } - - log::info!("Current atlas: {:?}", self); - - Some(entry) - } - - fn remove(&mut self, entry: &Entry, _: &mut Self::State<'_>) { - log::info!("Removing atlas entry: {:?}", entry); - - match entry { - Entry::Contiguous(allocation) => { - self.deallocate(allocation); - } - Entry::Fragmented { fragments, .. } => { - for fragment in fragments { - self.deallocate(&fragment.allocation); - } - } - } - } -} diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs index 4b06bd95..7e4c92a2 100644 --- a/wgpu/src/image/atlas/entry.rs +++ b/wgpu/src/image/atlas/entry.rs @@ -1,5 +1,4 @@ use crate::core::Size; -use crate::graphics::image; use crate::image::atlas; #[derive(Debug)] @@ -11,8 +10,9 @@ pub enum Entry { }, } -impl image::storage::Entry for Entry { - fn size(&self) -> Size { +impl Entry { + #[cfg(feature = "image")] + pub fn size(&self) -> Size { match self { Entry::Contiguous(allocation) => allocation.size(), Entry::Fragmented { size, .. } => *size, diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs new file mode 100644 index 00000000..9b38dce4 --- /dev/null +++ b/wgpu/src/image/raster.rs @@ -0,0 +1,121 @@ +use crate::core::image; +use crate::core::Size; +use crate::graphics; +use crate::graphics::image::image_rs; +use crate::image::atlas::{self, Atlas}; + +use std::collections::{HashMap, HashSet}; + +/// Entry in cache corresponding to an image handle +#[derive(Debug)] +pub enum Memory { + /// Image data on host + Host(image_rs::ImageBuffer, Vec>), + /// Storage entry + Device(atlas::Entry), + /// Image not found + NotFound, + /// Invalid image data + Invalid, +} + +impl Memory { + /// Width and height of image + pub fn dimensions(&self) -> Size { + match self { + Memory::Host(image) => { + let (width, height) = image.dimensions(); + + Size::new(width, height) + } + Memory::Device(entry) => entry.size(), + Memory::NotFound => Size::new(1, 1), + Memory::Invalid => Size::new(1, 1), + } + } +} + +/// Caches image raster data +#[derive(Debug, Default)] +pub struct Cache { + map: HashMap, + hits: HashSet, +} + +impl Cache { + /// Load image + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { + if self.contains(handle) { + return self.get(handle).unwrap(); + } + + let memory = match graphics::image::load(handle) { + Ok(image) => Memory::Host(image.to_rgba8()), + Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound, + Err(_) => Memory::Invalid, + }; + + self.insert(handle, memory); + self.get(handle).unwrap() + } + + /// Load image and upload raster data + pub fn upload( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + handle: &image::Handle, + atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let memory = self.load(handle); + + if let Memory::Host(image) = memory { + let (width, height) = image.dimensions(); + + let entry = + atlas.upload(device, queue, encoder, width, height, image)?; + + *memory = Memory::Device(entry); + } + + if let Memory::Device(allocation) = memory { + Some(allocation) + } else { + None + } + } + + /// Trim cache misses from cache + pub fn trim(&mut self, atlas: &mut Atlas) { + let hits = &self.hits; + + self.map.retain(|k, memory| { + let retain = hits.contains(k); + + if !retain { + if let Memory::Device(entry) = memory { + atlas.remove(entry); + } + } + + retain + }); + + self.hits.clear(); + } + + fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); + + self.map.get_mut(&handle.id()) + } + + fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + } + + fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } +} diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs new file mode 100644 index 00000000..3624e46b --- /dev/null +++ b/wgpu/src/image/vector.rs @@ -0,0 +1,181 @@ +use crate::core::svg; +use crate::core::{Color, Size}; +use crate::image::atlas::{self, Atlas}; + +use resvg::tiny_skia; +use resvg::usvg; +use std::collections::{HashMap, HashSet}; +use std::fs; + +/// Entry in cache corresponding to an svg handle +pub enum Svg { + /// Parsed svg + Loaded(usvg::Tree), + /// Svg not found or failed to parse + NotFound, +} + +impl Svg { + /// Viewport width and height + pub fn viewport_dimensions(&self) -> Size { + match self { + Svg::Loaded(tree) => { + let size = tree.size; + + Size::new(size.width() as u32, size.height() as u32) + } + Svg::NotFound => Size::new(1, 1), + } + } +} + +/// Caches svg vector and raster data +#[derive(Debug, Default)] +pub struct Cache { + svgs: HashMap, + rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, + svg_hits: HashSet, + rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>, +} + +type ColorFilter = Option<[u8; 4]>; + +impl Cache { + /// Load svg + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + let svg = match handle.data() { + svg::Data::Path(path) => { + let tree = fs::read_to_string(path).ok().and_then(|contents| { + usvg::Tree::from_str(&contents, &usvg::Options::default()) + .ok() + }); + + tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) + } + svg::Data::Bytes(bytes) => { + match usvg::Tree::from_data(bytes, &usvg::Options::default()) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + } + } + }; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + /// Load svg and upload raster data + pub fn upload( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + handle: &svg::Handle, + color: Option, + [width, height]: [f32; 2], + scale: f32, + atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * width).ceil() as u32, + (scale * height).ceil() as u32, + ); + + let color = color.map(Color::into_rgba8); + let key = (id, width, height, color); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&key) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert(key); + + return self.rasterized.get(&key); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let mut img = tiny_skia::Pixmap::new(width, height)?; + + resvg::render( + tree, + if width > height { + usvg::FitTo::Width(width) + } else { + usvg::FitTo::Height(height) + }, + tiny_skia::Transform::default(), + img.as_mut(), + )?; + + let mut rgba = img.take(); + + if let Some(color) = color { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + rgba[0] = color[0]; + rgba[1] = color[1]; + rgba[2] = color[2]; + } + }); + } + + let allocation = atlas + .upload(device, queue, encoder, width, height, &rgba)?; + + log::debug!("allocating {} {}x{}", id, width, height); + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert(key); + let _ = self.rasterized.insert(key, allocation); + + self.rasterized.get(&key) + } + Svg::NotFound => None, + } + } + + /// Load svg and upload raster data + pub fn trim(&mut self, atlas: &mut Atlas) { + let svg_hits = &self.svg_hits; + let rasterized_hits = &self.rasterized_hits; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + atlas.remove(entry); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} -- cgit