diff options
43 files changed, 2617 insertions, 2036 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 5de65154..49ca3252 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [alias] -lint = "clippy --workspace --no-deps -- -D warnings" +lint = "clippy --workspace --benches --all-features --no-deps -- -D warnings" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4107e618..ceac39e7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,14 +1,6 @@ name: Check on: [push, pull_request] jobs: - widget: - runs-on: ubuntu-latest - steps: - - uses: hecrj/setup-rust-action@v2 - - uses: actions/checkout@master - - name: Check standalone `iced_widget` crate - run: cargo check --package iced_widget --features image,svg,canvas - wasm: runs-on: ubuntu-latest env: @@ -27,3 +19,11 @@ jobs: run: cargo build --package todos --target wasm32-unknown-unknown - name: Check compilation of `integration` example run: cargo build --package integration --target wasm32-unknown-unknown + + widget: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v2 + - uses: actions/checkout@master + - name: Check standalone `iced_widget` crate + run: cargo check --package iced_widget --features image,svg,canvas diff --git a/benches/wgpu.rs b/benches/wgpu.rs index 2c117858..780c6bc2 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -12,18 +12,15 @@ use iced_wgpu::Renderer; criterion_main!(benches); criterion_group!(benches, wgpu_benchmark); +#[allow(unused_results)] pub fn wgpu_benchmark(c: &mut Criterion) { - let _ = c - .bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); - - let _ = c.bench_function("wgpu — canvas (heavy)", |b| { - benchmark(b, scene(1_000)) - }); + c.bench_function("wgpu — canvas (light)", |b| benchmark(b, scene(10))); + c.bench_function("wgpu — canvas (heavy)", |b| benchmark(b, scene(1_000))); } -fn benchmark<'a>( +fn benchmark( bencher: &mut Bencher<'_>, - widget: Element<'a, (), Theme, Renderer>, + widget: Element<'_, (), Theme, Renderer>, ) { use iced_futures::futures::executor; use iced_wgpu::graphics; @@ -58,22 +55,25 @@ fn benchmark<'a>( let format = wgpu::TextureFormat::Bgra8UnormSrgb; - let backend = iced_wgpu::Backend::new( + let mut engine = iced_wgpu::Engine::new( &adapter, &device, &queue, + format, + Some(Antialiasing::MSAAx4), + ); + + let mut renderer = Renderer::new( iced_wgpu::Settings { present_mode: wgpu::PresentMode::Immediate, - internal_backend: wgpu::Backends::all(), + backends: wgpu::Backends::all(), default_font: Font::DEFAULT, default_text_size: Pixels::from(16), antialiasing: Some(Antialiasing::MSAAx4), }, - format, + &engine, ); - let mut renderer = Renderer::new(backend, Font::DEFAULT, Pixels::from(16)); - let viewport = graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0); @@ -117,25 +117,20 @@ fn benchmark<'a>( label: None, }); - renderer.with_primitives(|backend, primitives| { - backend.present::<&str>( - &device, - &queue, - &mut encoder, - Some(Color::BLACK), - format, - &texture_view, - primitives, - &viewport, - &[], - ); - - let submission = queue.submit(Some(encoder.finish())); - backend.recall(); - - let _ = - device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); - }); + renderer.present::<&str>( + &mut engine, + &device, + &queue, + &mut encoder, + Some(Color::BLACK), + format, + &texture_view, + &viewport, + &[], + ); + + let submission = engine.submit(&queue, encoder); + let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); }); } diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index c1c2eeac..446d3769 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -16,24 +16,32 @@ pub struct Rectangle<T = f32> { pub height: T, } -impl Rectangle<f32> { - /// Creates a new [`Rectangle`] with its top-left corner in the given - /// [`Point`] and with the provided [`Size`]. - pub fn new(top_left: Point, size: Size) -> Self { +impl<T> Rectangle<T> +where + T: Default, +{ + /// Creates a new [`Rectangle`] with its top-left corner at the origin + /// and with the provided [`Size`]. + pub fn with_size(size: Size<T>) -> Self { Self { - x: top_left.x, - y: top_left.y, + x: T::default(), + y: T::default(), width: size.width, height: size.height, } } +} - /// Creates a new [`Rectangle`] with its top-left corner at the origin - /// and with the provided [`Size`]. - pub fn with_size(size: Size) -> Self { +impl Rectangle<f32> { + /// A rectangle starting at [`Point::ORIGIN`] with infinite width and height. + pub const INFINITE: Self = Self::new(Point::ORIGIN, Size::INFINITY); + + /// Creates a new [`Rectangle`] with its top-left corner in the given + /// [`Point`] and with the provided [`Size`]. + pub const fn new(top_left: Point, size: Size) -> Self { Self { - x: 0.0, - y: 0.0, + x: top_left.x, + y: top_left.y, width: size.width, height: size.height, } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 6712314e..f5ef8f68 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -9,7 +9,7 @@ use crate::{ /// A component that can be used by widgets to draw themselves on a screen. pub trait Renderer { /// Starts recording a new layer. - fn start_layer(&mut self); + fn start_layer(&mut self, bounds: Rectangle); /// Ends recording a new layer. /// @@ -20,13 +20,13 @@ pub trait Renderer { /// /// The layer will clip its contents to the provided `bounds`. fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - self.start_layer(); + self.start_layer(bounds); f(self); self.end_layer(bounds); } /// Starts recording with a new [`Transformation`]. - fn start_transformation(&mut self); + fn start_transformation(&mut self, transformation: Transformation); /// Ends recording a new layer. /// @@ -39,7 +39,7 @@ pub trait Renderer { transformation: Transformation, f: impl FnOnce(&mut Self), ) { - self.start_transformation(); + self.start_transformation(transformation); f(self); self.end_transformation(transformation); } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 1caf71b3..f36d19aa 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -10,11 +10,11 @@ use crate::{ use std::borrow::Cow; impl Renderer for () { - fn start_layer(&mut self) {} + fn start_layer(&mut self, _bounds: Rectangle) {} fn end_layer(&mut self, _bounds: Rectangle) {} - fn start_transformation(&mut self) {} + fn start_transformation(&mut self, _transformation: Transformation) {} fn end_transformation(&mut self, _transformation: Transformation) {} diff --git a/core/src/size.rs b/core/src/size.rs index 55db759d..c2b5671a 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -99,3 +99,17 @@ where } } } + +impl<T> std::ops::Mul<T> for Size<T> +where + T: std::ops::Mul<Output = T> + Copy, +{ + type Output = Size<T>; + + fn mul(self, rhs: T) -> Self::Output { + Size { + width: self.width * rhs, + height: self.height * rhs, + } + } +} diff --git a/core/src/transformation.rs b/core/src/transformation.rs index b2c488b0..74183147 100644 --- a/core/src/transformation.rs +++ b/core/src/transformation.rs @@ -42,6 +42,12 @@ impl Transformation { } } +impl Default for Transformation { + fn default() -> Self { + Transformation::IDENTITY + } +} + impl Mul for Transformation { type Output = Self; diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 16cdb86f..31b8a4ce 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -6,7 +6,10 @@ mod rainbow { use iced::advanced::renderer; use iced::advanced::widget::{self, Widget}; use iced::mouse; - use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector}; + use iced::{ + Element, Length, Rectangle, Renderer, Size, Theme, Transformation, + Vector, + }; #[derive(Debug, Clone, Copy, Default)] pub struct Rainbow; @@ -79,7 +82,6 @@ mod rainbow { let posn_l = [0.0, bounds.height / 2.0]; let mesh = Mesh::Solid { - size: bounds.size(), buffers: mesh::Indexed { vertices: vec![ SolidVertex2D { @@ -130,6 +132,8 @@ mod rainbow { 0, 8, 1, // L ], }, + transformation: Transformation::IDENTITY, + clip_bounds: Rectangle::INFINITE, }; renderer.with_translation( diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 9cd801b2..c292709f 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -5,12 +5,12 @@ use controls::Controls; use scene::Scene; use iced_wgpu::graphics::Viewport; -use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_wgpu::{wgpu, Engine, Renderer, Settings}; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; use iced_winit::core::window; -use iced_winit::core::{Color, Font, Pixels, Size, Theme}; +use iced_winit::core::{Color, Size, Theme}; use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; @@ -155,11 +155,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { // Initialize iced let mut debug = Debug::new(); - let mut renderer = Renderer::new( - Backend::new(&adapter, &device, &queue, Settings::default(), format), - Font::default(), - Pixels(16.0), - ); + let mut engine = Engine::new(&adapter, &device, &queue, format, None); + let mut renderer = Renderer::new(Settings::default(), &engine); let mut state = program::State::new( controls, @@ -228,19 +225,17 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { } // And then iced on top - renderer.with_primitives(|backend, primitive| { - backend.present( - &device, - &queue, - &mut encoder, - None, - frame.texture.format(), - &view, - primitive, - &viewport, - &debug.overlay(), - ); - }); + renderer.present( + &mut engine, + &device, + &queue, + &mut encoder, + None, + frame.texture.format(), + &view, + &viewport, + &debug.overlay(), + ); // Then we submit the work queue.submit(Some(encoder.finish())); diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs index b52f9d9d..1ba82f9f 100644 --- a/graphics/src/cached.rs +++ b/graphics/src/cached.rs @@ -5,7 +5,7 @@ use std::sync::Arc; /// A piece of data that can be cached. pub trait Cached: Sized { /// The type of cache produced. - type Cache; + type Cache: Clone; /// Loads the [`Cache`] into a proper instance. /// @@ -15,7 +15,7 @@ pub trait Cached: Sized { /// Caches this value, producing its corresponding [`Cache`]. /// /// [`Cache`]: Self::Cache - fn cache(self) -> Self::Cache; + fn cache(self, previous: Option<Self::Cache>) -> Self::Cache; } impl<T> Cached for Primitive<T> { @@ -27,7 +27,7 @@ impl<T> Cached for Primitive<T> { } } - fn cache(self) -> Arc<Self> { + fn cache(self, _previous: Option<Arc<Self>>) -> Arc<Self> { Arc::new(self) } } @@ -38,5 +38,5 @@ impl Cached for () { fn load(_cache: &Self::Cache) -> Self {} - fn cache(self) -> Self::Cache {} + fn cache(self, _previous: Option<Self::Cache>) -> Self::Cache {} } diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs index 37d433c2..665e996b 100644 --- a/graphics/src/geometry/cache.rs +++ b/graphics/src/geometry/cache.rs @@ -22,13 +22,20 @@ where /// Creates a new empty [`Cache`]. pub fn new() -> Self { Cache { - state: RefCell::new(State::Empty), + state: RefCell::new(State::Empty { previous: None }), } } /// Clears the [`Cache`], forcing a redraw the next time it is used. pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; + use std::ops::Deref; + + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { geometry, .. } => Some(geometry.clone()), + }; + + *self.state.borrow_mut() = State::Empty { previous }; } /// Draws geometry using the provided closure and stores it in the @@ -49,20 +56,24 @@ where ) -> Renderer::Geometry { use std::ops::Deref; - if let State::Filled { - bounds: cached_bounds, - geometry, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Cached::load(geometry); + let previous = match self.state.borrow().deref() { + State::Empty { previous } => previous.clone(), + State::Filled { + bounds: cached_bounds, + geometry, + } => { + if *cached_bounds == bounds { + return Cached::load(geometry); + } + + Some(geometry.clone()) } - } + }; let mut frame = Frame::new(renderer, bounds); draw_fn(&mut frame); - let geometry = frame.into_geometry().cache(); + let geometry = frame.into_geometry().cache(previous); let result = Cached::load(&geometry); *self.state.borrow_mut() = State::Filled { bounds, geometry }; @@ -79,7 +90,7 @@ where let state = self.state.borrow(); match *state { - State::Empty => write!(f, "Cache::Empty"), + State::Empty { .. } => write!(f, "Cache::Empty"), State::Filled { bounds, .. } => { write!(f, "Cache::Filled {{ bounds: {bounds:?} }}") } @@ -100,7 +111,9 @@ enum State<Geometry> where Geometry: Cached, { - Empty, + Empty { + previous: Option<Geometry::Cache>, + }, Filled { bounds: Size, geometry: Geometry::Cache, diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index ad35e8ec..377589d7 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -113,13 +113,11 @@ where region: Rectangle, f: impl FnOnce(&mut Self) -> R, ) -> R { - let mut frame = self.draft(region.size()); + let mut frame = self.draft(region); let result = f(&mut frame); - let origin = Point::new(region.x, region.y); - - self.paste(frame, origin); + self.paste(frame, Point::new(region.x, region.y)); result } @@ -129,14 +127,14 @@ where /// Draw its contents back to this [`Frame`] with [`paste`]. /// /// [`paste`]: Self::paste - pub fn draft(&mut self, size: Size) -> Self { + fn draft(&mut self, clip_bounds: Rectangle) -> Self { Self { - raw: self.raw.draft(size), + raw: self.raw.draft(clip_bounds), } } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - pub fn paste(&mut self, frame: Self, at: Point) { + fn paste(&mut self, frame: Self, at: Point) { self.raw.paste(frame.raw, at); } @@ -187,7 +185,7 @@ pub trait Backend: Sized { fn scale(&mut self, scale: impl Into<f32>); fn scale_nonuniform(&mut self, scale: impl Into<Vector>); - fn draft(&mut self, size: Size) -> Self; + fn draft(&mut self, clip_bounds: Rectangle) -> Self; fn paste(&mut self, frame: Self, at: Point); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>); @@ -232,7 +230,7 @@ impl Backend for () { fn scale(&mut self, _scale: impl Into<f32>) {} fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {} - fn draft(&mut self, _size: Size) -> Self {} + fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} fn paste(&mut self, _frame: Self, _at: Point) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index d89caace..47a7b30e 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -1,14 +1,96 @@ //! Load and operate on images. -use crate::core::image::{Data, Handle}; +#[cfg(feature = "image")] +pub use ::image as image_rs; -use bitflags::bitflags; +use crate::core::image; +use crate::core::svg; +use crate::core::{Color, Rectangle}; -pub use ::image as image_rs; +/// 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 filter method of a raster image. + filter_method: image::FilterMethod, + + /// The bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option<Color>, + /// The bounds of the image. + bounds: Rectangle, + }, +} + +#[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. -pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> { +/// +/// [`Handle`]: image::Handle +pub fn load( + handle: &image::Handle, +) -> ::image::ImageResult<::image::DynamicImage> { + use bitflags::bitflags; + + bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b001; + const ROTATE_180 = 0b010; + const FLIP_DIAGONALLY = 0b100; + } + } + + impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error> + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .and_then(|value| Self::from_bits(value.saturating_sub(1))) + .unwrap_or_else(Self::empty)) + } + + fn perform( + self, + mut image: ::image::DynamicImage, + ) -> ::image::DynamicImage { + use ::image::imageops; + + if self.contains(Self::FLIP_DIAGONALLY) { + imageops::flip_vertical_in_place(&mut image); + } + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } + } + match handle.data() { - Data::Path(path) => { + image::Data::Path(path) => { let image = ::image::open(path)?; let operation = std::fs::File::open(path) @@ -19,7 +101,7 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> { Ok(operation.perform(image)) } - Data::Bytes(bytes) => { + image::Data::Bytes(bytes) => { let image = ::image::load_from_memory(bytes)?; let operation = Operation::from_exif(&mut std::io::Cursor::new(bytes)) @@ -28,68 +110,22 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> { Ok(operation.perform(image)) } - Data::Rgba { + image::Data::Rgba { width, height, pixels, } => { - if let Some(image) = image_rs::ImageBuffer::from_vec( - *width, - *height, - pixels.to_vec(), - ) { - Ok(image_rs::DynamicImage::ImageRgba8(image)) + if let Some(image) = + ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec()) + { + Ok(::image::DynamicImage::ImageRgba8(image)) } else { - Err(image_rs::error::ImageError::Limits( - image_rs::error::LimitError::from_kind( - image_rs::error::LimitErrorKind::DimensionError, + Err(::image::error::ImageError::Limits( + ::image::error::LimitError::from_kind( + ::image::error::LimitErrorKind::DimensionError, ), )) } } } } - -bitflags! { - struct Operation: u8 { - const FLIP_HORIZONTALLY = 0b001; - const ROTATE_180 = 0b010; - const FLIP_DIAGONALLY = 0b100; - } -} - -impl Operation { - // Meaning of the returned value is described e.g. at: - // https://magnushoff.com/articles/jpeg-orientation/ - fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error> - where - R: std::io::BufRead + std::io::Seek, - { - let exif = exif::Reader::new().read_from_container(reader)?; - - Ok(exif - .get_field(exif::Tag::Orientation, exif::In::PRIMARY) - .and_then(|field| field.value.get_uint(0)) - .and_then(|value| u8::try_from(value).ok()) - .and_then(|value| Self::from_bits(value.saturating_sub(1))) - .unwrap_or_else(Self::empty)) - } - - fn perform(self, mut image: image::DynamicImage) -> image::DynamicImage { - use image::imageops; - - if self.contains(Self::FLIP_DIAGONALLY) { - imageops::flip_vertical_in_place(&mut image); - } - - if self.contains(Self::ROTATE_180) { - imageops::rotate180_in_place(&mut image); - } - - if self.contains(Self::FLIP_HORIZONTALLY) { - imageops::flip_horizontal_in_place(&mut image); - } - - image - } -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 18bfd981..b79ef70d 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -20,6 +20,7 @@ pub mod compositor; pub mod damage; pub mod error; pub mod gradient; +pub mod image; pub mod mesh; pub mod renderer; pub mod text; @@ -27,9 +28,6 @@ pub mod text; #[cfg(feature = "geometry")] pub mod geometry; -#[cfg(feature = "image")] -pub mod image; - pub use antialiasing::Antialiasing; pub use backend::Backend; pub use cached::Cached; @@ -37,10 +35,12 @@ pub use compositor::Compositor; pub use damage::Damage; pub use error::Error; pub use gradient::Gradient; +pub use image::Image; pub use mesh::Mesh; pub use primitive::Primitive; pub use renderer::Renderer; pub use settings::Settings; +pub use text::Text; pub use viewport::Viewport; pub use iced_core as core; diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs index 20692b07..76602319 100644 --- a/graphics/src/mesh.rs +++ b/graphics/src/mesh.rs @@ -1,8 +1,7 @@ //! Draw triangles! use crate::color; -use crate::core::{Rectangle, Size}; +use crate::core::{Rectangle, Transformation}; use crate::gradient; -use crate::Damage; use bytemuck::{Pod, Zeroable}; @@ -14,29 +13,55 @@ pub enum Mesh { /// The vertices and indices of the mesh. buffers: Indexed<SolidVertex2D>, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. - size: Size, + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, /// A mesh with a gradient. Gradient { /// The vertices and indices of the mesh. buffers: Indexed<GradientVertex2D>, - /// The size of the drawable region of the mesh. - /// - /// Any geometry that falls out of this region will be clipped. - size: Size, + /// The [`Transformation`] for the vertices of the [`Mesh`]. + transformation: Transformation, + + /// The clip bounds of the [`Mesh`]. + clip_bounds: Rectangle, }, } -impl Damage for Mesh { - fn bounds(&self) -> Rectangle { +impl Mesh { + /// 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 [`Transformation`] of the [`Mesh`]. + pub fn transformation(&self) -> Transformation { + match self { + Self::Solid { transformation, .. } + | Self::Gradient { transformation, .. } => *transformation, + } + } + + /// Returns the clip bounds of the [`Mesh`]. + pub fn clip_bounds(&self) -> Rectangle { match self { - Self::Solid { size, .. } | Self::Gradient { size, .. } => { - Rectangle::with_size(*size) + Self::Solid { + clip_bounds, + transformation, + .. } + | Self::Gradient { + clip_bounds, + transformation, + .. + } => *clip_bounds * *transformation, } } } @@ -75,6 +100,47 @@ pub struct GradientVertex2D { pub gradient: gradient::Packed, } +/// 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 solid meshes. + pub solids: usize, + + /// The total amount of gradient vertices. + pub gradient_vertices: usize, + + /// The total amount of gradient meshes. + pub gradients: 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(meshes: &[Mesh]) -> AttributeCount { + meshes + .iter() + .fold(AttributeCount::default(), |mut count, mesh| { + match mesh { + Mesh::Solid { buffers, .. } => { + count.solids += 1; + count.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradients += 1; + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} + /// A renderer capable of drawing a [`Mesh`]. pub trait Renderer { /// Draws the given [`Mesh`]. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index fb1a0d73..d4f91dab 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -62,7 +62,7 @@ impl<B: Backend> Renderer<B> { } impl<B: Backend> iced_core::Renderer for Renderer<B> { - fn start_layer(&mut self) { + fn start_layer(&mut self, _bounds: Rectangle) { self.stack.push(std::mem::take(&mut self.primitives)); } @@ -75,7 +75,7 @@ impl<B: Backend> iced_core::Renderer for Renderer<B> { self.primitives.push(Primitive::group(layer).clip(bounds)); } - fn start_transformation(&mut self) { + fn start_transformation(&mut self, _transformation: Transformation) { self.stack.push(std::mem::take(&mut self.primitives)); } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 0310ead7..f9fc1fec 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -9,14 +9,67 @@ pub use paragraph::Paragraph; pub use cosmic_text; +use crate::core::alignment; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::{Color, Point, Rectangle, Size}; +use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock, Weak}; +/// A text primitive. +#[derive(Debug, Clone)] +pub enum Text { + /// A paragraph. + #[allow(missing_docs)] + Paragraph { + paragraph: paragraph::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// An editor. + #[allow(missing_docs)] + Editor { + editor: editor::Weak, + position: Point, + color: Color, + clip_bounds: Rectangle, + transformation: Transformation, + }, + /// Some cached text. + Cached { + /// The contents of the text. + content: String, + /// The bounds of the text. + bounds: Rectangle, + /// The color of the text. + color: Color, + /// The size of the text in logical pixels. + size: Pixels, + /// The line height of the text. + line_height: Pixels, + /// The font of the text. + font: Font, + /// The horizontal alignment of the text. + horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the text. + vertical_alignment: alignment::Vertical, + /// The shaping strategy of the text. + shaping: Shaping, + /// The clip bounds of the text. + clip_bounds: Rectangle, + }, + /// Some raw text. + #[allow(missing_docs)] + Raw { + raw: Raw, + transformation: Transformation, + }, +} + /// The regular variant of the [Fira Sans] font. /// /// It is loaded as part of the default fonts in Wasm builds. diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 6fdb4228..458681dd 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -19,7 +19,6 @@ tiny-skia = ["iced_tiny_skia"] image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] -tracing = ["iced_wgpu?/tracing"] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 85ac3d93..2676ba87 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -47,16 +47,20 @@ where delegate!(self, renderer, renderer.clear()); } - fn start_layer(&mut self) { - delegate!(self, renderer, renderer.start_layer()); + fn start_layer(&mut self, bounds: Rectangle) { + delegate!(self, renderer, renderer.start_layer(bounds)); } fn end_layer(&mut self, bounds: Rectangle) { delegate!(self, renderer, renderer.end_layer(bounds)); } - fn start_transformation(&mut self) { - delegate!(self, renderer, renderer.start_transformation()); + fn start_transformation(&mut self, transformation: Transformation) { + delegate!( + self, + renderer, + renderer.start_transformation(transformation) + ); } fn end_transformation(&mut self, transformation: Transformation) { @@ -421,7 +425,7 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::{Point, Radians, Size, Vector}; + use crate::core::{Point, Radians, Rectangle, Size, Vector}; use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; use crate::graphics::Cached; @@ -457,7 +461,7 @@ mod geometry { } } - #[derive(Debug)] + #[derive(Debug, Clone)] pub enum Geometry<A, B> { Primary(A), Secondary(B), @@ -477,12 +481,23 @@ mod geometry { } } - fn cache(self) -> Self::Cache { - match self { - Self::Primary(geometry) => Geometry::Primary(geometry.cache()), - Self::Secondary(geometry) => { - Geometry::Secondary(geometry.cache()) + fn cache(self, previous: Option<Self::Cache>) -> Self::Cache { + match (self, previous) { + ( + Self::Primary(geometry), + Some(Geometry::Primary(previous)), + ) => Geometry::Primary(geometry.cache(Some(previous))), + (Self::Primary(geometry), None) => { + Geometry::Primary(geometry.cache(None)) } + ( + Self::Secondary(geometry), + Some(Geometry::Secondary(previous)), + ) => Geometry::Secondary(geometry.cache(Some(previous))), + (Self::Secondary(geometry), None) => { + Geometry::Secondary(geometry.cache(None)) + } + _ => unreachable!(), } } } @@ -545,10 +560,10 @@ mod geometry { delegate!(self, frame, frame.pop_transform()); } - fn draft(&mut self, size: Size) -> Self { + fn draft(&mut self, bounds: Rectangle) -> Self { match self { - Self::Primary(frame) => Self::Primary(frame.draft(size)), - Self::Secondary(frame) => Self::Secondary(frame.draft(size)), + Self::Primary(frame) => Self::Primary(frame.draft(bounds)), + Self::Secondary(frame) => Self::Secondary(frame.draft(bounds)), } } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 9bc3664d..a9dcb610 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -196,8 +196,8 @@ impl geometry::frame::Backend for Frame { self.transform = self.stack.pop().expect("Pop transform"); } - fn draft(&mut self, size: Size) -> Self { - Self::new(size) + fn draft(&mut self, clip_bounds: Rectangle) -> Self { + Self::new(clip_bounds.size()) } fn paste(&mut self, frame: Self, at: Point) { diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 5f464522..30545fa2 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -44,7 +44,3 @@ lyon.optional = true resvg.workspace = true resvg.optional = true - -tracing.workspace = true -tracing.optional = true - diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs deleted file mode 100644 index 6ccf4111..00000000 --- a/wgpu/src/backend.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::buffer; -use crate::core::{Color, Size, Transformation}; -use crate::graphics::backend; -use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::pipeline; -use crate::primitive::{self, Primitive}; -use crate::quad; -use crate::text; -use crate::triangle; -use crate::window; -use crate::{Layer, Settings}; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image; - -use std::borrow::Cow; - -/// A [`wgpu`] graphics backend for [`iced`]. -/// -/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -/// [`iced`]: https://github.com/iced-rs/iced -#[allow(missing_debug_implementations)] -pub struct Backend { - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - pipeline_storage: pipeline::Storage, - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, - staging_belt: wgpu::util::StagingBelt, -} - -impl Backend { - /// Creates a new [`Backend`]. - pub fn new( - _adapter: &wgpu::Adapter, - device: &wgpu::Device, - queue: &wgpu::Queue, - settings: Settings, - format: wgpu::TextureFormat, - ) -> Self { - let text_pipeline = text::Pipeline::new(device, queue, format); - let quad_pipeline = quad::Pipeline::new(device, format); - let triangle_pipeline = - triangle::Pipeline::new(device, format, settings.antialiasing); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = { - let backend = _adapter.get_info().backend; - - image::Pipeline::new(device, format, backend) - }; - - Self { - quad_pipeline, - text_pipeline, - triangle_pipeline, - pipeline_storage: pipeline::Storage::default(), - - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - - // TODO: Resize belt smartly (?) - // It would be great if the `StagingBelt` API exposed methods - // for introspection to detect when a resize may be worth it. - staging_belt: wgpu::util::StagingBelt::new( - buffer::MAX_WRITE_SIZE as u64, - ), - } - } - - /// Draws the provided primitives in the given `TextureView`. - /// - /// The text provided as overlay will be rendered on top of the primitives. - /// This is useful for rendering debug information. - pub fn present<T: AsRef<str>>( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - encoder: &mut wgpu::CommandEncoder, - clear_color: Option<Color>, - format: wgpu::TextureFormat, - frame: &wgpu::TextureView, - primitives: &[Primitive], - viewport: &Viewport, - overlay_text: &[T], - ) { - log::debug!("Drawing"); - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Backend", "PRESENT").entered(); - - let target_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let transformation = viewport.projection(); - - let mut layers = Layer::generate(primitives, viewport); - - if !overlay_text.is_empty() { - layers.push(Layer::overlay(overlay_text, viewport)); - } - - self.prepare( - device, - queue, - format, - encoder, - scale_factor, - target_size, - transformation, - &layers, - ); - - self.staging_belt.finish(); - - self.render( - device, - encoder, - frame, - clear_color, - scale_factor, - target_size, - &layers, - ); - - self.quad_pipeline.end_frame(); - self.text_pipeline.end_frame(); - self.triangle_pipeline.end_frame(); - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.end_frame(); - } - - /// Recalls staging memory for future uploads. - /// - /// This method should be called after the command encoder - /// has been submitted. - pub fn recall(&mut self) { - self.staging_belt.recall(); - } - - fn prepare( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - encoder: &mut wgpu::CommandEncoder, - scale_factor: f32, - target_size: Size<u32>, - transformation: Transformation, - layers: &[Layer<'_>], - ) { - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.quads, - transformation, - scale_factor, - ); - } - - if !layer.meshes.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.triangle_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.meshes, - scaled, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = - transformation * Transformation::scale(scale_factor); - - self.image_pipeline.prepare( - device, - encoder, - &mut self.staging_belt, - &layer.images, - scaled, - scale_factor, - ); - } - } - - if !layer.text.is_empty() { - self.text_pipeline.prepare( - device, - queue, - encoder, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - } - - if !layer.pipelines.is_empty() { - for pipeline in &layer.pipelines { - pipeline.primitive.prepare( - format, - device, - queue, - pipeline.bounds, - target_size, - scale_factor, - &mut self.pipeline_storage, - ); - } - } - } - } - - fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - clear_color: Option<Color>, - scale_factor: f32, - target_size: Size<u32>, - layers: &[Layer<'_>], - ) { - use std::mem::ManuallyDrop; - - let mut quad_layer = 0; - let mut triangle_layer = 0; - #[cfg(any(feature = "image", feature = "svg"))] - let mut image_layer = 0; - let mut text_layer = 0; - - let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: match clear_color { - Some(background_color) => wgpu::LoadOp::Clear({ - let [r, g, b, a] = - color::pack(background_color).components(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - None => wgpu::LoadOp::Load, - }, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - - for layer in layers { - let bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - continue; - } - - if !layer.quads.is_empty() { - self.quad_pipeline.render( - quad_layer, - bounds, - &layer.quads, - &mut render_pass, - ); - - quad_layer += 1; - } - - if !layer.meshes.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - self.triangle_pipeline.render( - device, - encoder, - target, - triangle_layer, - target_size, - &layer.meshes, - scale_factor, - ); - - triangle_layer += 1; - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - - #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - self.image_pipeline.render( - image_layer, - bounds, - &mut render_pass, - ); - - image_layer += 1; - } - } - - if !layer.text.is_empty() { - self.text_pipeline - .render(text_layer, bounds, &mut render_pass); - - text_layer += 1; - } - - if !layer.pipelines.is_empty() { - let _ = ManuallyDrop::into_inner(render_pass); - - for pipeline in &layer.pipelines { - let viewport = (pipeline.viewport * scale_factor).snap(); - - if viewport.width < 1 || viewport.height < 1 { - continue; - } - - pipeline.primitive.render( - &self.pipeline_storage, - target, - target_size, - viewport, - encoder, - ); - } - - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); - } - } - - let _ = ManuallyDrop::into_inner(render_pass); - } -} - -impl backend::Backend for Backend { - type Primitive = primitive::Custom; - type Compositor = window::Compositor; -} - -impl backend::Text for Backend { - fn load_font(&mut self, font: Cow<'static, [u8]>) { - self.text_pipeline.load_font(font); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { - self.image_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size<u32> { - self.image_pipeline.viewport_dimensions(handle) - } -} - -#[cfg(feature = "geometry")] -impl crate::graphics::geometry::Backend for Backend { - type Frame = crate::geometry::Frame; - - fn new_frame(&self, size: Size) -> Self::Frame { - crate::geometry::Frame::new(size) - } -} diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs new file mode 100644 index 00000000..e45b62b2 --- /dev/null +++ b/wgpu/src/engine.rs @@ -0,0 +1,79 @@ +use crate::buffer; +use crate::graphics::Antialiasing; +use crate::primitive::pipeline; +use crate::quad; +use crate::text; +use crate::triangle; + +#[allow(missing_debug_implementations)] +pub struct Engine { + pub(crate) quad_pipeline: quad::Pipeline, + pub(crate) text_pipeline: text::Pipeline, + pub(crate) triangle_pipeline: triangle::Pipeline, + pub(crate) _pipeline_storage: pipeline::Storage, + #[cfg(any(feature = "image", feature = "svg"))] + pub(crate) image_pipeline: crate::image::Pipeline, + pub(crate) staging_belt: wgpu::util::StagingBelt, +} + +impl Engine { + pub fn new( + _adapter: &wgpu::Adapter, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily + ) -> Self { + let text_pipeline = text::Pipeline::new(device, queue, format); + let quad_pipeline = quad::Pipeline::new(device, format); + let triangle_pipeline = + triangle::Pipeline::new(device, format, antialiasing); + + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = { + let backend = _adapter.get_info().backend; + + crate::image::Pipeline::new(device, format, backend) + }; + + Self { + // TODO: Resize belt smartly (?) + // It would be great if the `StagingBelt` API exposed methods + // for introspection to detect when a resize may be worth it. + staging_belt: wgpu::util::StagingBelt::new( + buffer::MAX_WRITE_SIZE as u64, + ), + quad_pipeline, + text_pipeline, + triangle_pipeline, + _pipeline_storage: pipeline::Storage::default(), + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, + } + } + + #[cfg(any(feature = "image", feature = "svg"))] + pub fn image_cache(&self) -> &crate::image::cache::Shared { + self.image_pipeline.cache() + } + + pub fn submit( + &mut self, + queue: &wgpu::Queue, + encoder: wgpu::CommandEncoder, + ) -> wgpu::SubmissionIndex { + self.staging_belt.finish(); + let index = queue.submit(Some(encoder.finish())); + self.staging_belt.recall(); + + self.quad_pipeline.end_frame(); + self.text_pipeline.end_frame(); + self.triangle_pipeline.end_frame(); + + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.end_frame(); + + index + } +} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index ba56c59d..985650e2 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -6,11 +6,13 @@ use crate::core::{ use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ - self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, + self, LineCap, LineDash, LineJoin, Path, Stroke, Style, }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::primitive::{self, Primitive}; +use crate::graphics::{self, Cached, Text}; +use crate::text; +use crate::triangle; use lyon::geom::euclid; use lyon::tessellation; @@ -20,92 +22,109 @@ use std::borrow::Cow; /// A frame for drawing some geometry. #[allow(missing_debug_implementations)] pub struct Frame { - size: Size, + clip_bounds: Rectangle, buffers: BufferStack, - primitives: Vec<Primitive>, + meshes: Vec<Mesh>, + text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, stroke_tessellator: tessellation::StrokeTessellator, } +#[derive(Debug)] +pub enum Geometry { + Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Cached(Cache), +} + +#[derive(Debug, Clone)] +pub struct Cache { + pub meshes: Option<triangle::Cache>, + pub text: Option<text::Cache>, +} + +impl Cached for Geometry { + type Cache = Cache; + + fn load(cache: &Self::Cache) -> Self { + Geometry::Cached(cache.clone()) + } + + fn cache(self, previous: Option<Self::Cache>) -> Self::Cache { + match self { + Self::Live { meshes, text } => { + if let Some(mut previous) = previous { + if let Some(cache) = &mut previous.meshes { + cache.update(meshes); + } else { + previous.meshes = triangle::Cache::new(meshes); + } + + if let Some(cache) = &mut previous.text { + cache.update(text); + } else { + previous.text = text::Cache::new(text); + } + + previous + } else { + Cache { + meshes: triangle::Cache::new(meshes), + text: text::Cache::new(text), + } + } + } + Self::Cached(cache) => cache, + } + } +} + impl Frame { /// Creates a new [`Frame`] with the given [`Size`]. pub fn new(size: Size) -> Frame { + Self::with_clip(Rectangle::with_size(size)) + } + + /// Creates a new [`Frame`] with the given clip bounds. + pub fn with_clip(bounds: Rectangle) -> Frame { Frame { - size, + clip_bounds: bounds, buffers: BufferStack::new(), - primitives: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform(lyon::math::Transform::identity()), + current: Transform(lyon::math::Transform::translation( + bounds.x, bounds.y, + )), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), } } - - fn into_primitives(mut self) -> Vec<Primitive> { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Solid { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - Buffer::Gradient(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::Custom( - primitive::Custom::Mesh(Mesh::Gradient { - buffers: mesh::Indexed { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }), - )); - } - } - } - } - - self.primitives - } } impl geometry::frame::Backend for Frame { - type Geometry = Primitive; - - /// 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. + type Geometry = Geometry; #[inline] fn width(&self) -> f32 { - self.size.width + self.clip_bounds.width } #[inline] fn height(&self) -> f32 { - self.size.height + self.clip_bounds.height } #[inline] fn size(&self) -> Size { - self.size + self.clip_bounds.size() } #[inline] fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) + Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0) } fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { @@ -208,7 +227,7 @@ impl geometry::frame::Backend for Frame { .expect("Stroke path"); } - fn fill_text(&mut self, text: impl Into<Text>) { + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); let (scale_x, scale_y) = self.transforms.current.scale(); @@ -246,18 +265,17 @@ impl geometry::frame::Backend for Frame { height: f32::INFINITY, }; - // TODO: Honor layering! - self.primitives.push(Primitive::Text { + self.text.push(graphics::Text::Cached { content: text.content, bounds, color: text.color, size, - line_height, + line_height: line_height.to_absolute(size), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), + clip_bounds: self.clip_bounds, }); } else { text.draw_with(|path, color| self.fill(&path, color)); @@ -308,41 +326,24 @@ impl geometry::frame::Backend for Frame { self.transforms.current = self.transforms.previous.pop().unwrap(); } - fn draft(&mut self, size: Size) -> Frame { - Frame::new(size) + fn draft(&mut self, clip_bounds: Rectangle) -> Frame { + Frame::with_clip(clip_bounds) } - fn paste(&mut self, frame: Frame, at: Point) { - let size = frame.size(); - let primitives = frame.into_primitives(); - let transformation = Transformation::translate(at.x, at.y); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + fn paste(&mut self, frame: Frame, _at: Point) { + self.meshes + .extend(frame.buffers.into_meshes(frame.clip_bounds)); - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Transform { - transformation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Transform { - transformation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); + self.text.extend(frame.text); } - fn into_geometry(self) -> Self::Geometry { - Primitive::Group { - primitives: self.into_primitives(), + fn into_geometry(mut self) -> Self::Geometry { + self.meshes + .extend(self.buffers.into_meshes(self.clip_bounds)); + + Geometry::Live { + meshes: self.meshes, + text: self.text, } } } @@ -429,6 +430,34 @@ impl BufferStack { _ => unreachable!(), } } + + fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> { + self.stack + .into_iter() + .filter_map(move |buffer| match buffer { + Buffer::Solid(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Solid { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + Buffer::Gradient(buffer) if !buffer.indices.is_empty() => { + Some(Mesh::Gradient { + buffers: mesh::Indexed { + vertices: buffer.vertices, + indices: buffer.indices, + }, + clip_bounds, + transformation: Transformation::IDENTITY, + }) + } + _ => None, + }) + } } #[derive(Debug)] @@ -591,7 +620,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { let mut draw_line = false; walk_along_path( - path.raw().iter().flattened(0.01), + path.raw().iter().flattened( + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + ), 0.0, lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, &mut RepeatedPattern { diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs new file mode 100644 index 00000000..32118ed6 --- /dev/null +++ b/wgpu/src/image/cache.rs @@ -0,0 +1,107 @@ +use crate::core::{self, Size}; +use crate::image::atlas::{self, Atlas}; + +use std::cell::{RefCell, RefMut}; +use std::rc::Rc; + +#[derive(Debug)] +pub struct Cache { + atlas: Atlas, + #[cfg(feature = "image")] + raster: crate::image::raster::Cache, + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache, +} + +impl Cache { + pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self { + Self { + atlas: Atlas::new(device, backend), + #[cfg(feature = "image")] + raster: crate::image::raster::Cache::default(), + #[cfg(feature = "svg")] + vector: crate::image::vector::Cache::default(), + } + } + + pub fn layer_count(&self) -> usize { + self.atlas.layer_count() + } + + #[cfg(feature = "image")] + pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> { + self.raster.load(handle).dimensions() + } + + #[cfg(feature = "svg")] + pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> { + self.vector.load(handle).viewport_dimensions() + } + + #[cfg(feature = "image")] + pub fn upload_raster( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::image::Handle, + ) -> Option<&atlas::Entry> { + self.raster.upload(device, encoder, handle, &mut self.atlas) + } + + #[cfg(feature = "svg")] + pub fn upload_vector( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + handle: &core::svg::Handle, + color: Option<core::Color>, + size: [f32; 2], + scale: f32, + ) -> Option<&atlas::Entry> { + self.vector.upload( + device, + encoder, + handle, + color, + size, + scale, + &mut self.atlas, + ) + } + + pub fn create_bind_group( + &self, + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image texture atlas bind group"), + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(self.atlas.view()), + }], + }) + } + + pub fn trim(&mut self) { + #[cfg(feature = "image")] + self.raster.trim(&mut self.atlas); + + #[cfg(feature = "svg")] + self.vector.trim(&mut self.atlas); + } +} + +#[derive(Debug, Clone)] +pub struct Shared(Rc<RefCell<Cache>>); + +impl Shared { + pub fn new(cache: Cache) -> Self { + Self(Rc::new(RefCell::new(cache))) + } + + pub fn lock(&self) -> RefMut<'_, Cache> { + self.0.borrow_mut() + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image/mod.rs index d0bf1182..86731cbf 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod cache; +pub(crate) use cache::Cache; + mod atlas; #[cfg(feature = "image")] @@ -6,194 +9,30 @@ mod raster; #[cfg(feature = "svg")] mod vector; -use atlas::Atlas; - use crate::core::{Rectangle, Size, Transformation}; -use crate::layer; use crate::Buffer; -use std::cell::RefCell; -use std::mem; - use bytemuck::{Pod, Zeroable}; +use std::mem; -#[cfg(feature = "image")] -use crate::core::image; - -#[cfg(feature = "svg")] -use crate::core::svg; +pub use crate::graphics::Image; -#[cfg(feature = "tracing")] -use tracing::info_span; +pub type Batch = Vec<Image>; #[derive(Debug)] pub struct Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell<raster::Cache>, - #[cfg(feature = "svg")] - vector_cache: RefCell<vector::Cache>, - pipeline: wgpu::RenderPipeline, nearest_sampler: wgpu::Sampler, linear_sampler: wgpu::Sampler, texture: wgpu::BindGroup, texture_version: usize, - texture_atlas: Atlas, texture_layout: wgpu::BindGroupLayout, constant_layout: wgpu::BindGroupLayout, - + cache: cache::Shared, layers: Vec<Layer>, prepare_layer: usize, } -#[derive(Debug)] -struct Layer { - uniforms: wgpu::Buffer, - nearest: Data, - linear: Data, -} - -impl Layer { - fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - nearest_sampler: &wgpu::Sampler, - linear_sampler: &wgpu::Sampler, - ) -> Self { - let uniforms = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image uniforms buffer"), - size: mem::size_of::<Uniforms>() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let nearest = - Data::new(device, constant_layout, nearest_sampler, &uniforms); - - let linear = - Data::new(device, constant_layout, linear_sampler, &uniforms); - - Self { - uniforms, - nearest, - linear, - } - } - - fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - nearest_instances: &[Instance], - linear_instances: &[Instance], - transformation: Transformation, - ) { - let uniforms = Uniforms { - transform: transformation.into(), - }; - - let bytes = bytemuck::bytes_of(&uniforms); - - belt.write_buffer( - encoder, - &self.uniforms, - 0, - (bytes.len() as u64).try_into().expect("Sized uniforms"), - device, - ) - .copy_from_slice(bytes); - - self.nearest - .upload(device, encoder, belt, nearest_instances); - - self.linear.upload(device, encoder, belt, linear_instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - self.nearest.render(render_pass); - self.linear.render(render_pass); - } -} - -#[derive(Debug)] -struct Data { - constants: wgpu::BindGroup, - instances: Buffer<Instance>, - instance_count: usize, -} - -impl Data { - pub fn new( - device: &wgpu::Device, - constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, - uniforms: &wgpu::Buffer, - ) -> Self { - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image constants bind group"), - layout: constant_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniforms, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(sampler), - }, - ], - }); - - let instances = Buffer::new( - device, - "iced_wgpu::image instance buffer", - Instance::INITIAL, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - Self { - constants, - instances, - instance_count: 0, - } - } - - fn upload( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - instances: &[Instance], - ) { - self.instance_count = instances.len(); - - if self.instance_count == 0 { - return; - } - - let _ = self.instances.resize(device, instances.len()); - let _ = self.instances.write(device, encoder, belt, 0, instances); - } - - fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - if self.instance_count == 0 { - return; - } - - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_vertex_buffer(0, self.instances.slice(..)); - - render_pass.draw(0..6, 0..self.instance_count as u32); - } -} - impl Pipeline { pub fn new( device: &wgpu::Device, @@ -276,9 +115,9 @@ impl Pipeline { label: Some("iced_wgpu image shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( concat!( - include_str!("shader/vertex.wgsl"), + include_str!("../shader/vertex.wgsl"), "\n", - include_str!("shader/image.wgsl"), + include_str!("../shader/image.wgsl"), ), )), }); @@ -341,54 +180,25 @@ impl Pipeline { multiview: None, }); - let texture_atlas = Atlas::new(device, backend); - - let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - texture_atlas.view(), - ), - }], - }); + let cache = Cache::new(device, backend); + let texture = cache.create_bind_group(device, &texture_layout); Pipeline { - #[cfg(feature = "image")] - raster_cache: RefCell::new(raster::Cache::default()), - - #[cfg(feature = "svg")] - vector_cache: RefCell::new(vector::Cache::default()), - pipeline, nearest_sampler, linear_sampler, texture, - texture_version: texture_atlas.layer_count(), - texture_atlas, + texture_version: cache.layer_count(), texture_layout, constant_layout, - + cache: cache::Shared::new(cache), layers: Vec::new(), prepare_layer: 0, } } - #[cfg(feature = "image")] - pub fn dimensions(&self, handle: &image::Handle) -> Size<u32> { - let mut cache = self.raster_cache.borrow_mut(); - let memory = cache.load(handle); - - memory.dimensions() - } - - #[cfg(feature = "svg")] - pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32> { - let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(handle); - - svg.viewport_dimensions() + pub fn cache(&self) -> &cache::Shared { + &self.cache } pub fn prepare( @@ -396,71 +206,56 @@ impl Pipeline { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, - images: &[layer::Image], + images: &Batch, transformation: Transformation, - _scale: f32, + scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "PREPARE").entered(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Image", "DRAW").entered(); + let transformation = transformation * Transformation::scale(scale); let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); - #[cfg(feature = "image")] - let mut raster_cache = self.raster_cache.borrow_mut(); - - #[cfg(feature = "svg")] - let mut vector_cache = self.vector_cache.borrow_mut(); + let mut cache = self.cache.lock(); for image in images { match &image { #[cfg(feature = "image")] - layer::Image::Raster { + Image::Raster { handle, filter_method, bounds, } => { - if let Some(atlas_entry) = raster_cache.upload( - device, - encoder, - handle, - &mut self.texture_atlas, - ) { + if let Some(atlas_entry) = + cache.upload_raster(device, encoder, handle) + { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, match filter_method { - image::FilterMethod::Nearest => { + crate::core::image::FilterMethod::Nearest => { nearest_instances } - image::FilterMethod::Linear => linear_instances, + crate::core::image::FilterMethod::Linear => { + linear_instances + } }, ); } } #[cfg(not(feature = "image"))] - layer::Image::Raster { .. } => {} + Image::Raster { .. } => {} #[cfg(feature = "svg")] - layer::Image::Vector { + Image::Vector { handle, color, bounds, } => { let size = [bounds.width, bounds.height]; - if let Some(atlas_entry) = vector_cache.upload( - device, - encoder, - handle, - *color, - size, - _scale, - &mut self.texture_atlas, + if let Some(atlas_entry) = cache.upload_vector( + device, encoder, handle, *color, size, scale, ) { add_instances( [bounds.x, bounds.y], @@ -471,7 +266,7 @@ impl Pipeline { } } #[cfg(not(feature = "svg"))] - layer::Image::Vector { .. } => {} + Image::Vector { .. } => {} } } @@ -479,23 +274,13 @@ impl Pipeline { return; } - let texture_version = self.texture_atlas.layer_count(); + let texture_version = cache.layer_count(); if self.texture_version != texture_version { log::info!("Atlas has grown. Recreating bind group..."); self.texture = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::image texture atlas bind group"), - layout: &self.texture_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - self.texture_atlas.view(), - ), - }], - }); - + cache.create_bind_group(device, &self.texture_layout); self.texture_version = texture_version; } @@ -545,13 +330,156 @@ impl Pipeline { } pub fn end_frame(&mut self) { - #[cfg(feature = "image")] - self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); + self.cache.lock().trim(); + self.prepare_layer = 0; + } +} - #[cfg(feature = "svg")] - self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + nearest: Data, + linear: Data, +} - self.prepare_layer = 0; +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + let uniforms = Uniforms { + transform: transformation.into(), + }; + + let bytes = bytemuck::bytes_of(&uniforms); + + belt.write_buffer( + encoder, + &self.uniforms, + 0, + (bytes.len() as u64).try_into().expect("Sized uniforms"), + device, + ) + .copy_from_slice(bytes); + + self.nearest + .upload(device, encoder, belt, nearest_instances); + + self.linear.upload(device, encoder, belt, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer<Instance>, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::image constants bind group"), + layout: constant_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniforms, + offset: 0, + size: None, + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(sampler), + }, + ], + }); + + let instances = Buffer::new( + device, + "iced_wgpu::image instance buffer", + Instance::INITIAL, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + constants, + instances, + instance_count: 0, + } + } + + fn upload( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + instances: &[Instance], + ) { + self.instance_count = instances.len(); + + if self.instance_count == 0 { + return; + } + + let _ = self.instances.resize(device, instances.len()); + let _ = self.instances.write(device, encoder, belt, 0, instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + if self.instance_count == 0 { + return; + } + + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(0, self.instances.slice(..)); + + render_pass.draw(0..6, 0..self.instance_count as u32); } } diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs new file mode 100644 index 00000000..c06d56be --- /dev/null +++ b/wgpu/src/image/null.rs @@ -0,0 +1,10 @@ +pub use crate::graphics::Image; + +#[derive(Debug, Default)] +pub struct Batch; + +impl Batch { + pub fn push(&mut self, _image: Image) {} + + pub fn clear(&mut self) {} +} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index cc767c25..c8c27c61 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,343 +1,324 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod pipeline; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use pipeline::Pipeline; -pub use text::Text; - -use crate::core; -use crate::core::alignment; -use crate::core::{ - Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector, -}; -use crate::graphics; +use crate::core::renderer; +use crate::core::{Background, Color, Point, Rectangle, Transformation}; use crate::graphics::color; -use crate::graphics::Viewport; -use crate::primitive::{self, Primitive}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Mesh; +use crate::image::{self, Image}; use crate::quad::{self, Quad}; +use crate::text::{self, Text}; +use crate::triangle; -/// A group of primitives that should be clipped together. #[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. +pub struct Layer { pub bounds: Rectangle, - - /// The quads of the [`Layer`]. pub quads: quad::Batch, - - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec<Mesh<'a>>, - - /// The text of the [`Layer`]. - pub text: Vec<Text<'a>>, - - /// The images of the [`Layer`]. - pub images: Vec<Image>, - - /// The custom pipelines of this [`Layer`]. - pub pipelines: Vec<Pipeline>, + pub triangles: triangle::Batch, + pub text: text::Batch, + pub images: image::Batch, } -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { +impl Default for Layer { + fn default() -> Self { Self { - bounds, + bounds: Rectangle::INFINITE, quads: quad::Batch::default(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - pipelines: Vec::new(), + triangles: triangle::Batch::default(), + text: text::Batch::default(), + images: image::Batch::default(), } } +} - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = text::Cached { - 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: Pixels(20.0), - line_height: core::text::LineHeight::default(), - font: Font::MONOSPACE, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }; - - overlay.text.push(Text::Cached(text.clone())); - - overlay.text.push(Text::Cached(text::Cached { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - })); +#[derive(Debug)] +pub struct Stack { + layers: Vec<Layer>, + transformations: Vec<Transformation>, + previous: Vec<usize>, + pending_meshes: Vec<Vec<Mesh>>, + pending_text: Vec<Vec<Text>>, + current: usize, + active_count: usize, +} + +impl Stack { + pub fn new() -> Self { + Self { + layers: vec![Layer::default()], + transformations: vec![Transformation::IDENTITY], + previous: vec![], + pending_meshes: vec![Vec::new()], + pending_text: vec![Vec::new()], + current: 0, + active_count: 1, } + } - overlay + pub fn draw_quad(&mut self, quad: renderer::Quad, background: Background) { + let transformation = self.transformations.last().unwrap(); + let bounds = quad.bounds * *transformation; + + let quad = Quad { + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + border_color: color::pack(quad.border.color), + border_radius: quad.border.radius.into(), + border_width: quad.border.width, + shadow_color: color::pack(quad.shadow.color), + shadow_offset: quad.shadow.offset.into(), + shadow_blur_radius: quad.shadow.blur_radius, + }; + + self.layers[self.current].quads.add(quad, &background); } - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec<Self> { - let first_layer = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - let mut layers = vec![first_layer]; - - for primitive in primitives { - Self::process_primitive( - &mut layers, - Transformation::IDENTITY, - primitive, - 0, - ); - } + pub fn draw_paragraph( + &mut self, + paragraph: &Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let paragraph = Text::Paragraph { + paragraph: paragraph.downgrade(), + position, + color, + clip_bounds, + transformation: self.transformations.last().copied().unwrap(), + }; + + self.pending_text[self.current].push(paragraph); + } - layers + pub fn draw_editor( + &mut self, + editor: &Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + let editor = Text::Editor { + editor: editor.downgrade(), + position, + color, + clip_bounds, + transformation: self.transformation(), + }; + + self.pending_text[self.current].push(editor); } - fn process_primitive( - layers: &mut Vec<Self>, - transformation: Transformation, - primitive: &'a Primitive, - current_layer: usize, + pub fn draw_text( + &mut self, + text: crate::core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, ) { - match primitive { - Primitive::Paragraph { - paragraph, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Paragraph { - paragraph: paragraph.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Editor { - editor, - position, - color, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Editor { - editor: editor.clone(), - position: *position, - color: *color, - clip_bounds: *clip_bounds, - transformation, - }); - } - Primitive::Text { - content, - bounds, - size, - line_height, - color, - font, - horizontal_alignment, - vertical_alignment, - shaping, - clip_bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text::Cached(text::Cached { - content, - bounds: *bounds + transformation.translation(), - size: *size * transformation.scale_factor(), - line_height: *line_height, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - shaping: *shaping, - clip_bounds: *clip_bounds * transformation, - })); - } - graphics::Primitive::RawText(raw) => { - let layer = &mut layers[current_layer]; + let transformation = self.transformation(); + + let text = Text::Cached { + content: text.content, + bounds: Rectangle::new(position, text.bounds) * transformation, + color, + size: text.size * transformation.scale_factor(), + line_height: text.line_height.to_absolute(text.size) + * transformation.scale_factor(), + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: clip_bounds * transformation, + }; + + self.pending_text[self.current].push(text); + } - layer.text.push(Text::Raw { - raw: raw.clone(), - transformation, - }); - } - Primitive::Quad { - bounds, - background, - border, - shadow, - } => { - let layer = &mut layers[current_layer]; - let bounds = *bounds * transformation; - - let quad = Quad { - position: [bounds.x, bounds.y], - size: [bounds.width, bounds.height], - border_color: color::pack(border.color), - border_radius: border.radius.into(), - border_width: border.width, - shadow_color: shadow.color.into_linear(), - shadow_offset: shadow.offset.into(), - shadow_blur_radius: shadow.blur_radius, - }; - - layer.quads.add(quad, background); - } - Primitive::Image { - handle, - filter_method, - bounds, - } => { - let layer = &mut layers[current_layer]; + pub fn draw_image( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, + ) { + let image = Image::Raster { + handle, + filter_method, + bounds: bounds * self.transformation(), + }; - layer.images.push(Image::Raster { - handle: handle.clone(), - filter_method: *filter_method, - bounds: *bounds * transformation, - }); + self.layers[self.current].images.push(image); + } + + pub fn draw_svg( + &mut self, + handle: crate::core::svg::Handle, + color: Option<Color>, + bounds: Rectangle, + ) { + let svg = Image::Vector { + handle, + color, + bounds: bounds * self.transformation(), + }; + + self.layers[self.current].images.push(svg); + } + + pub fn draw_mesh(&mut self, mut mesh: Mesh) { + match &mut mesh { + Mesh::Solid { transformation, .. } + | Mesh::Gradient { transformation, .. } => { + *transformation = *transformation * self.transformation(); } - Primitive::Svg { - handle, - color, + } + + self.pending_meshes[self.current].push(mesh); + } + + pub fn draw_mesh_group(&mut self, meshes: Vec<Mesh>) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current] + .triangles + .push(triangle::Item::Group { + transformation, + meshes, + }); + } + + pub fn draw_mesh_cache(&mut self, cache: triangle::Cache) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current] + .triangles + .push(triangle::Item::Cached { + transformation, + cache, + }); + } + + pub fn draw_text_group(&mut self, text: Vec<Text>) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current].text.push(text::Item::Group { + transformation, + text, + }); + } + + pub fn draw_text_cache(&mut self, cache: text::Cache) { + self.flush_pending(); + + let transformation = self.transformation(); + + self.layers[self.current].text.push(text::Item::Cached { + transformation, + cache, + }); + } + + pub fn push_clip(&mut self, bounds: Rectangle) { + self.previous.push(self.current); + + self.current = self.active_count; + self.active_count += 1; + + let bounds = bounds * self.transformation(); + + if self.current == self.layers.len() { + self.layers.push(Layer { bounds, - } => { - let layer = &mut layers[current_layer]; + ..Layer::default() + }); + self.pending_meshes.push(Vec::new()); + self.pending_text.push(Vec::new()); + } else { + self.layers[self.current].bounds = bounds; + } + } - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds * transformation, - }); - } - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - transformation, - primitive, - current_layer, - ); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds * transformation; - - // 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, - transformation, - content, - layers.len() - 1, - ); - } - } - Primitive::Transform { - transformation: new_transformation, - content, - } => { - Self::process_primitive( - layers, - transformation * *new_transformation, - content, - current_layer, - ); - } - Primitive::Cache { content } => { - Self::process_primitive( - layers, + pub fn pop_clip(&mut self) { + self.flush_pending(); + + self.current = self.previous.pop().unwrap(); + } + + pub fn push_transformation(&mut self, transformation: Transformation) { + self.flush_pending(); + + self.transformations + .push(self.transformation() * transformation); + } + + pub fn pop_transformation(&mut self) { + let _ = self.transformations.pop(); + } + + fn transformation(&self) -> Transformation { + self.transformations.last().copied().unwrap() + } + + pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Layer> { + self.flush_pending(); + + self.layers[..self.active_count].iter_mut() + } + + pub fn iter(&self) -> impl Iterator<Item = &Layer> { + self.layers[..self.active_count].iter() + } + + pub fn clear(&mut self) { + for (live, pending_meshes) in self.layers[..self.active_count] + .iter_mut() + .zip(self.pending_meshes.iter_mut()) + { + live.bounds = Rectangle::INFINITE; + + live.quads.clear(); + live.triangles.clear(); + live.text.clear(); + live.images.clear(); + pending_meshes.clear(); + } + + self.current = 0; + self.active_count = 1; + self.previous.clear(); + } + + // We want to keep the allocated memory + #[allow(clippy::drain_collect)] + fn flush_pending(&mut self) { + let transformation = self.transformation(); + + let pending_meshes = &mut self.pending_meshes[self.current]; + if !pending_meshes.is_empty() { + self.layers[self.current] + .triangles + .push(triangle::Item::Group { transformation, - content, - current_layer, - ); - } - Primitive::Custom(custom) => match custom { - primitive::Custom::Mesh(mesh) => match mesh { - graphics::Mesh::Solid { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Solid { - transformation, - buffers, - clip_bounds, - }); - } - } - graphics::Mesh::Gradient { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = - Rectangle::with_size(*size) * transformation; - - // Only draw visible content - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.meshes.push(Mesh::Gradient { - transformation, - buffers, - clip_bounds, - }); - } - } - }, - primitive::Custom::Pipeline(pipeline) => { - let layer = &mut layers[current_layer]; - let bounds = pipeline.bounds * transformation; - - if let Some(clip_bounds) = - layer.bounds.intersection(&bounds) - { - layer.pipelines.push(Pipeline { - bounds, - viewport: clip_bounds, - primitive: pipeline.primitive.clone(), - }); - } - } - }, + meshes: pending_meshes.drain(..).collect(), + }); } + + let pending_text = &mut self.pending_text[self.current]; + if !pending_text.is_empty() { + self.layers[self.current].text.push(text::Item::Group { + transformation, + text: pending_text.drain(..).collect(), + }); + } + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() } } diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs deleted file mode 100644 index facbe192..00000000 --- a/wgpu/src/layer/image.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; - -/// 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 filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs deleted file mode 100644 index 5ed7c654..00000000 --- a/wgpu/src/layer/mesh.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! A collection of triangle primitives. -use crate::core::{Rectangle, Transformation}; -use crate::graphics::mesh; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::SolidVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The [`Transformation`] for the vertices of the [`Mesh`]. - transformation: Transformation, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a mesh::Indexed<mesh::GradientVertex2D>, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle<f32>, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn transformation(&self) -> Transformation { - match self { - Self::Solid { transformation, .. } - | Self::Gradient { transformation, .. } => *transformation, - } - } - - /// Returns the indices of the [`Mesh`]. - pub fn indices(&self) -> &[u32] { - match self { - Self::Solid { buffers, .. } => &buffers.indices, - Self::Gradient { buffers, .. } => &buffers.indices, - } - } - - /// Returns the clip bounds of the [`Mesh`]. - pub fn clip_bounds(&self) -> Rectangle<f32> { - match self { - Self::Solid { clip_bounds, .. } - | Self::Gradient { clip_bounds, .. } => *clip_bounds, - } - } -} - -/// The result of counting the attributes of a set of meshes. -#[derive(Debug, Clone, Copy, Default)] -pub struct AttributeCount { - /// The total amount of solid vertices. - pub solid_vertices: usize, - - /// The total amount of solid meshes. - pub solids: usize, - - /// The total amount of gradient vertices. - pub gradient_vertices: usize, - - /// The total amount of gradient meshes. - pub gradients: 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.solids += 1; - count.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradients += 1; - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs deleted file mode 100644 index 6dfe6750..00000000 --- a/wgpu/src/layer/pipeline.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::core::Rectangle; -use crate::primitive::pipeline::Primitive; - -use std::sync::Arc; - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Pipeline { - /// The bounds of the [`Pipeline`]. - pub bounds: Rectangle, - - /// The viewport of the [`Pipeline`]. - pub viewport: Rectangle, - - /// The [`Primitive`] to render. - pub primitive: Arc<dyn Primitive>, -} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs deleted file mode 100644 index b3a00130..00000000 --- a/wgpu/src/layer/text.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::core::alignment; -use crate::core::text; -use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation}; -use crate::graphics; -use crate::graphics::text::editor; -use crate::graphics::text::paragraph; - -/// A text primitive. -#[derive(Debug, Clone)] -pub enum Text<'a> { - /// A paragraph. - #[allow(missing_docs)] - Paragraph { - paragraph: paragraph::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// An editor. - #[allow(missing_docs)] - Editor { - editor: editor::Weak, - position: Point, - color: Color, - clip_bounds: Rectangle, - transformation: Transformation, - }, - /// Some cached text. - Cached(Cached<'a>), - /// Some raw text. - #[allow(missing_docs)] - Raw { - raw: graphics::text::Raw, - transformation: Transformation, - }, -} - -#[derive(Debug, Clone)] -pub struct Cached<'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`] in logical pixels. - pub size: Pixels, - - /// The line height of the [`Text`]. - pub line_height: text::LineHeight, - - /// 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, - - /// The shaping strategy of the text. - pub shaping: text::Shaping, - - /// The clip bounds of the text. - pub clip_bounds: Rectangle, -} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 2cb9cf04..ccad08d5 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -21,6 +21,7 @@ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(missing_docs)] pub mod layer; pub mod primitive; pub mod settings; @@ -29,13 +30,21 @@ pub mod window; #[cfg(feature = "geometry")] pub mod geometry; -mod backend; mod buffer; mod color; +mod engine; mod quad; mod text; mod triangle; +#[cfg(any(feature = "image", feature = "svg"))] +#[path = "image/mod.rs"] +mod image; + +#[cfg(not(any(feature = "image", feature = "svg")))] +#[path = "image/null.rs"] +mod image; + use buffer::Buffer; pub use iced_graphics as graphics; @@ -43,16 +52,476 @@ pub use iced_graphics::core; pub use wgpu; -pub use backend::Backend; +pub use engine::Engine; pub use layer::Layer; pub use primitive::Primitive; pub use settings::Settings; -#[cfg(any(feature = "image", feature = "svg"))] -mod image; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + +use crate::core::{ + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, +}; +use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::Viewport; + +use std::borrow::Cow; /// A [`wgpu`] graphics renderer for [`iced`]. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = iced_graphics::Renderer<Backend>; +#[allow(missing_debug_implementations)] +pub struct Renderer { + default_font: Font, + default_text_size: Pixels, + layers: layer::Stack, + + triangle_storage: triangle::Storage, + text_storage: text::Storage, + + // TODO: Centralize all the image feature handling + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: image::cache::Shared, +} + +impl Renderer { + pub fn new(settings: Settings, _engine: &Engine) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + layers: layer::Stack::new(), + + triangle_storage: triangle::Storage::new(), + text_storage: text::Storage::new(), + + #[cfg(any(feature = "svg", feature = "image"))] + image_cache: _engine.image_cache().clone(), + } + } + + pub fn draw_primitive(&mut self, _primitive: Primitive) {} + + pub fn present<T: AsRef<str>>( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + clear_color: Option<Color>, + format: wgpu::TextureFormat, + frame: &wgpu::TextureView, + viewport: &Viewport, + overlay: &[T], + ) { + self.draw_overlay(overlay, viewport); + self.prepare(engine, device, queue, format, encoder, viewport); + self.render(engine, device, encoder, frame, clear_color, viewport); + + self.triangle_storage.trim(); + self.text_storage.trim(); + } + + fn prepare( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + queue: &wgpu::Queue, + _format: wgpu::TextureFormat, + encoder: &mut wgpu::CommandEncoder, + viewport: &Viewport, + ) { + let scale_factor = viewport.scale_factor() as f32; + + for layer in self.layers.iter_mut() { + if !layer.quads.is_empty() { + engine.quad_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.quads, + viewport.projection(), + scale_factor, + ); + } + + if !layer.triangles.is_empty() { + engine.triangle_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &mut self.triangle_storage, + &layer.triangles, + viewport.projection() * Transformation::scale(scale_factor), + ); + } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + viewport.physical_size(), + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.prepare( + device, + encoder, + &mut engine.staging_belt, + &layer.images, + viewport.projection(), + scale_factor, + ); + } + } + } + + fn render( + &mut self, + engine: &mut Engine, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + frame: &wgpu::TextureView, + clear_color: Option<Color>, + viewport: &Viewport, + ) { + use std::mem::ManuallyDrop; + + let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: match clear_color { + Some(background_color) => wgpu::LoadOp::Clear({ + let [r, g, b, a] = + graphics::color::pack(background_color) + .components(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + None => wgpu::LoadOp::Load, + }, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + + let mut quad_layer = 0; + let mut mesh_layer = 0; + let mut text_layer = 0; + + #[cfg(any(feature = "svg", feature = "image"))] + let mut image_layer = 0; + + let scale_factor = viewport.scale_factor() as f32; + let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( + viewport.physical_size(), + )); + + let scale = Transformation::scale(scale_factor); + + for layer in self.layers.iter() { + let Some(physical_bounds) = + physical_bounds.intersection(&(layer.bounds * scale)) + else { + continue; + }; + + let scissor_rect = physical_bounds.snap(); + + if !layer.quads.is_empty() { + engine.quad_pipeline.render( + quad_layer, + scissor_rect, + &layer.quads, + &mut render_pass, + ); + + quad_layer += 1; + } + + if !layer.triangles.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + mesh_layer += engine.triangle_pipeline.render( + device, + encoder, + frame, + &self.triangle_storage, + mesh_layer, + &layer.triangles, + viewport.physical_size(), + physical_bounds, + scale, + ); + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: frame, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } + + #[cfg(any(feature = "svg", feature = "image"))] + if !layer.images.is_empty() { + engine.image_pipeline.render( + image_layer, + scissor_rect, + &mut render_pass, + ); + + image_layer += 1; + } + } + + let _ = ManuallyDrop::into_inner(render_pass); + } + + fn draw_overlay( + &mut self, + overlay: &[impl AsRef<str>], + viewport: &Viewport, + ) { + use crate::core::alignment; + use crate::core::text::Renderer as _; + use crate::core::Renderer as _; + use crate::core::Vector; + + self.with_layer( + Rectangle::with_size(viewport.logical_size()), + |renderer| { + for (i, line) in overlay.iter().enumerate() { + let text = crate::core::Text { + content: line.as_ref().to_owned(), + bounds: viewport.logical_size(), + size: Pixels(20.0), + line_height: core::text::LineHeight::default(), + font: Font::MONOSPACE, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: core::text::Shaping::Basic, + }; + + renderer.fill_text( + text.clone(), + Point::new(11.0, 11.0 + 25.0 * i as f32), + Color::new(0.9, 0.9, 0.9, 1.0), + Rectangle::with_size(Size::INFINITY), + ); + + renderer.fill_text( + text, + Point::new(11.0, 11.0 + 25.0 * i as f32) + + Vector::new(-1.0, -1.0), + Color::BLACK, + Rectangle::with_size(Size::INFINITY), + ); + } + }, + ); + } +} + +impl core::Renderer for Renderer { + fn start_layer(&mut self, bounds: Rectangle) { + self.layers.push_clip(bounds); + } + + fn end_layer(&mut self, _bounds: Rectangle) { + self.layers.pop_clip(); + } + + fn start_transformation(&mut self, transformation: Transformation) { + self.layers.push_transformation(transformation); + } + + fn end_transformation(&mut self, _transformation: Transformation) { + self.layers.pop_transformation(); + } + + fn fill_quad( + &mut self, + quad: core::renderer::Quad, + background: impl Into<Background>, + ) { + self.layers.draw_quad(quad, background.into()); + } + + fn clear(&mut self) { + self.layers.clear(); + } +} + +impl core::text::Renderer for Renderer { + type Font = Font; + type Paragraph = Paragraph; + type Editor = Editor; + + const ICON_FONT: Font = Font::with_name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Self::Font { + self.default_font + } + + fn default_size(&self) -> Pixels { + self.default_text_size + } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + graphics::text::font_system() + .write() + .expect("Write font system") + .load_font(font); + + // TODO: Invalidate buffer cache + } + + fn fill_paragraph( + &mut self, + text: &Self::Paragraph, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers + .draw_paragraph(text, position, color, clip_bounds); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers + .draw_editor(editor, position, color, clip_bounds); + } + + fn fill_text( + &mut self, + text: core::Text, + position: Point, + color: Color, + clip_bounds: Rectangle, + ) { + self.layers.draw_text(text, position, color, clip_bounds); + } +} + +#[cfg(feature = "image")] +impl core::image::Renderer for Renderer { + type Handle = core::image::Handle; + + fn measure_image(&self, handle: &Self::Handle) -> Size<u32> { + self.image_cache.lock().measure_image(handle) + } + + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + ) { + self.layers.draw_image(handle, filter_method, bounds); + } +} + +#[cfg(feature = "svg")] +impl core::svg::Renderer for Renderer { + fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> { + self.image_cache.lock().measure_svg(handle) + } + + fn draw_svg( + &mut self, + handle: core::svg::Handle, + color_filter: Option<Color>, + bounds: Rectangle, + ) { + self.layers.draw_svg(handle, color_filter, bounds); + } +} + +impl graphics::mesh::Renderer for Renderer { + fn draw_mesh(&mut self, mesh: graphics::Mesh) { + self.layers.draw_mesh(mesh); + } +} + +#[cfg(feature = "geometry")] +impl graphics::geometry::Renderer for Renderer { + type Geometry = Geometry; + type Frame = geometry::Frame; + + fn new_frame(&self, size: Size) -> Self::Frame { + geometry::Frame::new(size) + } + + fn draw_geometry(&mut self, geometry: Self::Geometry) { + match geometry { + Geometry::Live { meshes, text } => { + self.layers.draw_mesh_group(meshes); + self.layers.draw_text_group(text); + } + Geometry::Cached(cache) => { + if let Some(meshes) = cache.meshes { + self.layers.draw_mesh_cache(meshes); + } + + if let Some(text) = cache.text { + self.layers.draw_text_cache(text); + } + } + } + } +} + +impl graphics::compositor::Default for crate::Renderer { + type Compositor = window::Compositor; +} diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index ee9af93c..8e311d2b 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -3,8 +3,7 @@ pub mod pipeline; pub use pipeline::Pipeline; -use crate::core::Rectangle; -use crate::graphics::{Damage, Mesh}; +use crate::graphics::Mesh; use std::fmt::Debug; @@ -19,20 +18,3 @@ pub enum Custom { /// A custom pipeline primitive. Pipeline(Pipeline), } - -impl Damage for Custom { - fn bounds(&self) -> Rectangle { - match self { - Self::Mesh(mesh) => mesh.bounds(), - Self::Pipeline(pipeline) => pipeline.bounds, - } - } -} - -impl TryFrom<Mesh> for Custom { - type Error = &'static str; - - fn try_from(mesh: Mesh) -> Result<Self, Self::Error> { - Ok(Custom::Mesh(mesh)) - } -} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 0717a031..de432d2f 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable}; use std::mem; -#[cfg(feature = "tracing")] -use tracing::info_span; - const INITIAL_INSTANCES: usize = 2_000; +/// The properties of a quad. +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: color::Packed, + + /// The border radii of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The shadow color of the [`Quad`]. + pub shadow_color: color::Packed, + + /// The shadow offset of the [`Quad`]. + pub shadow_offset: [f32; 2], + + /// The shadow blur radius of the [`Quad`]. + pub shadow_blur_radius: f32, +} + #[derive(Debug)] pub struct Pipeline { solid: solid::Pipeline, @@ -124,7 +150,7 @@ impl Pipeline { } #[derive(Debug)] -struct Layer { +pub struct Layer { constants: wgpu::BindGroup, constants_buffer: wgpu::Buffer, solid: solid::Layer, @@ -169,9 +195,26 @@ impl Layer { transformation: Transformation, scale: f32, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "PREPARE").entered(); + self.update(device, encoder, belt, transformation, scale); + + if !quads.solids.is_empty() { + self.solid.prepare(device, encoder, belt, &quads.solids); + } + if !quads.gradients.is_empty() { + self.gradient + .prepare(device, encoder, belt, &quads.gradients); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + transformation: Transformation, + scale: f32, + ) { let uniforms = Uniforms::new(transformation, scale); let bytes = bytemuck::bytes_of(&uniforms); @@ -183,47 +226,9 @@ impl Layer { device, ) .copy_from_slice(bytes); - - if !quads.solids.is_empty() { - self.solid.prepare(device, encoder, belt, &quads.solids); - } - - if !quads.gradients.is_empty() { - self.gradient - .prepare(device, encoder, belt, &quads.gradients); - } } } -/// The properties of a quad. -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: color::Packed, - - /// The border radii of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The shadow color of the [`Quad`]. - pub shadow_color: [f32; 4], - - /// The shadow offset of the [`Quad`]. - pub shadow_offset: [f32; 2], - - /// The shadow blur radius of the [`Quad`]. - pub shadow_blur_radius: f32, -} - /// A group of [`Quad`]s rendered together. #[derive(Default, Debug)] pub struct Batch { @@ -233,10 +238,13 @@ pub struct Batch { /// The gradient quads of the [`Layer`]. gradients: Vec<Gradient>, - /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count. - order: Vec<(Kind, usize)>, + /// The quad order of the [`Layer`]. + order: Order, } +/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count. +type Order = Vec<(Kind, usize)>; + impl Batch { /// Returns true if there are no quads of any type in [`Quads`]. pub fn is_empty(&self) -> bool { @@ -276,6 +284,12 @@ impl Batch { } } } + + pub fn clear(&mut self) { + self.solids.clear(); + self.gradients.clear(); + self.order.clear(); + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 828d9e09..a6aea0a5 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -2,18 +2,18 @@ use crate::core::{Font, Pixels}; use crate::graphics::{self, Antialiasing}; -/// The settings of a [`Backend`]. +/// The settings of a [`Renderer`]. /// -/// [`Backend`]: crate::Backend +/// [`Renderer`]: crate::Renderer #[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { - /// The present mode of the [`Backend`]. + /// The present mode of the [`Renderer`]. /// - /// [`Backend`]: crate::Backend + /// [`Renderer`]: crate::Renderer pub present_mode: wgpu::PresentMode, - /// The internal graphics backend to use. - pub internal_backend: wgpu::Backends, + /// The graphics backends to use. + pub backends: wgpu::Backends, /// The default [`Font`] to use. pub default_font: Font, @@ -33,7 +33,7 @@ impl Default for Settings { fn default() -> Settings { Settings { present_mode: wgpu::PresentMode::AutoVsync, - internal_backend: wgpu::Backends::all(), + backends: wgpu::Backends::all(), default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: None, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 97ff77f5..1b21bb1c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,20 +1,193 @@ use crate::core::alignment; use crate::core::{Rectangle, Size, Transformation}; use crate::graphics::color; -use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::cache::{self, Cache as BufferCache}; use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; -use crate::layer::Text; -use std::borrow::Cow; -use std::cell::RefCell; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::hash_map; +use std::rc::Rc; +use std::sync::atomic::{self, AtomicU64}; use std::sync::Arc; +pub use crate::graphics::Text; + +const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION { + glyphon::ColorMode::Accurate +} else { + glyphon::ColorMode::Web +}; + +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + text: Vec<Text>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + text: Rc<[Text]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(text: Vec<Text>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if text.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + text: Rc::from(text), + version: 0, + }) + } + + pub fn update(&mut self, text: Vec<Text>) { + self.text = Rc::from(text); + self.version += 1; + } +} + +struct Upload { + renderer: glyphon::TextRenderer, + atlas: glyphon::TextAtlas, + buffer_cache: BufferCache, + transformation: Transformation, + version: usize, +} + +#[derive(Default)] +pub struct Storage { + uploads: FxHashMap<Id, Upload>, + recently_used: FxHashSet<Id>, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.text.is_empty() { + return None; + } + + self.uploads.get(&cache.id) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + format: wgpu::TextureFormat, + cache: &Cache, + new_transformation: Transformation, + bounds: Rectangle, + target_size: Size<u32>, + ) { + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if !cache.text.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) + { + let _ = prepare( + device, + queue, + encoder, + &mut upload.renderer, + &mut upload.atlas, + &mut upload.buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + + upload.version = cache.version; + upload.transformation = new_transformation; + + upload.buffer_cache.trim(); + upload.atlas.trim(); + } + } + hash_map::Entry::Vacant(entry) => { + let mut atlas = glyphon::TextAtlas::with_color_mode( + device, queue, format, COLOR_MODE, + ); + + let mut renderer = glyphon::TextRenderer::new( + &mut atlas, + device, + wgpu::MultisampleState::default(), + None, + ); + + let mut buffer_cache = BufferCache::new(); + + let _ = prepare( + device, + queue, + encoder, + &mut renderer, + &mut atlas, + &mut buffer_cache, + &cache.text, + bounds, + new_transformation, + target_size, + ); + + let _ = entry.insert(Upload { + renderer, + atlas, + buffer_cache, + transformation: new_transformation, + version: 0, + }); + + log::info!( + "New text upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + + let _ = self.recently_used.insert(cache.id); + } + + pub fn trim(&mut self) { + self.uploads.retain(|id, _| self.recently_used.contains(id)); + self.recently_used.clear(); + } +} + #[allow(missing_debug_implementations)] pub struct Pipeline { - renderers: Vec<glyphon::TextRenderer>, + format: wgpu::TextureFormat, atlas: glyphon::TextAtlas, + renderers: Vec<glyphon::TextRenderer>, prepare_layer: usize, - cache: RefCell<Cache>, + cache: BufferCache, } impl Pipeline { @@ -24,275 +197,95 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { + format, renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( - device, - queue, - format, - if color::GAMMA_CORRECTION { - glyphon::ColorMode::Accurate - } else { - glyphon::ColorMode::Web - }, + device, queue, format, COLOR_MODE, ), prepare_layer: 0, - cache: RefCell::new(Cache::new()), + cache: BufferCache::new(), } } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - font_system() - .write() - .expect("Write font system") - .load_font(bytes); - - self.cache = RefCell::new(Cache::new()); - } - pub fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, - sections: &[Text<'_>], + storage: &mut Storage, + batch: &Batch, layer_bounds: Rectangle, - scale_factor: f32, + layer_transformation: Transformation, target_size: Size<u32>, ) { - if self.renderers.len() <= self.prepare_layer { - self.renderers.push(glyphon::TextRenderer::new( - &mut self.atlas, - device, - wgpu::MultisampleState::default(), - None, - )); - } - - let mut font_system = font_system().write().expect("Write font system"); - let font_system = font_system.raw(); - - let renderer = &mut self.renderers[self.prepare_layer]; - let cache = self.cache.get_mut(); - - enum Allocation { - Paragraph(Paragraph), - Editor(Editor), - Cache(cache::KeyHash), - Raw(Arc<glyphon::Buffer>), - } - - let allocations: Vec<_> = sections - .iter() - .map(|section| match section { - Text::Paragraph { paragraph, .. } => { - paragraph.upgrade().map(Allocation::Paragraph) - } - Text::Editor { editor, .. } => { - editor.upgrade().map(Allocation::Editor) - } - Text::Cached(text) => { - let (key, _) = cache.allocate( - font_system, - cache::Key { - content: text.content, - size: text.size.into(), - line_height: f32::from( - text.line_height.to_absolute(text.size), - ), - font: text.font, - bounds: Size { - width: text.bounds.width, - height: text.bounds.height, - }, - shaping: text.shaping, - }, - ); - - Some(Allocation::Cache(key)) - } - Text::Raw { raw, .. } => { - raw.buffer.upgrade().map(Allocation::Raw) - } - }) - .collect(); - - let layer_bounds = layer_bounds * scale_factor; - - let text_areas = sections.iter().zip(allocations.iter()).filter_map( - |(section, allocation)| { - let ( - buffer, - bounds, - horizontal_alignment, - vertical_alignment, - color, - clip_bounds, + for item in batch { + match item { + Item::Group { transformation, - ) = match section { - Text::Paragraph { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Paragraph as _; - - let Some(Allocation::Paragraph(paragraph)) = allocation - else { - return None; - }; - - ( - paragraph.buffer(), - Rectangle::new(*position, paragraph.min_bounds()), - paragraph.horizontal_alignment(), - paragraph.vertical_alignment(), - *color, - *clip_bounds, - *transformation, - ) - } - Text::Editor { - position, - color, - clip_bounds, - transformation, - .. - } => { - use crate::core::text::Editor as _; - - let Some(Allocation::Editor(editor)) = allocation - else { - return None; - }; - - ( - editor.buffer(), - Rectangle::new(*position, editor.bounds()), - alignment::Horizontal::Left, - alignment::Vertical::Top, - *color, - *clip_bounds, - *transformation, - ) - } - Text::Cached(text) => { - let Some(Allocation::Cache(key)) = allocation else { - return None; - }; - - let entry = cache.get(key).expect("Get cached buffer"); - - ( - &entry.buffer, - Rectangle::new( - text.bounds.position(), - entry.min_bounds, - ), - text.horizontal_alignment, - text.vertical_alignment, - text.color, - text.clip_bounds, - Transformation::IDENTITY, - ) - } - Text::Raw { - raw, - transformation, - } => { - let Some(Allocation::Raw(buffer)) = allocation else { - return None; - }; - - let (width, height) = buffer.size(); - - ( - buffer.as_ref(), - Rectangle::new( - raw.position, - Size::new(width, height), - ), - alignment::Horizontal::Left, - alignment::Vertical::Top, - raw.color, - raw.clip_bounds, - *transformation, - ) + text, + } => { + if self.renderers.len() <= self.prepare_layer { + self.renderers.push(glyphon::TextRenderer::new( + &mut self.atlas, + device, + wgpu::MultisampleState::default(), + None, + )); } - }; - let bounds = bounds * transformation * scale_factor; - - let left = match horizontal_alignment { - alignment::Horizontal::Left => bounds.x, - alignment::Horizontal::Center => { - bounds.x - bounds.width / 2.0 - } - alignment::Horizontal::Right => bounds.x - bounds.width, - }; + let renderer = &mut self.renderers[self.prepare_layer]; + let result = prepare( + device, + queue, + encoder, + renderer, + &mut self.atlas, + &mut self.cache, + text, + layer_bounds, + layer_transformation * *transformation, + target_size, + ); - let top = match vertical_alignment { - alignment::Vertical::Top => bounds.y, - alignment::Vertical::Center => { - bounds.y - bounds.height / 2.0 + match result { + Ok(()) => { + self.prepare_layer += 1; + } + Err(glyphon::PrepareError::AtlasFull) => { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + } } - alignment::Vertical::Bottom => bounds.y - bounds.height, - }; - - let clip_bounds = layer_bounds.intersection( - &(clip_bounds * transformation * scale_factor), - )?; - - Some(glyphon::TextArea { - buffer, - left, - top, - scale: scale_factor * transformation.scale_factor(), - bounds: glyphon::TextBounds { - left: clip_bounds.x as i32, - top: clip_bounds.y as i32, - right: (clip_bounds.x + clip_bounds.width) as i32, - bottom: (clip_bounds.y + clip_bounds.height) as i32, - }, - default_color: to_color(color), - }) - }, - ); - - let result = renderer.prepare( - device, - queue, - encoder, - font_system, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - &mut glyphon::SwashCache::new(), - ); - - match result { - Ok(()) => { - self.prepare_layer += 1; - } - Err(glyphon::PrepareError::AtlasFull) => { - // If the atlas cannot grow, then all bets are off. - // Instead of panicking, we will just pray that the result - // will be somewhat readable... + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + queue, + encoder, + self.format, + cache, + layer_transformation * *transformation, + layer_bounds, + target_size, + ); + } } } } pub fn render<'a>( &'a self, - layer: usize, + storage: &'a Storage, + start: usize, + batch: &'a Batch, bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, - ) { - let renderer = &self.renderers[layer]; + ) -> usize { + let mut layer_count = 0; render_pass.set_scissor_rect( bounds.x, @@ -301,15 +294,253 @@ impl Pipeline { bounds.height, ); - renderer - .render(&self.atlas, render_pass) - .expect("Render text"); + for item in batch { + match item { + Item::Group { .. } => { + let renderer = &self.renderers[start + layer_count]; + + renderer + .render(&self.atlas, render_pass) + .expect("Render text"); + + layer_count += 1; + } + Item::Cached { cache, .. } => { + if let Some(upload) = storage.get(cache) { + upload + .renderer + .render(&upload.atlas, render_pass) + .expect("Render cached text"); + } + } + } + } + + layer_count } pub fn end_frame(&mut self) { self.atlas.trim(); - self.cache.get_mut().trim(); + self.cache.trim(); self.prepare_layer = 0; } } + +fn prepare( + device: &wgpu::Device, + queue: &wgpu::Queue, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut glyphon::TextRenderer, + atlas: &mut glyphon::TextAtlas, + buffer_cache: &mut BufferCache, + sections: &[Text], + layer_bounds: Rectangle, + layer_transformation: Transformation, + target_size: Size<u32>, +) -> Result<(), glyphon::PrepareError> { + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + + enum Allocation { + Paragraph(Paragraph), + Editor(Editor), + Cache(cache::KeyHash), + Raw(Arc<glyphon::Buffer>), + } + + let allocations: Vec<_> = sections + .iter() + .map(|section| match section { + Text::Paragraph { paragraph, .. } => { + paragraph.upgrade().map(Allocation::Paragraph) + } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } + Text::Cached { + content, + bounds, + size, + line_height, + font, + shaping, + .. + } => { + let (key, _) = buffer_cache.allocate( + font_system, + cache::Key { + content, + size: f32::from(*size), + line_height: f32::from(*line_height), + font: *font, + bounds: Size { + width: bounds.width, + height: bounds.height, + }, + shaping: *shaping, + }, + ); + + Some(Allocation::Cache(key)) + } + Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw), + }) + .collect(); + + let layer_bounds = layer_bounds * layer_transformation; + + let text_areas = sections.iter().zip(allocations.iter()).filter_map( + |(section, allocation)| { + let ( + buffer, + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + transformation, + ) = match section { + Text::Paragraph { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Paragraph as _; + + let Some(Allocation::Paragraph(paragraph)) = allocation + else { + return None; + }; + + ( + paragraph.buffer(), + Rectangle::new(*position, paragraph.min_bounds()), + paragraph.horizontal_alignment(), + paragraph.vertical_alignment(), + *color, + *clip_bounds, + *transformation, + ) + } + Text::Editor { + position, + color, + clip_bounds, + transformation, + .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + *clip_bounds, + *transformation, + ) + } + Text::Cached { + bounds, + horizontal_alignment, + vertical_alignment, + color, + clip_bounds, + .. + } => { + let Some(Allocation::Cache(key)) = allocation else { + return None; + }; + + let entry = + buffer_cache.get(key).expect("Get cached buffer"); + + ( + &entry.buffer, + Rectangle::new(bounds.position(), entry.min_bounds), + *horizontal_alignment, + *vertical_alignment, + *color, + *clip_bounds, + Transformation::IDENTITY, + ) + } + Text::Raw { + raw, + transformation, + } => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new(raw.position, Size::new(width, height)), + alignment::Horizontal::Left, + alignment::Vertical::Top, + raw.color, + raw.clip_bounds, + *transformation, + ) + } + }; + + let bounds = bounds * transformation * layer_transformation; + + let left = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - bounds.width / 2.0, + alignment::Horizontal::Right => bounds.x - bounds.width, + }; + + let top = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - bounds.height / 2.0, + alignment::Vertical::Bottom => bounds.y - bounds.height, + }; + + let clip_bounds = layer_bounds.intersection( + &(clip_bounds * transformation * layer_transformation), + )?; + + Some(glyphon::TextArea { + buffer, + left, + top, + scale: transformation.scale_factor() + * layer_transformation.scale_factor(), + bounds: glyphon::TextBounds { + left: clip_bounds.x as i32, + top: clip_bounds.y as i32, + right: (clip_bounds.x + clip_bounds.width) as i32, + bottom: (clip_bounds.y + clip_bounds.height) as i32, + }, + default_color: to_color(color), + }) + }, + ); + + renderer.prepare( + device, + queue, + encoder, + font_system, + atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + text_areas, + &mut glyphon::SwashCache::new(), + ) +} diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index b6be54d4..06c0ef02 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,14 +1,158 @@ //! Draw meshes of triangles. mod msaa; -use crate::core::{Size, Transformation}; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::mesh::{self, Mesh}; use crate::graphics::Antialiasing; -use crate::layer::mesh::{self, Mesh}; use crate::Buffer; +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::hash_map; +use std::rc::Rc; +use std::sync::atomic::{self, AtomicU64}; + const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; +pub type Batch = Vec<Item>; + +#[derive(Debug)] +pub enum Item { + Group { + transformation: Transformation, + meshes: Vec<Mesh>, + }, + Cached { + transformation: Transformation, + cache: Cache, + }, +} + +#[derive(Debug, Clone)] +pub struct Cache { + id: Id, + batch: Rc<[Mesh]>, + version: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(u64); + +impl Cache { + pub fn new(meshes: Vec<Mesh>) -> Option<Self> { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + + if meshes.is_empty() { + return None; + } + + Some(Self { + id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), + batch: Rc::from(meshes), + version: 0, + }) + } + + pub fn update(&mut self, meshes: Vec<Mesh>) { + self.batch = Rc::from(meshes); + self.version += 1; + } +} + +#[derive(Debug)] +struct Upload { + layer: Layer, + transformation: Transformation, + version: usize, +} + +#[derive(Debug, Default)] +pub struct Storage { + uploads: FxHashMap<Id, Upload>, + recently_used: FxHashSet<Id>, +} + +impl Storage { + pub fn new() -> Self { + Self::default() + } + + fn get(&self, cache: &Cache) -> Option<&Upload> { + if cache.batch.is_empty() { + return None; + } + + self.uploads.get(&cache.id) + } + + fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + cache: &Cache, + new_transformation: Transformation, + ) { + match self.uploads.entry(cache.id) { + hash_map::Entry::Occupied(entry) => { + let upload = entry.into_mut(); + + if !cache.batch.is_empty() + && (upload.version != cache.version + || upload.transformation != new_transformation) + { + upload.layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + upload.version = cache.version; + upload.transformation = new_transformation; + } + } + hash_map::Entry::Vacant(entry) => { + let mut layer = Layer::new(device, solid, gradient); + + layer.prepare( + device, + encoder, + belt, + solid, + gradient, + &cache.batch, + new_transformation, + ); + + let _ = entry.insert(Upload { + layer, + transformation: new_transformation, + version: 0, + }); + + log::info!( + "New mesh upload: {} (total: {})", + cache.id.0, + self.uploads.len() + ); + } + } + + let _ = self.recently_used.insert(cache.id); + } + + pub fn trim(&mut self) { + self.uploads.retain(|id, _| self.recently_used.contains(id)); + self.recently_used.clear(); + } +} + #[derive(Debug)] pub struct Pipeline { blit: Option<msaa::Blit>, @@ -18,8 +162,197 @@ pub struct Pipeline { prepare_layer: usize, } +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option<Antialiasing>, + ) -> Pipeline { + Pipeline { + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + gradient: gradient::Pipeline::new(device, format, antialiasing), + layers: Vec::new(), + prepare_layer: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + belt: &mut wgpu::util::StagingBelt, + storage: &mut Storage, + items: &[Item], + projection: Transformation, + ) { + for item in items { + match item { + Item::Group { + transformation, + meshes, + } => { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.solid, + &self.gradient, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + meshes, + projection * *transformation, + ); + + self.prepare_layer += 1; + } + Item::Cached { + transformation, + cache, + } => { + storage.prepare( + device, + encoder, + belt, + &self.solid, + &self.gradient, + cache, + projection * *transformation, + ); + } + } + } + } + + pub fn render( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + storage: &Storage, + start: usize, + batch: &Batch, + target_size: Size<u32>, + bounds: Rectangle, + screen_transformation: Transformation, + ) -> usize { + let mut layer_count = 0; + + let items = batch.iter().filter_map(|item| match item { + Item::Group { + transformation, + meshes, + } => { + let layer = &self.layers[start + layer_count]; + layer_count += 1; + + Some(( + layer, + meshes.as_slice(), + screen_transformation * *transformation, + )) + } + Item::Cached { + transformation, + cache, + } => { + let upload = storage.get(cache)?; + + Some(( + &upload.layer, + &cache.batch, + screen_transformation * *transformation, + )) + } + }); + + render( + device, + encoder, + target, + self.blit.as_mut(), + &self.solid, + &self.gradient, + target_size, + bounds, + items, + ); + + layer_count + } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + } +} + +fn render<'a>( + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + mut blit: Option<&mut msaa::Blit>, + solid: &solid::Pipeline, + gradient: &gradient::Pipeline, + target_size: Size<u32>, + bounds: Rectangle, + group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>, +) { + { + let (attachment, resolve_target, load) = if let Some(blit) = &mut blit { + let (attachment, resolve_target) = + blit.targets(device, target_size.width, target_size.height); + + ( + attachment, + Some(resolve_target), + wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + ) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu.triangle.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: attachment, + resolve_target, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + for (layer, meshes, transformation) in group { + layer.render( + solid, + gradient, + meshes, + bounds, + transformation, + &mut render_pass, + ); + } + } + + if let Some(blit) = blit { + blit.draw(encoder, target); + } +} + #[derive(Debug)] -struct Layer { +pub struct Layer { index_buffer: Buffer<u32>, index_strides: Vec<u32>, solid: solid::Layer, @@ -52,7 +385,7 @@ impl Layer { belt: &mut wgpu::util::StagingBelt, solid: &solid::Pipeline, gradient: &gradient::Pipeline, - meshes: &[Mesh<'_>], + meshes: &[Mesh], transformation: Transformation, ) { // Count the total amount of vertices & indices we need to handle @@ -157,8 +490,9 @@ impl Layer { &'a self, solid: &'a solid::Pipeline, gradient: &'a gradient::Pipeline, - meshes: &[Mesh<'_>], - scale_factor: f32, + meshes: &[Mesh], + bounds: Rectangle, + transformation: Transformation, render_pass: &mut wgpu::RenderPass<'a>, ) { let mut num_solids = 0; @@ -166,11 +500,13 @@ impl Layer { let mut last_is_solid = None; for (index, mesh) in meshes.iter().enumerate() { - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); - - if clip_bounds.width < 1 || clip_bounds.height < 1 { + let Some(clip_bounds) = + bounds.intersection(&(mesh.clip_bounds() * transformation)) + else { continue; - } + }; + + let clip_bounds = clip_bounds.snap(); render_pass.set_scissor_rect( clip_bounds.x, @@ -234,119 +570,6 @@ impl Layer { } } -impl Pipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - antialiasing: Option<Antialiasing>, - ) -> Pipeline { - Pipeline { - blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), - solid: solid::Pipeline::new(device, format, antialiasing), - gradient: gradient::Pipeline::new(device, format, antialiasing), - layers: Vec::new(), - prepare_layer: 0, - } - } - - pub fn prepare( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - meshes: &[Mesh<'_>], - transformation: Transformation, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered(); - - if self.layers.len() <= self.prepare_layer { - self.layers - .push(Layer::new(device, &self.solid, &self.gradient)); - } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare( - device, - encoder, - belt, - &self.solid, - &self.gradient, - meshes, - transformation, - ); - - self.prepare_layer += 1; - } - - pub fn render( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - layer: usize, - target_size: Size<u32>, - meshes: &[Mesh<'_>], - scale_factor: f32, - ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered(); - - { - let (attachment, resolve_target, load) = if let Some(blit) = - &mut self.blit - { - let (attachment, resolve_target) = - blit.targets(device, target_size.width, target_size.height); - - ( - attachment, - Some(resolve_target), - wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - ) - } else { - (target, None, wgpu::LoadOp::Load) - }; - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu.triangle.render_pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: attachment, - resolve_target, - ops: wgpu::Operations { - load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - let layer = &mut self.layers[layer]; - - layer.render( - &self.solid, - &self.gradient, - meshes, - scale_factor, - &mut render_pass, - ); - } - - if let Some(blit) = &mut self.blit { - blit.draw(encoder, target); - } - } - - pub fn end_frame(&mut self) { - self.prepare_layer = 0; - } -} - fn fragment_target( texture_format: wgpu::TextureFormat, ) -> wgpu::ColorTargetState { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 482d705b..d546a6dc 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -4,18 +4,19 @@ use crate::graphics::color; use crate::graphics::compositor; use crate::graphics::error; use crate::graphics::{self, Viewport}; -use crate::{Backend, Primitive, Renderer, Settings}; +use crate::{Engine, Renderer, Settings}; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { - settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat, alpha_mode: wgpu::CompositeAlphaMode, + engine: Engine, + settings: Settings, } /// A compositor error. @@ -53,7 +54,7 @@ impl Compositor { compatible_window: Option<W>, ) -> Result<Self, Error> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: settings.internal_backend, + backends: settings.backends, ..Default::default() }); @@ -62,7 +63,7 @@ impl Compositor { #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance - .enumerate_adapters(settings.internal_backend) + .enumerate_adapters(settings.backends) .iter() .map(wgpu::Adapter::get_info) .collect(); @@ -167,15 +168,24 @@ impl Compositor { match result { Ok((device, queue)) => { + let engine = Engine::new( + &adapter, + &device, + &queue, + format, + settings.antialiasing, + ); + return Ok(Compositor { instance, - settings, adapter, device, queue, format, alpha_mode, - }) + engine, + settings, + }); } Err(error) => { errors.push((required_limits, error)); @@ -185,21 +195,9 @@ impl Compositor { Err(Error::RequestDeviceFailed(errors)) } - - /// Creates a new rendering [`Backend`] for this [`Compositor`]. - pub fn create_backend(&self) -> Backend { - Backend::new( - &self.adapter, - &self.device, - &self.queue, - self.settings, - self.format, - ) - } } -/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and -/// window. +/// Creates a [`Compositor`] with the given [`Settings`] and window. pub async fn new<W: compositor::Window>( settings: Settings, compatible_window: W, @@ -207,12 +205,11 @@ pub async fn new<W: compositor::Window>( Compositor::request(settings, Some(compatible_window)).await } -/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +/// Presents the given primitives with the given [`Compositor`]. pub fn present<T: AsRef<str>>( compositor: &mut Compositor, - backend: &mut Backend, + renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, - primitives: &[Primitive], viewport: &Viewport, background_color: Color, overlay: &[T], @@ -229,21 +226,21 @@ pub fn present<T: AsRef<str>>( .texture .create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), frame.texture.format(), view, - primitives, viewport, overlay, ); - // Submit work - let _submission = compositor.queue.submit(Some(encoder.finish())); - backend.recall(); + let _ = compositor.engine.submit(&compositor.queue, encoder); + + // Present the frame frame.present(); Ok(()) @@ -275,7 +272,7 @@ impl graphics::Compositor for Compositor { match backend { None | Some("wgpu") => Ok(new( Settings { - internal_backend: wgpu::util::backend_bits_from_env() + backends: wgpu::util::backend_bits_from_env() .unwrap_or(wgpu::Backends::all()), ..settings.into() }, @@ -292,11 +289,7 @@ impl graphics::Compositor for Compositor { } fn create_renderer(&self) -> Self::Renderer { - Renderer::new( - self.create_backend(), - self.settings.default_font, - self.settings.default_text_size, - ) + Renderer::new(self.settings, &self.engine) } fn create_surface<W: compositor::Window>( @@ -333,7 +326,7 @@ impl graphics::Compositor for Compositor { height, alpha_mode: self.alpha_mode, view_formats: vec![], - desired_maximum_frame_latency: 2, + desired_maximum_frame_latency: 1, }, ); } @@ -355,17 +348,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - renderer.with_primitives(|backend, primitives| { - present( - self, - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) - }) + present(self, renderer, surface, viewport, background_color, overlay) } fn screenshot<T: AsRef<str>>( @@ -376,16 +359,7 @@ impl graphics::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Vec<u8> { - renderer.with_primitives(|backend, primitives| { - screenshot( - self, - backend, - primitives, - viewport, - background_color, - overlay, - ) - }) + screenshot(self, renderer, viewport, background_color, overlay) } } @@ -393,19 +367,12 @@ impl graphics::Compositor for Compositor { /// /// Returns RGBA bytes of the texture data. pub fn screenshot<T: AsRef<str>>( - compositor: &Compositor, - backend: &mut Backend, - primitives: &[Primitive], + compositor: &mut Compositor, + renderer: &mut Renderer, viewport: &Viewport, background_color: Color, overlay: &[T], ) -> Vec<u8> { - let mut encoder = compositor.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu.offscreen.encoder"), - }, - ); - let dimensions = BufferDimensions::new(viewport.physical_size()); let texture_extent = wgpu::Extent3d { @@ -429,14 +396,20 @@ pub fn screenshot<T: AsRef<str>>( let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - backend.present( + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu.offscreen.encoder"), + }, + ); + + renderer.present( + &mut compositor.engine, &compositor.device, &compositor.queue, &mut encoder, Some(background_color), texture.format(), &view, - primitives, viewport, overlay, ); @@ -474,7 +447,7 @@ pub fn screenshot<T: AsRef<str>>( texture_extent, ); - let index = compositor.queue.submit(Some(encoder.finish())); + let index = compositor.engine.submit(&compositor.queue, encoder); let slice = output_buffer.slice(..); slice.map_async(wgpu::MapMode::Read, |_| {}); diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 7a21895a..42f92de0 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -17,7 +17,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget, + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; use crate::graphics::geometry; @@ -222,8 +222,8 @@ where let state = tree.state.downcast_ref::<P::State>(); - renderer.with_transformation( - Transformation::translate(bounds.x, bounds.y), + renderer.with_translation( + Vector::new(bounds.x, bounds.y), |renderer| { let layers = self.program.draw(state, renderer, theme, bounds, cursor); diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 84e9ac15..668c5372 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -651,7 +651,7 @@ where defaults: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State>(); @@ -767,8 +767,8 @@ where renderer.with_layer( Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, + width: (bounds.width + 2.0).min(viewport.width), + height: (bounds.height + 2.0).min(viewport.height), ..bounds }, |renderer| { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index a814df78..e9f07838 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -368,7 +368,7 @@ where let text = value.to_string(); - let (cursor, offset) = if let Some(focus) = state + let (cursor, offset, is_selecting) = if let Some(focus) = state .is_focused .as_ref() .filter(|focus| focus.is_window_focused) @@ -406,7 +406,7 @@ where None }; - (cursor, offset) + (cursor, offset, false) } cursor::State::Selection { start, end } => { let left = start.min(end); @@ -446,11 +446,12 @@ where } else { left_offset }, + true, ) } } } else { - (None, 0.0) + (None, 0.0, false) }; let draw = |renderer: &mut Renderer, viewport| { @@ -482,7 +483,7 @@ where ); }; - if cursor.is_some() { + if is_selecting { renderer .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { |