diff options
Diffstat (limited to 'wgpu/src')
75 files changed, 4010 insertions, 741 deletions
diff --git a/wgpu/src/defaults.rs b/wgpu/src/defaults.rs new file mode 100644 index 00000000..11718a87 --- /dev/null +++ b/wgpu/src/defaults.rs @@ -0,0 +1,32 @@ +//! Use default styling attributes to inherit styles. +use iced_native::Color; + +/// Some default styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Defaults { + /// Text styling + pub text: Text, +} + +impl Default for Defaults { + fn default() -> Defaults { + Defaults { + text: Text::default(), + } + } +} + +/// Some default text styling attributes. +#[derive(Debug, Clone, Copy)] +pub struct Text { + /// The default color of text + pub color: Color, +} + +impl Default for Text { + fn default() -> Text { + Text { + color: Color::BLACK, + } + } +} diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 5dc972ac..d3603676 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -1,11 +1,30 @@ +mod atlas; + +#[cfg(feature = "image")] +mod raster; + +#[cfg(feature = "svg")] +mod vector; + use crate::Transformation; +use atlas::Atlas; + use iced_native::Rectangle; +use std::cell::RefCell; +use std::mem; + +#[cfg(feature = "image")] +use iced_native::image; -use std::{cell::RefCell, collections::HashMap, mem, rc::Rc}; +#[cfg(feature = "svg")] +use iced_native::svg; #[derive(Debug)] pub struct Pipeline { - cache: RefCell<HashMap<String, Memory>>, + #[cfg(feature = "image")] + raster_cache: RefCell<raster::Cache>, + #[cfg(feature = "svg")] + vector_cache: RefCell<vector::Cache>, pipeline: wgpu::RenderPipeline, uniforms: wgpu::Buffer, @@ -13,11 +32,14 @@ pub struct Pipeline { indices: wgpu::Buffer, instances: wgpu::Buffer, constants: wgpu::BindGroup, + texture: wgpu::BindGroup, + texture_version: usize, texture_layout: wgpu::BindGroupLayout, + texture_atlas: Atlas, } impl Pipeline { - pub fn new(device: &wgpu::Device) -> Self { + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -124,7 +146,7 @@ impl Pipeline { }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format, color_blend: wgpu::BlendDescriptor { src_factor: wgpu::BlendFactor::SrcAlpha, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, @@ -163,6 +185,21 @@ impl Pipeline { format: wgpu::VertexFormat::Float2, offset: 4 * 2, }, + wgpu::VertexAttributeDescriptor { + shader_location: 3, + format: wgpu::VertexFormat::Float2, + offset: 4 * 4, + }, + wgpu::VertexAttributeDescriptor { + shader_location: 4, + format: wgpu::VertexFormat::Float2, + offset: 4 * 6, + }, + wgpu::VertexAttributeDescriptor { + shader_location: 5, + format: wgpu::VertexFormat::Uint, + offset: 4 * 8, + }, ], }, ], @@ -180,12 +217,28 @@ impl Pipeline { .fill_from_slice(&QUAD_INDICES); let instances = device.create_buffer(&wgpu::BufferDescriptor { - size: mem::size_of::<Instance>() as u64, + size: mem::size_of::<Instance>() as u64 * Instance::MAX as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, }); + let texture_atlas = Atlas::new(device); + + let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &texture_atlas.view(), + ), + }], + }); + Pipeline { - cache: RefCell::new(HashMap::new()), + #[cfg(feature = "image")] + raster_cache: RefCell::new(raster::Cache::new()), + + #[cfg(feature = "svg")] + vector_cache: RefCell::new(vector::Cache::new()), pipeline, uniforms: uniforms_buffer, @@ -193,39 +246,99 @@ impl Pipeline { indices, instances, constants: constant_bind_group, + texture, + texture_version: texture_atlas.layer_count(), texture_layout, + texture_atlas, } } - pub fn dimensions(&self, path: &str) -> (u32, u32) { - self.load(path); + #[cfg(feature = "image")] + pub fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + let mut cache = self.raster_cache.borrow_mut(); + let memory = cache.load(&handle); - self.cache.borrow().get(path).unwrap().dimensions() + memory.dimensions() } - fn load(&self, path: &str) { - if !self.cache.borrow().contains_key(path) { - let memory = if let Ok(image) = image::open(path) { - Memory::Host { - image: image.to_bgra(), - } - } else { - Memory::NotFound - }; + #[cfg(feature = "svg")] + pub fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + let mut cache = self.vector_cache.borrow_mut(); + let svg = cache.load(&handle); - let _ = self.cache.borrow_mut().insert(path.to_string(), memory); - } + svg.viewport_dimensions() } pub fn draw( &mut self, device: &mut wgpu::Device, encoder: &mut wgpu::CommandEncoder, - instances: &[Image], + images: &[Image], transformation: Transformation, bounds: Rectangle<u32>, target: &wgpu::TextureView, + _scale: f32, ) { + let 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(); + + for image in images { + match &image.handle { + #[cfg(feature = "image")] + Handle::Raster(handle) => { + if let Some(atlas_entry) = raster_cache.upload( + handle, + device, + encoder, + &mut self.texture_atlas, + ) { + add_instances(image, atlas_entry, instances); + } + } + #[cfg(feature = "svg")] + Handle::Vector(handle) => { + if let Some(atlas_entry) = vector_cache.upload( + handle, + image.size, + _scale, + device, + encoder, + &mut self.texture_atlas, + ) { + add_instances(image, atlas_entry, instances); + } + } + } + } + + if instances.is_empty() { + return; + } + + let texture_version = self.texture_atlas.layer_count(); + + if self.texture_version != texture_version { + log::info!("Atlas has grown. Recreating bind group..."); + + self.texture = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.texture_atlas.view(), + ), + }], + }); + + self.texture_version = texture_version; + } + let uniforms_buffer = device .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC) .fill_from_slice(&[Uniforms { @@ -240,193 +353,94 @@ impl Pipeline { std::mem::size_of::<Uniforms>() as u64, ); - // TODO: Batch draw calls using a texture atlas - // Guillotière[1] by @nical can help us a lot here. - // - // [1]: https://github.com/nical/guillotiere - for image in instances { - self.load(&image.path); - - if let Some(texture) = self - .cache - .borrow_mut() - .get_mut(&image.path) - .unwrap() - .upload(device, encoder, &self.texture_layout) - { - let instance_buffer = device - .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC) - .fill_from_slice(&[Instance { - _position: image.position, - _scale: image.scale, - }]); - - encoder.copy_buffer_to_buffer( - &instance_buffer, - 0, - &self.instances, - 0, - mem::size_of::<Instance>() as u64, - ); - - { - let mut render_pass = encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - color_attachments: &[ - wgpu::RenderPassColorAttachmentDescriptor { - attachment: target, - resolve_target: None, - load_op: wgpu::LoadOp::Load, - store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }, - }, - ], - depth_stencil_attachment: None, + let instances_buffer = device + .create_buffer_mapped(instances.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&instances); + + let mut i = 0; + let total = instances.len(); + + while i < total { + let end = (i + Instance::MAX).min(total); + let amount = end - i; + + encoder.copy_buffer_to_buffer( + &instances_buffer, + (i * std::mem::size_of::<Instance>()) as u64, + &self.instances, + 0, + (amount * std::mem::size_of::<Instance>()) as u64, + ); + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + load_op: wgpu::LoadOp::Load, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, }, - ); - - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_bind_group(1, &texture, &[]); - render_pass.set_index_buffer(&self.indices, 0); - render_pass.set_vertex_buffers( - 0, - &[(&self.vertices, 0), (&self.instances, 0)], - ); - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - render_pass.draw_indexed( - 0..QUAD_INDICES.len() as u32, - 0, - 0..1 as u32, - ); - } - } - } - } -} - -#[derive(Debug)] -enum Memory { - Host { - image: image::ImageBuffer<image::Bgra<u8>, Vec<u8>>, - }, - Device { - bind_group: Rc<wgpu::BindGroup>, - width: u32, - height: u32, - }, - NotFound, -} + ], + depth_stencil_attachment: None, + }); -impl Memory { - fn dimensions(&self) -> (u32, u32) { - match self { - Memory::Host { image } => image.dimensions(), - Memory::Device { width, height, .. } => (*width, *height), - Memory::NotFound => (1, 1), + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_index_buffer(&self.indices, 0); + render_pass.set_vertex_buffers( + 0, + &[(&self.vertices, 0), (&self.instances, 0)], + ); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.draw_indexed( + 0..QUAD_INDICES.len() as u32, + 0, + 0..amount as u32, + ); + + i += Instance::MAX; } } - fn upload( - &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - texture_layout: &wgpu::BindGroupLayout, - ) -> Option<Rc<wgpu::BindGroup>> { - match self { - Memory::Host { image } => { - let (width, height) = image.dimensions(); - - let extent = wgpu::Extent3d { - width, - height, - depth: 1, - }; - - let texture = device.create_texture(&wgpu::TextureDescriptor { - size: extent, - array_layer_count: 1, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - usage: wgpu::TextureUsage::COPY_DST - | wgpu::TextureUsage::SAMPLED, - }); - - let slice = image.clone().into_raw(); - - let temp_buf = device - .create_buffer_mapped( - slice.len(), - wgpu::BufferUsage::COPY_SRC, - ) - .fill_from_slice(&slice[..]); - - encoder.copy_buffer_to_texture( - wgpu::BufferCopyView { - buffer: &temp_buf, - offset: 0, - row_pitch: 4 * width as u32, - image_height: height as u32, - }, - wgpu::TextureCopyView { - texture: &texture, - array_layer: 0, - mip_level: 0, - origin: wgpu::Origin3d { - x: 0.0, - y: 0.0, - z: 0.0, - }, - }, - extent, - ); - - let bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: texture_layout, - bindings: &[wgpu::Binding { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &texture.create_default_view(), - ), - }], - }); - - let bind_group = Rc::new(bind_group); - - *self = Memory::Device { - bind_group: bind_group.clone(), - width, - height, - }; + pub fn trim_cache(&mut self) { + #[cfg(feature = "image")] + self.raster_cache.borrow_mut().trim(&mut self.texture_atlas); - Some(bind_group) - } - Memory::Device { bind_group, .. } => Some(bind_group.clone()), - Memory::NotFound => None, - } + #[cfg(feature = "svg")] + self.vector_cache.borrow_mut().trim(&mut self.texture_atlas); } } pub struct Image { - pub path: String, + pub handle: Handle, pub position: [f32; 2], - pub scale: [f32; 2], + pub size: [f32; 2], +} + +pub enum Handle { + #[cfg(feature = "image")] + Raster(image::Handle), + + #[cfg(feature = "svg")] + Vector(svg::Handle), } +#[repr(C)] #[derive(Clone, Copy)] pub struct Vertex { _position: [f32; 2], @@ -449,10 +463,18 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -#[derive(Clone, Copy)] +#[repr(C)] +#[derive(Debug, Clone, Copy)] struct Instance { _position: [f32; 2], - _scale: [f32; 2], + _size: [f32; 2], + _position_in_atlas: [f32; 2], + _size_in_atlas: [f32; 2], + _layer: u32, +} + +impl Instance { + pub const MAX: usize = 1_000; } #[repr(C)] @@ -460,3 +482,67 @@ struct Instance { struct Uniforms { transform: [f32; 16], } + +fn add_instances( + image: &Image, + entry: &atlas::Entry, + instances: &mut Vec<Instance>, +) { + match entry { + atlas::Entry::Contiguous(allocation) => { + add_instance(image.position, image.size, allocation, instances); + } + atlas::Entry::Fragmented { fragments, size } => { + let scaling_x = image.size[0] / size.0 as f32; + let scaling_y = image.size[1] / size.1 as f32; + + for fragment in fragments { + let allocation = &fragment.allocation; + + let [x, y] = image.position; + let (fragment_x, fragment_y) = fragment.position; + let (fragment_width, fragment_height) = allocation.size(); + + let position = [ + x + fragment_x as f32 * scaling_x, + y + fragment_y as f32 * scaling_y, + ]; + + let size = [ + fragment_width as f32 * scaling_x, + fragment_height as f32 * scaling_y, + ]; + + add_instance(position, size, allocation, instances); + } + } + } +} + +#[inline] +fn add_instance( + position: [f32; 2], + size: [f32; 2], + allocation: &atlas::Allocation, + instances: &mut Vec<Instance>, +) { + let (x, y) = allocation.position(); + let (width, height) = allocation.size(); + let layer = allocation.layer(); + + let instance = Instance { + _position: position, + _size: size, + _position_in_atlas: [ + (x as f32 + 0.5) / atlas::SIZE as f32, + (y as f32 + 0.5) / atlas::SIZE as f32, + ], + _size_in_atlas: [ + (width as f32 - 1.0) / atlas::SIZE as f32, + (height as f32 - 1.0) / atlas::SIZE as f32, + ], + _layer: layer as u32, + }; + + instances.push(instance); +} diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs new file mode 100644 index 00000000..86a5ff49 --- /dev/null +++ b/wgpu/src/image/atlas.rs @@ -0,0 +1,361 @@ +pub mod entry; + +mod allocation; +mod allocator; +mod layer; + +pub use allocation::Allocation; +pub use entry::Entry; +pub use layer::Layer; + +use allocator::Allocator; + +pub const SIZE: u32 = 2048; + +#[derive(Debug)] +pub struct Atlas { + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + layers: Vec<Layer>, +} + +impl Atlas { + pub fn new(device: &wgpu::Device) -> Self { + let extent = wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }; + + let texture = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 2, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let texture_view = texture.create_default_view(); + + Atlas { + texture, + texture_view, + layers: vec![Layer::Empty, Layer::Empty], + } + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.texture_view + } + + pub fn layer_count(&self) -> usize { + self.layers.len() + } + + pub fn upload<C>( + &mut self, + width: u32, + height: u32, + data: &[C], + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) -> Option<Entry> + where + C: Copy + 'static, + { + let entry = { + let current_size = self.layers.len(); + let entry = self.allocate(width, height)?; + + // We grow the internal texture after allocating if necessary + let new_layers = self.layers.len() - current_size; + self.grow(new_layers, device, encoder); + + entry + }; + + log::info!("Allocated atlas entry: {:?}", entry); + + let buffer = device + .create_buffer_mapped(data.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(data); + + match &entry { + Entry::Contiguous(allocation) => { + self.upload_allocation( + &buffer, + width, + height, + 0, + &allocation, + encoder, + ); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + let (x, y) = fragment.position; + let offset = (y * width + x) as usize * 4; + + self.upload_allocation( + &buffer, + width, + height, + offset, + &fragment.allocation, + encoder, + ); + } + } + } + + log::info!("Current atlas: {:?}", self); + + Some(entry) + } + + pub fn remove(&mut self, entry: &Entry) { + log::info!("Removing atlas entry: {:?}", entry); + + match entry { + Entry::Contiguous(allocation) => { + self.deallocate(allocation); + } + Entry::Fragmented { fragments, .. } => { + for fragment in fragments { + self.deallocate(&fragment.allocation); + } + } + } + } + + fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> { + // Allocate one layer if texture fits perfectly + if width == SIZE && height == SIZE { + let mut empty_layers = self + .layers + .iter_mut() + .enumerate() + .filter(|(_, layer)| layer.is_empty()); + + if let Some((i, layer)) = empty_layers.next() { + *layer = Layer::Full; + + return Some(Entry::Contiguous(Allocation::Full { layer: i })); + } + + self.layers.push(Layer::Full); + + return Some(Entry::Contiguous(Allocation::Full { + layer: self.layers.len() - 1, + })); + } + + // Split big textures across multiple layers + if width > SIZE || height > SIZE { + let mut fragments = Vec::new(); + let mut y = 0; + + while y < height { + let height = std::cmp::min(height - y, SIZE); + let mut x = 0; + + while x < width { + let width = std::cmp::min(width - x, SIZE); + + let allocation = self.allocate(width, height)?; + + if let Entry::Contiguous(allocation) = allocation { + fragments.push(entry::Fragment { + position: (x, y), + allocation, + }); + } + + x += width; + } + + y += height; + } + + return Some(Entry::Fragmented { + size: (width, height), + fragments, + }); + } + + // Try allocating on an existing layer + for (i, layer) in self.layers.iter_mut().enumerate() { + match layer { + Layer::Empty => { + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + *layer = Layer::Busy(allocator); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + Layer::Busy(allocator) => { + if let Some(region) = allocator.allocate(width, height) { + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: i, + })); + } + } + _ => {} + } + } + + // Create new layer with atlas allocator + let mut allocator = Allocator::new(SIZE); + + if let Some(region) = allocator.allocate(width, height) { + self.layers.push(Layer::Busy(allocator)); + + return Some(Entry::Contiguous(Allocation::Partial { + region, + layer: self.layers.len() - 1, + })); + } + + // We ran out of memory (?) + None + } + + fn deallocate(&mut self, allocation: &Allocation) { + log::info!("Deallocating atlas: {:?}", allocation); + + match allocation { + Allocation::Full { layer } => { + self.layers[*layer] = Layer::Empty; + } + Allocation::Partial { layer, region } => { + let layer = &mut self.layers[*layer]; + + if let Layer::Busy(allocator) = layer { + allocator.deallocate(region); + + if allocator.is_empty() { + *layer = Layer::Empty; + } + } + } + } + } + + fn upload_allocation( + &mut self, + buffer: &wgpu::Buffer, + image_width: u32, + image_height: u32, + offset: usize, + allocation: &Allocation, + encoder: &mut wgpu::CommandEncoder, + ) { + let (x, y) = allocation.position(); + let (width, height) = allocation.size(); + let layer = allocation.layer(); + + let extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + + encoder.copy_buffer_to_texture( + wgpu::BufferCopyView { + buffer, + offset: offset as u64, + row_pitch: 4 * image_width, + image_height, + }, + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: layer as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: x as f32, + y: y as f32, + z: 0.0, + }, + }, + extent, + ); + } + + fn grow( + &mut self, + amount: usize, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + ) { + if amount == 0 { + return; + } + + let new_texture = device.create_texture(&wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + array_layer_count: self.layers.len() as u32, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::COPY_DST + | wgpu::TextureUsage::COPY_SRC + | wgpu::TextureUsage::SAMPLED, + }); + + let amount_to_copy = self.layers.len() - amount; + + for (i, layer) in + self.layers.iter_mut().take(amount_to_copy).enumerate() + { + if layer.is_empty() { + continue; + } + + encoder.copy_texture_to_texture( + wgpu::TextureCopyView { + texture: &self.texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::TextureCopyView { + texture: &new_texture, + array_layer: i as u32, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::Extent3d { + width: SIZE, + height: SIZE, + depth: 1, + }, + ); + } + + self.texture = new_texture; + self.texture_view = self.texture.create_default_view(); + } +} diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs new file mode 100644 index 00000000..59b7239f --- /dev/null +++ b/wgpu/src/image/atlas/allocation.rs @@ -0,0 +1,35 @@ +use crate::image::atlas::{self, allocator}; + +#[derive(Debug)] +pub enum Allocation { + Partial { + layer: usize, + region: allocator::Region, + }, + Full { + layer: usize, + }, +} + +impl Allocation { + pub fn position(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.position(), + Allocation::Full { .. } => (0, 0), + } + } + + pub fn size(&self) -> (u32, u32) { + match self { + Allocation::Partial { region, .. } => region.size(), + Allocation::Full { .. } => (atlas::SIZE, atlas::SIZE), + } + } + + pub fn layer(&self) -> usize { + match self { + Allocation::Partial { layer, .. } => *layer, + Allocation::Full { layer } => *layer, + } + } +} diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs new file mode 100644 index 00000000..7a4ff5b1 --- /dev/null +++ b/wgpu/src/image/atlas/allocator.rs @@ -0,0 +1,69 @@ +use guillotiere::{AtlasAllocator, Size}; + +pub struct Allocator { + raw: AtlasAllocator, + allocations: usize, +} + +impl Allocator { + pub fn new(size: u32) -> Allocator { + let raw = AtlasAllocator::new(Size::new(size as i32, size as i32)); + + Allocator { + raw, + allocations: 0, + } + } + + pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> { + let allocation = + self.raw.allocate(Size::new(width as i32, height as i32))?; + + self.allocations += 1; + + Some(Region { allocation }) + } + + pub fn deallocate(&mut self, region: &Region) { + self.raw.deallocate(region.allocation.id); + + self.allocations = self.allocations.saturating_sub(1); + } + + pub fn is_empty(&self) -> bool { + self.allocations == 0 + } +} + +pub struct Region { + allocation: guillotiere::Allocation, +} + +impl Region { + pub fn position(&self) -> (u32, u32) { + let rectangle = &self.allocation.rectangle; + + (rectangle.min.x as u32, rectangle.min.y as u32) + } + + pub fn size(&self) -> (u32, u32) { + let size = self.allocation.rectangle.size(); + + (size.width as u32, size.height as u32) + } +} + +impl std::fmt::Debug for Allocator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Allocator") + } +} + +impl std::fmt::Debug for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Region") + .field("id", &self.allocation.id) + .field("rectangle", &self.allocation.rectangle) + .finish() + } +} diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs new file mode 100644 index 00000000..0310fc54 --- /dev/null +++ b/wgpu/src/image/atlas/entry.rs @@ -0,0 +1,26 @@ +use crate::image::atlas; + +#[derive(Debug)] +pub enum Entry { + Contiguous(atlas::Allocation), + Fragmented { + size: (u32, u32), + fragments: Vec<Fragment>, + }, +} + +impl Entry { + #[cfg(feature = "image")] + pub fn size(&self) -> (u32, u32) { + match self { + Entry::Contiguous(allocation) => allocation.size(), + Entry::Fragmented { size, .. } => *size, + } + } +} + +#[derive(Debug)] +pub struct Fragment { + pub position: (u32, u32), + pub allocation: atlas::Allocation, +} diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs new file mode 100644 index 00000000..b1084ed9 --- /dev/null +++ b/wgpu/src/image/atlas/layer.rs @@ -0,0 +1,17 @@ +use crate::image::atlas::Allocator; + +#[derive(Debug)] +pub enum Layer { + Empty, + Busy(Allocator), + Full, +} + +impl Layer { + pub fn is_empty(&self) -> bool { + match self { + Layer::Empty => true, + _ => false, + } + } +} diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs new file mode 100644 index 00000000..4f69df8c --- /dev/null +++ b/wgpu/src/image/raster.rs @@ -0,0 +1,134 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::image; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug)] +pub enum Memory { + Host(::image::ImageBuffer<::image::Bgra<u8>, Vec<u8>>), + Device(atlas::Entry), + NotFound, + Invalid, +} + +impl Memory { + pub fn dimensions(&self) -> (u32, u32) { + match self { + Memory::Host(image) => image.dimensions(), + Memory::Device(entry) => entry.size(), + Memory::NotFound => (1, 1), + Memory::Invalid => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + map: HashMap<u64, Memory>, + hits: HashSet<u64>, +} + +impl Cache { + pub fn new() -> Self { + Self { + map: HashMap::new(), + hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { + if self.contains(handle) { + return self.get(handle).unwrap(); + } + + let memory = match handle.data() { + image::Data::Path(path) => { + if let Ok(image) = ::image::open(path) { + Memory::Host(image.to_bgra()) + } else { + Memory::NotFound + } + } + image::Data::Bytes(bytes) => { + if let Ok(image) = ::image::load_from_memory(&bytes) { + Memory::Host(image.to_bgra()) + } else { + Memory::Invalid + } + } + image::Data::Pixels { + width, + height, + pixels, + } => { + if let Some(image) = ::image::ImageBuffer::from_vec( + *width, + *height, + pixels.to_vec(), + ) { + Memory::Host(image) + } else { + Memory::Invalid + } + } + }; + + self.insert(handle, memory); + self.get(handle).unwrap() + } + + pub fn upload( + &mut self, + handle: &image::Handle, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let memory = self.load(handle); + + if let Memory::Host(image) = memory { + let (width, height) = image.dimensions(); + + let entry = atlas.upload(width, height, &image, device, encoder)?; + + *memory = Memory::Device(entry); + } + + if let Memory::Device(allocation) = memory { + Some(allocation) + } else { + None + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let hits = &self.hits; + + self.map.retain(|k, memory| { + let retain = hits.contains(k); + + if !retain { + if let Memory::Device(entry) = memory { + atlas.remove(entry); + } + } + + retain + }); + + self.hits.clear(); + } + + fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); + + self.map.get_mut(&handle.id()) + } + + fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + } + + fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } +} diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs new file mode 100644 index 00000000..bae0f82f --- /dev/null +++ b/wgpu/src/image/vector.rs @@ -0,0 +1,151 @@ +use crate::image::atlas::{self, Atlas}; +use iced_native::svg; +use std::collections::{HashMap, HashSet}; + +pub enum Svg { + Loaded(resvg::usvg::Tree), + NotFound, +} + +impl Svg { + pub fn viewport_dimensions(&self) -> (u32, u32) { + match self { + Svg::Loaded(tree) => { + let size = tree.svg_node().size; + + (size.width() as u32, size.height() as u32) + } + Svg::NotFound => (1, 1), + } + } +} + +#[derive(Debug)] +pub struct Cache { + svgs: HashMap<u64, Svg>, + rasterized: HashMap<(u64, u32, u32), atlas::Entry>, + svg_hits: HashSet<u64>, + rasterized_hits: HashSet<(u64, u32, u32)>, +} + +impl Cache { + pub fn new() -> Self { + Self { + svgs: HashMap::new(), + rasterized: HashMap::new(), + svg_hits: HashSet::new(), + rasterized_hits: HashSet::new(), + } + } + + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + let opt = resvg::Options::default(); + + let svg = match resvg::usvg::Tree::from_file(handle.path(), &opt.usvg) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + }; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + pub fn upload( + &mut self, + handle: &svg::Handle, + [width, height]: [f32; 2], + scale: f32, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + texture_atlas: &mut Atlas, + ) -> Option<&atlas::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * width).round() as u32, + (scale * height).round() as u32, + ); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&(id, width, height)) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + + return self.rasterized.get(&(id, width, height)); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let screen_size = + resvg::ScreenSize::new(width, height).unwrap(); + + let mut canvas = + resvg::raqote::DrawTarget::new(width as i32, height as i32); + + resvg::backend_raqote::render_to_canvas( + tree, + &resvg::Options::default(), + screen_size, + &mut canvas, + ); + + let allocation = texture_atlas.upload( + width, + height, + canvas.get_data(), + device, + encoder, + )?; + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized.insert((id, width, height), allocation); + + self.rasterized.get(&(id, width, height)) + } + Svg::NotFound => None, + } + } + + pub fn trim(&mut self, atlas: &mut Atlas) { + let svg_hits = &self.svg_hits; + let rasterized_hits = &self.rasterized_hits; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + atlas.remove(entry); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9f9ed8db..4e0cbc60 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -22,18 +22,36 @@ #![deny(missing_docs)] #![deny(missing_debug_implementations)] #![deny(unused_results)] -#![deny(unsafe_code)] -#![deny(rust_2018_idioms)] -mod image; +#![forbid(unsafe_code)] +#![forbid(rust_2018_idioms)] +pub mod defaults; +pub mod settings; +pub mod triangle; +pub mod widget; +pub mod window; + mod primitive; mod quad; mod renderer; +mod target; mod text; mod transformation; +mod viewport; + +pub use wgpu; + +pub use defaults::Defaults; +pub use primitive::Primitive; +pub use renderer::Renderer; +pub use settings::Settings; +pub use target::Target; +pub use viewport::Viewport; + +#[doc(no_inline)] +pub use widget::*; -pub(crate) use crate::image::Image; pub(crate) use quad::Quad; pub(crate) use transformation::Transformation; -pub use primitive::Primitive; -pub use renderer::{Renderer, Target}; +#[cfg(any(feature = "image", feature = "svg"))] +mod image; diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 9efd74f6..823b4b72 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,8 +1,11 @@ use iced_native::{ - Background, Color, Font, HorizontalAlignment, Rectangle, Vector, - VerticalAlignment, + image, svg, Background, Color, Font, HorizontalAlignment, Point, Rectangle, + Vector, VerticalAlignment, }; +use crate::triangle; +use std::sync::Arc; + /// A rendering primitive. #[derive(Debug, Clone)] pub enum Primitive { @@ -38,14 +41,26 @@ pub enum Primitive { background: Background, /// The border radius of the quad border_radius: u16, + /// The border width of the quad + border_width: u16, + /// The border color of the quad + border_color: Color, }, /// An image primitive Image { - /// The path of the image - path: String, + /// The handle of the image + handle: image::Handle, /// The bounds of the image bounds: Rectangle, }, + /// An SVG primitive + Svg { + /// The path of the SVG file + handle: svg::Handle, + + /// The bounds of the viewport + bounds: Rectangle, + }, /// A clip primitive Clip { /// The bounds of the clip @@ -55,4 +70,20 @@ pub enum Primitive { /// The content of the clip content: Box<Primitive>, }, + /// A low-level primitive to render a mesh of triangles. + /// + /// It can be used to render many kinds of geometry freely. + Mesh2D { + /// The top-left coordinate of the mesh + origin: Point, + + /// The vertex and index buffers of the mesh + buffers: Arc<triangle::Mesh2D>, + }, +} + +impl Default for Primitive { + fn default() -> Primitive { + Primitive::None + } } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 3d797758..9047080d 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -14,7 +14,10 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &mut wgpu::Device) -> Pipeline { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + ) -> Pipeline { let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { bindings: &[wgpu::BindGroupLayoutBinding { @@ -79,7 +82,7 @@ impl Pipeline { }), primitive_topology: wgpu::PrimitiveTopology::TriangleList, color_states: &[wgpu::ColorStateDescriptor { - format: wgpu::TextureFormat::Bgra8UnormSrgb, + format, color_blend: wgpu::BlendDescriptor { src_factor: wgpu::BlendFactor::SrcAlpha, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, @@ -125,9 +128,19 @@ impl Pipeline { }, wgpu::VertexAttributeDescriptor { shader_location: 4, - format: wgpu::VertexFormat::Float, + format: wgpu::VertexFormat::Float4, offset: 4 * (2 + 2 + 4), }, + wgpu::VertexAttributeDescriptor { + shader_location: 5, + format: wgpu::VertexFormat::Float, + offset: 4 * (2 + 2 + 4 + 4), + }, + wgpu::VertexAttributeDescriptor { + shader_location: 6, + format: wgpu::VertexFormat::Float, + offset: 4 * (2 + 2 + 4 + 4 + 1), + }, ], }, ], @@ -233,7 +246,8 @@ impl Pipeline { bounds.x, bounds.y, bounds.width, - bounds.height, + // TODO: Address anti-aliasing adjustments properly + bounds.height + 1, ); render_pass.draw_indexed( @@ -248,6 +262,7 @@ impl Pipeline { } } +#[repr(C)] #[derive(Clone, Copy)] pub struct Vertex { _position: [f32; 2], @@ -270,12 +285,15 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; +#[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Quad { pub position: [f32; 2], pub scale: [f32; 2], pub color: [f32; 4], + pub border_color: [f32; 4], pub border_radius: f32, + pub border_width: f32, } impl Quad { diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs index f27a4b8a..1da19b1a 100644 --- a/wgpu/src/renderer.rs +++ b/wgpu/src/renderer.rs @@ -1,37 +1,41 @@ -use crate::{quad, text, Image, Primitive, Quad, Transformation}; -use iced_native::{ - renderer::{Debugger, Windowed}, - Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, Widget, +use crate::{ + quad, text, triangle, Defaults, Primitive, Quad, Settings, Target, + Transformation, }; -use wgpu::{ - Adapter, BackendBit, CommandEncoderDescriptor, Device, DeviceDescriptor, - Extensions, Limits, PowerPreference, Queue, RequestAdapterOptions, +#[cfg(any(feature = "image", feature = "svg"))] +use crate::image::{self, Image}; + +use iced_native::{ + layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, + Widget, }; +use std::sync::Arc; -mod target; mod widget; -pub use target::Target; - /// A [`wgpu`] renderer. /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs #[derive(Debug)] pub struct Renderer { - device: Device, - queue: Queue, quad_pipeline: quad::Pipeline, - image_pipeline: crate::image::Pipeline, text_pipeline: text::Pipeline, + triangle_pipeline: triangle::Pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline: image::Pipeline, } struct Layer<'a> { bounds: Rectangle<u32>, offset: Vector<u32>, quads: Vec<Quad>, - images: Vec<Image>, + meshes: Vec<(Point, Arc<triangle::Mesh2D>)>, text: Vec<wgpu_glyph::Section<'a>>, + + #[cfg(any(feature = "image", feature = "svg"))] + images: Vec<Image>, } impl<'a> Layer<'a> { @@ -40,72 +44,62 @@ impl<'a> Layer<'a> { bounds, offset, quads: Vec::new(), - images: Vec::new(), text: Vec::new(), + meshes: Vec::new(), + + #[cfg(any(feature = "image", feature = "svg"))] + images: Vec::new(), } } } impl Renderer { - fn new() -> Self { - let adapter = Adapter::request(&RequestAdapterOptions { - power_preference: PowerPreference::LowPower, - backends: BackendBit::all(), - }) - .expect("Request adapter"); - - let (mut device, queue) = adapter.request_device(&DeviceDescriptor { - extensions: Extensions { - anisotropic_filtering: false, - }, - limits: Limits { max_bind_groups: 2 }, - }); + /// Creates a new [`Renderer`]. + /// + /// [`Renderer`]: struct.Renderer.html + pub fn new(device: &mut wgpu::Device, settings: Settings) -> Self { + let text_pipeline = + text::Pipeline::new(device, settings.format, settings.default_font); + let quad_pipeline = quad::Pipeline::new(device, settings.format); + let triangle_pipeline = triangle::Pipeline::new( + device, + settings.format, + settings.antialiasing, + ); - let text_pipeline = text::Pipeline::new(&mut device); - let quad_pipeline = quad::Pipeline::new(&mut device); - let image_pipeline = crate::image::Pipeline::new(&mut device); + #[cfg(any(feature = "image", feature = "svg"))] + let image_pipeline = image::Pipeline::new(device, settings.format); Self { - device, - queue, quad_pipeline, - image_pipeline, text_pipeline, + triangle_pipeline, + + #[cfg(any(feature = "image", feature = "svg"))] + image_pipeline, } } - fn draw<T: AsRef<str>>( + /// Draws the provided primitives in the given [`Target`]. + /// + /// The text provided as overlay will be renderer on top of the primitives. + /// This is useful for rendering debug information. + /// + /// [`Target`]: struct.Target.html + pub fn draw<T: AsRef<str>>( &mut self, + device: &mut wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: Target<'_>, (primitive, mouse_cursor): &(Primitive, MouseCursor), + scale_factor: f64, overlay: &[T], - target: &mut Target, ) -> MouseCursor { log::debug!("Drawing"); - let (width, height) = target.dimensions(); - let dpi = target.dpi(); - let transformation = target.transformation(); - let frame = target.next_frame(); - - let mut encoder = self - .device - .create_command_encoder(&CommandEncoderDescriptor { todo: 0 }); - - let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &frame.view, - resolve_target: None, - load_op: wgpu::LoadOp::Clear, - store_op: wgpu::StoreOp::Store, - clear_color: wgpu::Color { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }, - }], - depth_stencil_attachment: None, - }); + let (width, height) = target.viewport.dimensions(); + let scale_factor = scale_factor as f32; + let transformation = target.viewport.transformation(); let mut layers = Vec::new(); @@ -123,10 +117,20 @@ impl Renderer { self.draw_overlay(overlay, &mut layers); for layer in layers { - self.flush(dpi, transformation, &layer, &mut encoder, &frame.view); + self.flush( + device, + scale_factor, + transformation, + &layer, + encoder, + target.texture, + width, + height, + ); } - self.queue.submit(&[encoder.finish()]); + #[cfg(any(feature = "image", feature = "svg"))] + self.image_pipeline.trim_cache(); *mouse_cursor } @@ -215,6 +219,8 @@ impl Renderer { bounds, background, border_radius, + border_width, + border_color, } => { // TODO: Move some of this computations to the GPU (?) layer.quads.push(Quad { @@ -227,39 +233,30 @@ impl Renderer { Background::Color(color) => color.into_linear(), }, border_radius: *border_radius as f32, + border_width: *border_width as f32, + border_color: border_color.into_linear(), }); } - Primitive::Image { path, bounds } => { - layer.images.push(Image { - path: path.clone(), - position: [bounds.x, bounds.y], - scale: [bounds.width, bounds.height], - }); + Primitive::Mesh2D { origin, buffers } => { + layer.meshes.push((*origin, buffers.clone())); } Primitive::Clip { bounds, offset, content, } => { - let x = bounds.x - layer.offset.x as f32; - let y = bounds.y - layer.offset.y as f32; - let width = (bounds.width + x).min(bounds.width); - let height = (bounds.height + y).min(bounds.height); - - // Only draw visible content on-screen - // TODO: Also, check for parent layer bounds to avoid further - // drawing in some circumstances. - if width > 0.0 && height > 0.0 { - let clip_layer = Layer::new( - Rectangle { - x: x.max(0.0).floor() as u32, - y: y.max(0.0).floor() as u32, - width: width.ceil() as u32, - height: height.ceil() as u32, - }, - layer.offset + *offset, - ); + let layer_bounds: Rectangle<f32> = layer.bounds.into(); + let clip = Rectangle { + x: bounds.x - layer.offset.x as f32, + y: bounds.y - layer.offset.y as f32, + ..*bounds + }; + + // Only draw visible content + if let Some(clip_bounds) = layer_bounds.intersection(&clip) { + let clip_layer = + Layer::new(clip_bounds.into(), layer.offset + *offset); let new_layer = Layer::new(layer.bounds, layer.offset); layers.push(clip_layer); @@ -267,6 +264,28 @@ impl Renderer { layers.push(new_layer); } } + + #[cfg(feature = "image")] + Primitive::Image { handle, bounds } => { + layer.images.push(Image { + handle: image::Handle::Raster(handle.clone()), + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "image"))] + Primitive::Image { .. } => {} + + #[cfg(feature = "svg")] + Primitive::Svg { handle, bounds } => { + layer.images.push(Image { + handle: image::Handle::Vector(handle.clone()), + position: [bounds.x, bounds.y], + size: [bounds.width, bounds.height], + }); + } + #[cfg(not(feature = "svg"))] + Primitive::Svg { .. } => {} } } @@ -306,42 +325,69 @@ impl Renderer { fn flush( &mut self, - dpi: f32, + device: &mut wgpu::Device, + scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, + target_width: u32, + target_height: u32, ) { - let bounds = layer.bounds * dpi; + let bounds = layer.bounds * scale_factor; + + if layer.meshes.len() > 0 { + let translated = transformation + * Transformation::scale(scale_factor, scale_factor) + * Transformation::translate( + -(layer.offset.x as f32), + -(layer.offset.y as f32), + ); + + self.triangle_pipeline.draw( + device, + encoder, + target, + target_width, + target_height, + translated, + &layer.meshes, + bounds, + ); + } if layer.quads.len() > 0 { self.quad_pipeline.draw( - &mut self.device, + device, encoder, &layer.quads, transformation, - dpi, + scale_factor, bounds, target, ); } - if layer.images.len() > 0 { - let translated_and_scaled = transformation - * Transformation::scale(dpi, dpi) - * Transformation::translate( - -(layer.offset.x as f32), - -(layer.offset.y as f32), - ); + #[cfg(any(feature = "image", feature = "svg"))] + { + if layer.images.len() > 0 { + let translated_and_scaled = transformation + * Transformation::scale(scale_factor, scale_factor) + * Transformation::translate( + -(layer.offset.x as f32), + -(layer.offset.y as f32), + ); - self.image_pipeline.draw( - &mut self.device, - encoder, - &layer.images, - translated_and_scaled, - bounds, - target, - ); + self.image_pipeline.draw( + device, + encoder, + &layer.images, + translated_and_scaled, + bounds, + target, + scale_factor, + ); + } } if layer.text.len() > 0 { @@ -353,25 +399,25 @@ impl Renderer { // bit "jumpy". We may be able to do better once we improve // our text rendering/caching pipeline. screen_position: ( - (text.screen_position.0 * dpi).round(), - (text.screen_position.1 * dpi).round(), + (text.screen_position.0 * scale_factor).round(), + (text.screen_position.1 * scale_factor).round(), ), - // TODO: Fix precision issues with some DPI factors. + // TODO: Fix precision issues with some scale factors. // // The `ceil` here can cause some words to render on the // same line when they should not. // // Ideally, `wgpu_glyph` should be able to compute layout // using logical positions, and then apply the proper - // DPI scaling. This would ensure that both measuring and - // rendering follow the same layout rules. + // scaling when rendering. This would ensure that both + // measuring and rendering follow the same layout rules. bounds: ( - (text.bounds.0 * dpi).ceil(), - (text.bounds.1 * dpi).ceil(), + (text.bounds.0 * scale_factor).ceil(), + (text.bounds.1 * scale_factor).ceil(), ), scale: wgpu_glyph::Scale { - x: text.scale.x * dpi, - y: text.scale.y * dpi, + x: text.scale.x * scale_factor, + y: text.scale.y * scale_factor, }, ..*text }; @@ -380,7 +426,7 @@ impl Renderer { } self.text_pipeline.draw_queued( - &mut self.device, + device, encoder, target, transformation, @@ -397,12 +443,14 @@ impl Renderer { impl iced_native::Renderer for Renderer { type Output = (Primitive, MouseCursor); + type Defaults = Defaults; fn layout<'a, Message>( &mut self, element: &iced_native::Element<'a, Message, Self>, + limits: &iced_native::layout::Limits, ) -> iced_native::layout::Node { - let node = element.layout(self, &iced_native::layout::Limits::NONE); + let node = element.layout(self, limits); self.text_pipeline.clear_measurement_cache(); @@ -410,33 +458,18 @@ impl iced_native::Renderer for Renderer { } } -impl Windowed for Renderer { - type Target = Target; - - fn new() -> Self { - Self::new() - } - - fn draw<T: AsRef<str>>( - &mut self, - output: &Self::Output, - overlay: &[T], - target: &mut Target, - ) -> MouseCursor { - self.draw(output, overlay, target) - } -} - -impl Debugger for Renderer { +impl layout::Debugger for Renderer { fn explain<Message>( &mut self, + defaults: &Defaults, widget: &dyn Widget<Message, Self>, layout: Layout<'_>, cursor_position: Point, color: Color, ) -> Self::Output { let mut primitives = Vec::new(); - let (primitive, cursor) = widget.draw(self, layout, cursor_position); + let (primitive, cursor) = + widget.draw(self, defaults, layout, cursor_position); explain_layout(layout, color, &mut primitives); primitives.push(primitive); @@ -450,11 +483,12 @@ fn explain_layout( color: Color, primitives: &mut Vec<Primitive>, ) { - // TODO: Draw borders instead primitives.push(Primitive::Quad { bounds: layout.bounds(), - background: Background::Color([0.0, 0.0, 0.0, 0.05].into()), + background: Background::Color(Color::TRANSPARENT), border_radius: 0, + border_width: 1, + border_color: [0.6, 0.6, 0.6, 0.5].into(), }); for child in layout.children() { diff --git a/wgpu/src/renderer/target.rs b/wgpu/src/renderer/target.rs deleted file mode 100644 index 569f3c62..00000000 --- a/wgpu/src/renderer/target.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{Renderer, Transformation}; - -use raw_window_handle::HasRawWindowHandle; - -/// A rendering target. -#[derive(Debug)] -pub struct Target { - surface: wgpu::Surface, - width: u16, - height: u16, - dpi: f32, - transformation: Transformation, - swap_chain: wgpu::SwapChain, -} - -impl Target { - pub(crate) fn dimensions(&self) -> (u16, u16) { - (self.width, self.height) - } - - pub(crate) fn dpi(&self) -> f32 { - self.dpi - } - - pub(crate) fn transformation(&self) -> Transformation { - self.transformation - } - - pub(crate) fn next_frame(&mut self) -> wgpu::SwapChainOutput<'_> { - self.swap_chain.get_next_texture() - } -} - -impl iced_native::renderer::Target for Target { - type Renderer = Renderer; - - fn new<W: HasRawWindowHandle>( - window: &W, - width: u16, - height: u16, - dpi: f32, - renderer: &Renderer, - ) -> Target { - let surface = wgpu::Surface::create(window); - let swap_chain = - new_swap_chain(&surface, width, height, &renderer.device); - - Target { - surface, - width, - height, - dpi, - transformation: Transformation::orthographic(width, height), - swap_chain, - } - } - - fn resize( - &mut self, - width: u16, - height: u16, - dpi: f32, - renderer: &Renderer, - ) { - self.width = width; - self.height = height; - self.dpi = dpi; - self.transformation = Transformation::orthographic(width, height); - self.swap_chain = - new_swap_chain(&self.surface, width, height, &renderer.device); - } -} - -fn new_swap_chain( - surface: &wgpu::Surface, - width: u16, - height: u16, - device: &wgpu::Device, -) -> wgpu::SwapChain { - device.create_swap_chain( - &surface, - &wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: u32::from(width), - height: u32::from(height), - present_mode: wgpu::PresentMode::Vsync, - }, - ) -} diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs index 52410bee..84f908e7 100644 --- a/wgpu/src/renderer/widget.rs +++ b/wgpu/src/renderer/widget.rs @@ -1,10 +1,18 @@ mod button; mod checkbox; mod column; -mod image; +mod container; +mod progress_bar; mod radio; mod row; mod scrollable; mod slider; +mod space; mod text; mod text_input; + +#[cfg(feature = "svg")] +mod svg; + +#[cfg(feature = "image")] +mod image; diff --git a/wgpu/src/renderer/widget/button.rs b/wgpu/src/renderer/widget/button.rs index 86963053..0de5bf5c 100644 --- a/wgpu/src/renderer/widget/button.rs +++ b/wgpu/src/renderer/widget/button.rs @@ -1,54 +1,88 @@ -use crate::{Primitive, Renderer}; -use iced_native::{button, Background, MouseCursor, Point, Rectangle}; +use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer}; +use iced_native::{ + Background, Color, Element, Layout, MouseCursor, Point, Rectangle, Vector, +}; -impl button::Renderer for Renderer { - fn draw( +impl iced_native::button::Renderer for Renderer { + const DEFAULT_PADDING: u16 = 5; + + type Style = Box<dyn StyleSheet>; + + fn draw<Message>( &mut self, + defaults: &Defaults, bounds: Rectangle, cursor_position: Point, + is_disabled: bool, is_pressed: bool, - background: Option<Background>, - border_radius: u16, - (content, _): Self::Output, + style: &Box<dyn StyleSheet>, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); - // TODO: Render proper shadows - // TODO: Make hovering and pressed styles configurable - let shadow_offset = if is_mouse_over { + let styling = if is_disabled { + style.disabled() + } else if is_mouse_over { if is_pressed { - 0.0 + style.pressed() } else { - 2.0 + style.hovered() } } else { - 1.0 + style.active() }; + let (content, _) = content.draw( + self, + &Defaults { + text: defaults::Text { + color: styling.text_color, + }, + ..*defaults + }, + content_layout, + cursor_position, + ); + ( - match background { - None => content, - Some(background) => Primitive::Group { - primitives: vec![ - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + shadow_offset, - ..bounds - }, - background: Background::Color( - [0.0, 0.0, 0.0, 0.5].into(), - ), - border_radius, - }, - Primitive::Quad { - bounds, - background, - border_radius, + if styling.background.is_some() || styling.border_width > 0 { + let background = Primitive::Quad { + bounds, + background: styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }; + + if styling.shadow_offset == Vector::default() { + Primitive::Group { + primitives: vec![background, content], + } + } else { + // TODO: Implement proper shadow support + let shadow = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds }, - content, - ], - }, + background: Background::Color( + [0.0, 0.0, 0.0, 0.5].into(), + ), + border_radius: styling.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![shadow, background, content], + } + } + } else { + content }, if is_mouse_over { MouseCursor::Pointer diff --git a/wgpu/src/renderer/widget/checkbox.rs b/wgpu/src/renderer/widget/checkbox.rs index 54b4b1cc..1a0585d3 100644 --- a/wgpu/src/renderer/widget/checkbox.rs +++ b/wgpu/src/renderer/widget/checkbox.rs @@ -1,15 +1,13 @@ -use crate::{Primitive, Renderer}; +use crate::{checkbox::StyleSheet, Primitive, Renderer}; use iced_native::{ - checkbox, Background, HorizontalAlignment, MouseCursor, Rectangle, - VerticalAlignment, + checkbox, HorizontalAlignment, MouseCursor, Rectangle, VerticalAlignment, }; -const SIZE: f32 = 28.0; - impl checkbox::Renderer for Renderer { - fn default_size(&self) -> u32 { - SIZE as u32 - } + type Style = Box<dyn StyleSheet>; + + const DEFAULT_SIZE: u16 = 20; + const DEFAULT_SPACING: u16 = 15; fn draw( &mut self, @@ -17,31 +15,21 @@ impl checkbox::Renderer for Renderer { is_checked: bool, is_mouse_over: bool, (label, _): Self::Output, + style_sheet: &Self::Style, ) -> Self::Output { - let (checkbox_border, checkbox_box) = ( - Primitive::Quad { - bounds, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: 6, - }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color( - if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: 5, - }, - ); + let style = if is_mouse_over { + style_sheet.hovered(is_checked) + } else { + style_sheet.active(is_checked) + }; + + let checkbox = Primitive::Quad { + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; ( Primitive::Group { @@ -51,14 +39,14 @@ impl checkbox::Renderer for Renderer { font: crate::text::BUILTIN_ICONS, size: bounds.height * 0.7, bounds: bounds, - color: [0.3, 0.3, 0.3].into(), + color: style.checkmark_color, horizontal_alignment: HorizontalAlignment::Center, vertical_alignment: VerticalAlignment::Center, }; - vec![checkbox_border, checkbox_box, check, label] + vec![checkbox, check, label] } else { - vec![checkbox_border, checkbox_box, label] + vec![checkbox, label] }, }, if is_mouse_over { diff --git a/wgpu/src/renderer/widget/column.rs b/wgpu/src/renderer/widget/column.rs index 6c31af90..95a7463a 100644 --- a/wgpu/src/renderer/widget/column.rs +++ b/wgpu/src/renderer/widget/column.rs @@ -4,6 +4,7 @@ use iced_native::{column, Element, Layout, MouseCursor, Point}; impl column::Renderer for Renderer { fn draw<Message>( &mut self, + defaults: &Self::Defaults, content: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, @@ -17,7 +18,7 @@ impl column::Renderer for Renderer { .zip(layout.children()) .map(|(child, layout)| { let (primitive, new_mouse_cursor) = - child.draw(self, layout, cursor_position); + child.draw(self, defaults, layout, cursor_position); if new_mouse_cursor > mouse_cursor { mouse_cursor = new_mouse_cursor; diff --git a/wgpu/src/renderer/widget/container.rs b/wgpu/src/renderer/widget/container.rs new file mode 100644 index 00000000..2d4d1db8 --- /dev/null +++ b/wgpu/src/renderer/widget/container.rs @@ -0,0 +1,49 @@ +use crate::{container, defaults, Defaults, Primitive, Renderer}; +use iced_native::{Background, Color, Element, Layout, Point, Rectangle}; + +impl iced_native::container::Renderer for Renderer { + type Style = Box<dyn container::StyleSheet>; + + fn draw<Message>( + &mut self, + defaults: &Defaults, + bounds: Rectangle, + cursor_position: Point, + style_sheet: &Self::Style, + content: &Element<'_, Message, Self>, + content_layout: Layout<'_>, + ) -> Self::Output { + let style = style_sheet.style(); + + let defaults = Defaults { + text: defaults::Text { + color: style.text_color.unwrap_or(defaults.text.color), + }, + ..*defaults + }; + + let (content, mouse_cursor) = + content.draw(self, &defaults, content_layout, cursor_position); + + if style.background.is_some() || style.border_width > 0 { + let quad = Primitive::Quad { + bounds, + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }; + + ( + Primitive::Group { + primitives: vec![quad, content], + }, + mouse_cursor, + ) + } else { + (content, mouse_cursor) + } + } +} diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs index 0006dde1..70dc5d97 100644 --- a/wgpu/src/renderer/widget/image.rs +++ b/wgpu/src/renderer/widget/image.rs @@ -2,14 +2,18 @@ use crate::{Primitive, Renderer}; use iced_native::{image, Layout, MouseCursor}; impl image::Renderer for Renderer { - fn dimensions(&self, path: &str) -> (u32, u32) { - self.image_pipeline.dimensions(path) + fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + self.image_pipeline.dimensions(handle) } - fn draw(&mut self, path: &str, layout: Layout<'_>) -> Self::Output { + fn draw( + &mut self, + handle: image::Handle, + layout: Layout<'_>, + ) -> Self::Output { ( Primitive::Image { - path: String::from(path), + handle, bounds: layout.bounds(), }, MouseCursor::OutOfBounds, diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/wgpu/src/renderer/widget/progress_bar.rs new file mode 100644 index 00000000..34e33276 --- /dev/null +++ b/wgpu/src/renderer/widget/progress_bar.rs @@ -0,0 +1,54 @@ +use crate::{progress_bar::StyleSheet, Primitive, Renderer}; +use iced_native::{progress_bar, Color, MouseCursor, Rectangle}; + +impl progress_bar::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + + const DEFAULT_HEIGHT: u16 = 30; + + fn draw( + &self, + bounds: Rectangle, + range: std::ops::RangeInclusive<f32>, + value: f32, + style_sheet: &Self::Style, + ) -> Self::Output { + let style = style_sheet.style(); + + let (range_start, range_end) = range.into_inner(); + let active_progress_width = bounds.width + * ((value - range_start) / (range_end - range_start).max(1.0)); + + let background = Primitive::Group { + primitives: vec![Primitive::Quad { + bounds: Rectangle { ..bounds }, + background: style.background, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }], + }; + + ( + if active_progress_width > 0.0 { + let bar = Primitive::Quad { + bounds: Rectangle { + width: active_progress_width, + ..bounds + }, + background: style.bar, + border_radius: style.border_radius, + border_width: 0, + border_color: Color::TRANSPARENT, + }; + + Primitive::Group { + primitives: vec![background, bar], + } + } else { + background + }, + MouseCursor::OutOfBounds, + ) + } +} diff --git a/wgpu/src/renderer/widget/radio.rs b/wgpu/src/renderer/widget/radio.rs index 3c00a4c2..564f066b 100644 --- a/wgpu/src/renderer/widget/radio.rs +++ b/wgpu/src/renderer/widget/radio.rs @@ -1,10 +1,12 @@ -use crate::{Primitive, Renderer}; -use iced_native::{radio, Background, MouseCursor, Rectangle}; +use crate::{radio::StyleSheet, Primitive, Renderer}; +use iced_native::{radio, Background, Color, MouseCursor, Rectangle}; const SIZE: f32 = 28.0; const DOT_SIZE: f32 = SIZE / 2.0; impl radio::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn default_size(&self) -> u32 { SIZE as u32 } @@ -15,31 +17,21 @@ impl radio::Renderer for Renderer { is_selected: bool, is_mouse_over: bool, (label, _): Self::Output, + style_sheet: &Self::Style, ) -> Self::Output { - let (radio_border, radio_box) = ( - Primitive::Quad { - bounds, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: (SIZE / 2.0) as u16, - }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color( - if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: (SIZE / 2.0 - 1.0) as u16, - }, - ); + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let radio = Primitive::Quad { + bounds, + background: style.background, + border_radius: (SIZE / 2.0) as u16, + border_width: style.border_width, + border_color: style.border_color, + }; ( Primitive::Group { @@ -51,13 +43,15 @@ impl radio::Renderer for Renderer { width: bounds.width - DOT_SIZE, height: bounds.height - DOT_SIZE, }, - background: Background::Color([0.3, 0.3, 0.3].into()), + background: Background::Color(style.dot_color), border_radius: (DOT_SIZE / 2.0) as u16, + border_width: 0, + border_color: Color::TRANSPARENT, }; - vec![radio_border, radio_box, radio_circle, label] + vec![radio, radio_circle, label] } else { - vec![radio_border, radio_box, label] + vec![radio, label] }, }, if is_mouse_over { diff --git a/wgpu/src/renderer/widget/row.rs b/wgpu/src/renderer/widget/row.rs index f082dc61..bd9f1a04 100644 --- a/wgpu/src/renderer/widget/row.rs +++ b/wgpu/src/renderer/widget/row.rs @@ -4,6 +4,7 @@ use iced_native::{row, Element, Layout, MouseCursor, Point}; impl row::Renderer for Renderer { fn draw<Message>( &mut self, + defaults: &Self::Defaults, children: &[Element<'_, Message, Self>], layout: Layout<'_>, cursor_position: Point, @@ -17,7 +18,7 @@ impl row::Renderer for Renderer { .zip(layout.children()) .map(|(child, layout)| { let (primitive, new_mouse_cursor) = - child.draw(self, layout, cursor_position); + child.draw(self, defaults, layout, cursor_position); if new_mouse_cursor > mouse_cursor { mouse_cursor = new_mouse_cursor; diff --git a/wgpu/src/renderer/widget/scrollable.rs b/wgpu/src/renderer/widget/scrollable.rs index 58dc3df9..bfee7411 100644 --- a/wgpu/src/renderer/widget/scrollable.rs +++ b/wgpu/src/renderer/widget/scrollable.rs @@ -1,45 +1,63 @@ use crate::{Primitive, Renderer}; use iced_native::{ - scrollable, Background, MouseCursor, Point, Rectangle, Vector, + scrollable, Background, Color, MouseCursor, Rectangle, Vector, }; const SCROLLBAR_WIDTH: u16 = 10; const SCROLLBAR_MARGIN: u16 = 2; -fn scrollbar_bounds(bounds: Rectangle) -> Rectangle { - Rectangle { - x: bounds.x + bounds.width - - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - y: bounds.y, - width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - height: bounds.height, - } -} - impl scrollable::Renderer for Renderer { - fn is_mouse_over_scrollbar( + type Style = Box<dyn iced_style::scrollable::StyleSheet>; + + fn scrollbar( &self, bounds: Rectangle, content_bounds: Rectangle, - cursor_position: Point, - ) -> bool { - content_bounds.height > bounds.height - && scrollbar_bounds(bounds).contains(cursor_position) + offset: u32, + ) -> Option<scrollable::Scrollbar> { + if content_bounds.height > bounds.height { + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + y: bounds.y, + width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scrollbar_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), + y: scrollbar_bounds.y + y_offset, + width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), + height: scrollbar_height, + }; + + Some(scrollable::Scrollbar { + bounds: scrollbar_bounds, + scroller: scrollable::Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } } fn draw( &mut self, state: &scrollable::State, bounds: Rectangle, - content_bounds: Rectangle, + _content_bounds: Rectangle, is_mouse_over: bool, is_mouse_over_scrollbar: bool, + scrollbar: Option<scrollable::Scrollbar>, offset: u32, + style_sheet: &Self::Style, (content, mouse_cursor): Self::Output, ) -> Self::Output { - let is_content_overflowing = content_bounds.height > bounds.height; - let scrollbar_bounds = scrollbar_bounds(bounds); - let clip = Primitive::Clip { bounds, offset: Vector::new(0, offset), @@ -47,51 +65,59 @@ impl scrollable::Renderer for Renderer { }; ( - if is_content_overflowing - && (is_mouse_over || state.is_scrollbar_grabbed()) - { - let ratio = bounds.height / content_bounds.height; - let scrollbar_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; + if let Some(scrollbar) = scrollbar { + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0; - let scrollbar = Primitive::Quad { - bounds: Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), - y: scrollbar_bounds.y + y_offset, - width: scrollbar_bounds.width - - f32::from(2 * SCROLLBAR_MARGIN), - height: scrollbar_height, - }, - background: Background::Color([0.0, 0.0, 0.0, 0.7].into()), - border_radius: 5, + let scroller = if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + Primitive::Quad { + bounds: scrollbar.scroller.bounds, + background: Background::Color(style.scroller.color), + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + } + } else { + Primitive::None }; - if is_mouse_over_scrollbar || state.is_scrollbar_grabbed() { - let scrollbar_background = Primitive::Quad { + let scrollbar = if is_scrollbar_visible { + Primitive::Quad { bounds: Rectangle { - x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), - width: scrollbar_bounds.width + x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), + width: scrollbar.bounds.width - f32::from(2 * SCROLLBAR_MARGIN), - ..scrollbar_bounds + ..scrollbar.bounds }, - background: Background::Color( - [0.0, 0.0, 0.0, 0.3].into(), - ), - border_radius: 5, - }; - - Primitive::Group { - primitives: vec![clip, scrollbar_background, scrollbar], + background: style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, } } else { - Primitive::Group { - primitives: vec![clip, scrollbar], - } + Primitive::None + }; + + Primitive::Group { + primitives: vec![clip, scrollbar, scroller], } } else { clip }, - if is_mouse_over_scrollbar || state.is_scrollbar_grabbed() { + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { MouseCursor::Idle } else { mouse_cursor diff --git a/wgpu/src/renderer/widget/slider.rs b/wgpu/src/renderer/widget/slider.rs index f561be0a..c8ebd0da 100644 --- a/wgpu/src/renderer/widget/slider.rs +++ b/wgpu/src/renderer/widget/slider.rs @@ -1,10 +1,14 @@ -use crate::{Primitive, Renderer}; +use crate::{ + slider::{HandleShape, StyleSheet}, + Primitive, Renderer, +}; use iced_native::{slider, Background, Color, MouseCursor, Point, Rectangle}; -const HANDLE_WIDTH: f32 = 8.0; const HANDLE_HEIGHT: f32 = 22.0; impl slider::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn height(&self) -> u32 { 30 } @@ -16,9 +20,18 @@ impl slider::Renderer for Renderer { range: std::ops::RangeInclusive<f32>, value: f32, is_dragging: bool, + style_sheet: &Self::Style, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); + let style = if is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + let rail_y = bounds.y + (bounds.height / 2.0).round(); let (rail_top, rail_bottom) = ( @@ -29,8 +42,10 @@ impl slider::Renderer for Renderer { width: bounds.width, height: 2.0, }, - background: Background::Color([0.6, 0.6, 0.6].into()), + background: Background::Color(style.rail_colors.0), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }, Primitive::Quad { bounds: Rectangle { @@ -39,51 +54,45 @@ impl slider::Renderer for Renderer { width: bounds.width, height: 2.0, }, - background: Background::Color(Color::WHITE), + background: Background::Color(style.rail_colors.1), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }, ); let (range_start, range_end) = range.into_inner(); - let handle_offset = (bounds.width - HANDLE_WIDTH) + let (handle_width, handle_height, handle_border_radius) = + match style.handle.shape { + HandleShape::Circle { radius } => { + (f32::from(radius * 2), f32::from(radius * 2), radius) + } + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), HANDLE_HEIGHT, border_radius), + }; + + let handle_offset = (bounds.width - handle_width) * ((value - range_start) / (range_end - range_start).max(1.0)); - let (handle_border, handle) = ( - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round() - 1.0, - y: rail_y - HANDLE_HEIGHT / 2.0 - 1.0, - width: HANDLE_WIDTH + 2.0, - height: HANDLE_HEIGHT + 2.0, - }, - background: Background::Color([0.6, 0.6, 0.6].into()), - border_radius: 5, + let handle = Primitive::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, }, - Primitive::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - HANDLE_HEIGHT / 2.0, - width: HANDLE_WIDTH, - height: HANDLE_HEIGHT, - }, - background: Background::Color( - if is_dragging { - [0.85, 0.85, 0.85] - } else if is_mouse_over { - [0.90, 0.90, 0.90] - } else { - [0.95, 0.95, 0.95] - } - .into(), - ), - border_radius: 4, - }, - ); + background: Background::Color(style.handle.color), + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }; ( Primitive::Group { - primitives: vec![rail_top, rail_bottom, handle_border, handle], + primitives: vec![rail_top, rail_bottom, handle], }, if is_dragging { MouseCursor::Grabbing diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs new file mode 100644 index 00000000..28e05437 --- /dev/null +++ b/wgpu/src/renderer/widget/space.rs @@ -0,0 +1,8 @@ +use crate::{Primitive, Renderer}; +use iced_native::{space, MouseCursor, Rectangle}; + +impl space::Renderer for Renderer { + fn draw(&mut self, _bounds: Rectangle) -> Self::Output { + (Primitive::None, MouseCursor::OutOfBounds) + } +} diff --git a/wgpu/src/renderer/widget/svg.rs b/wgpu/src/renderer/widget/svg.rs new file mode 100644 index 00000000..67bc3fe1 --- /dev/null +++ b/wgpu/src/renderer/widget/svg.rs @@ -0,0 +1,22 @@ +use crate::{Primitive, Renderer}; +use iced_native::{svg, Layout, MouseCursor}; + +impl svg::Renderer for Renderer { + fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + self.image_pipeline.viewport_dimensions(handle) + } + + fn draw( + &mut self, + handle: svg::Handle, + layout: Layout<'_>, + ) -> Self::Output { + ( + Primitive::Svg { + handle, + bounds: layout.bounds(), + }, + MouseCursor::OutOfBounds, + ) + } +} diff --git a/wgpu/src/renderer/widget/text.rs b/wgpu/src/renderer/widget/text.rs index 08a162ba..33e549cd 100644 --- a/wgpu/src/renderer/widget/text.rs +++ b/wgpu/src/renderer/widget/text.rs @@ -6,13 +6,8 @@ use iced_native::{ use std::f32; -// TODO: Obtain from renderer configuration -const DEFAULT_TEXT_SIZE: f32 = 20.0; - impl text::Renderer for Renderer { - fn default_size(&self) -> u16 { - DEFAULT_TEXT_SIZE as u16 - } + const DEFAULT_SIZE: u16 = 20; fn measure( &self, @@ -27,6 +22,7 @@ impl text::Renderer for Renderer { fn draw( &mut self, + defaults: &Self::Defaults, bounds: Rectangle, content: &str, size: u16, @@ -40,7 +36,7 @@ impl text::Renderer for Renderer { content: content.to_string(), size: f32::from(size), bounds, - color: color.unwrap_or(Color::BLACK), + color: color.unwrap_or(defaults.text.color), font, horizontal_alignment, vertical_alignment, diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs index 9ed3b415..e2a1b3a9 100644 --- a/wgpu/src/renderer/widget/text_input.rs +++ b/wgpu/src/renderer/widget/text_input.rs @@ -1,4 +1,4 @@ -use crate::{Primitive, Renderer}; +use crate::{text_input::StyleSheet, Primitive, Renderer}; use iced_native::{ text_input, Background, Color, Font, HorizontalAlignment, MouseCursor, @@ -7,48 +7,85 @@ use iced_native::{ use std::f32; impl text_input::Renderer for Renderer { + type Style = Box<dyn StyleSheet>; + fn default_size(&self) -> u16 { // TODO: Make this configurable 20 } + fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 { + let (mut width, _) = self.text_pipeline.measure( + value, + f32::from(size), + font, + Size::INFINITY, + ); + + let spaces_at_the_end = value.len() - value.trim_end().len(); + + if spaces_at_the_end > 0 { + let space_width = self.text_pipeline.space_width(size as f32); + width += spaces_at_the_end as f32 * space_width; + } + + width + } + + fn offset( + &self, + text_bounds: Rectangle, + size: u16, + value: &text_input::Value, + state: &text_input::State, + font: Font, + ) -> f32 { + if state.is_focused() { + let (_, offset) = measure_cursor_and_scroll_offset( + self, + text_bounds, + value, + size, + state.cursor_position(value), + font, + ); + + offset + } else { + 0.0 + } + } + fn draw( &mut self, bounds: Rectangle, text_bounds: Rectangle, cursor_position: Point, size: u16, + font: Font, placeholder: &str, value: &text_input::Value, state: &text_input::State, + style_sheet: &Self::Style, ) -> Self::Output { let is_mouse_over = bounds.contains(cursor_position); - let border = Primitive::Quad { - bounds, - background: Background::Color( - if is_mouse_over || state.is_focused() { - [0.5, 0.5, 0.5] - } else { - [0.7, 0.7, 0.7] - } - .into(), - ), - border_radius: 5, + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() }; let input = Primitive::Quad { - bounds: Rectangle { - x: bounds.x + 1.0, - y: bounds.y + 1.0, - width: bounds.width - 2.0, - height: bounds.height - 2.0, - }, - background: Background::Color(Color::WHITE), - border_radius: 5, + bounds, + background: style.background, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, }; - let size = f32::from(size); let text = value.to_string(); let text_value = Primitive::Text { @@ -58,40 +95,31 @@ impl text_input::Renderer for Renderer { text.clone() }, color: if text.is_empty() { - [0.7, 0.7, 0.7] + style_sheet.placeholder_color() } else { - [0.3, 0.3, 0.3] + style_sheet.value_color() } .into(), - font: Font::Default, + font, bounds: Rectangle { width: f32::INFINITY, ..text_bounds }, - size, + size: f32::from(size), horizontal_alignment: HorizontalAlignment::Left, vertical_alignment: VerticalAlignment::Center, }; let (contents_primitive, offset) = if state.is_focused() { - let text_before_cursor = - value.until(state.cursor_position(value)).to_string(); - - let (mut text_value_width, _) = self.text_pipeline.measure( - &text_before_cursor, + let (text_value_width, offset) = measure_cursor_and_scroll_offset( + self, + text_bounds, + value, size, - Font::Default, - Size::new(f32::INFINITY, text_bounds.height), + state.cursor_position(value), + font, ); - let spaces_at_the_end = - text_before_cursor.len() - text_before_cursor.trim_end().len(); - - if spaces_at_the_end > 0 { - let space_width = self.text_pipeline.space_width(size); - text_value_width += spaces_at_the_end as f32 * space_width; - } - let cursor = Primitive::Quad { bounds: Rectangle { x: text_bounds.x + text_value_width, @@ -99,19 +127,17 @@ impl text_input::Renderer for Renderer { width: 1.0, height: text_bounds.height, }, - background: Background::Color(Color::BLACK), + background: Background::Color(style_sheet.value_color()), border_radius: 0, + border_width: 0, + border_color: Color::TRANSPARENT, }; ( Primitive::Group { primitives: vec![text_value, cursor], }, - Vector::new( - ((text_value_width + 5.0) - text_bounds.width).max(0.0) - as u32, - 0, - ), + Vector::new(offset as u32, 0), ) } else { (text_value, Vector::new(0, 0)) @@ -125,7 +151,7 @@ impl text_input::Renderer for Renderer { ( Primitive::Group { - primitives: vec![border, input, contents], + primitives: vec![input, contents], }, if is_mouse_over { MouseCursor::Text @@ -135,3 +161,22 @@ impl text_input::Renderer for Renderer { ) } } + +fn measure_cursor_and_scroll_offset( + renderer: &Renderer, + text_bounds: Rectangle, + value: &text_input::Value, + size: u16, + cursor_index: usize, + font: Font, +) -> (f32, f32) { + use iced_native::text_input::Renderer; + + let text_before_cursor = value.until(cursor_index).to_string(); + + let text_value_width = + renderer.measure_value(&text_before_cursor, size, font); + let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); + + (text_value_width, offset) +} diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs new file mode 100644 index 00000000..f946ce0d --- /dev/null +++ b/wgpu/src/settings.rs @@ -0,0 +1,56 @@ +//! Configure a [`Renderer`]. +//! +//! [`Renderer`]: struct.Renderer.html + +/// The settings of a [`Renderer`]. +/// +/// [`Renderer`]: ../struct.Renderer.html +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Settings { + /// The output format of the [`Renderer`]. + /// + /// [`Renderer`]: ../struct.Renderer.html + pub format: wgpu::TextureFormat, + + /// The bytes of the font that will be used by default. + /// + /// If `None` is provided, a default system font will be chosen. + pub default_font: Option<&'static [u8]>, + + /// The antialiasing strategy that will be used for triangle primitives. + pub antialiasing: Option<Antialiasing>, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + default_font: None, + antialiasing: None, + } + } +} + +/// An antialiasing strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Antialiasing { + /// Multisample AA with 2 samples + MSAAx2, + /// Multisample AA with 4 samples + MSAAx4, + /// Multisample AA with 8 samples + MSAAx8, + /// Multisample AA with 16 samples + MSAAx16, +} + +impl Antialiasing { + pub(crate) fn sample_count(&self) -> u32 { + match self { + Antialiasing::MSAAx2 => 2, + Antialiasing::MSAAx4 => 4, + Antialiasing::MSAAx8 => 8, + Antialiasing::MSAAx16 => 16, + } + } +} diff --git a/wgpu/src/shader/blit.frag b/wgpu/src/shader/blit.frag new file mode 100644 index 00000000..dfed960f --- /dev/null +++ b/wgpu/src/shader/blit.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(location = 0) in vec2 v_Uv; + +layout(set = 0, binding = 0) uniform sampler u_Sampler; +layout(set = 1, binding = 0) uniform texture2D u_Texture; + +layout(location = 0) out vec4 o_Color; + +void main() { + o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv); +} diff --git a/wgpu/src/shader/blit.frag.spv b/wgpu/src/shader/blit.frag.spv Binary files differnew file mode 100644 index 00000000..2c5638b5 --- /dev/null +++ b/wgpu/src/shader/blit.frag.spv diff --git a/wgpu/src/shader/blit.vert b/wgpu/src/shader/blit.vert new file mode 100644 index 00000000..1c081b9e --- /dev/null +++ b/wgpu/src/shader/blit.vert @@ -0,0 +1,26 @@ +#version 450 + +layout(location = 0) out vec2 o_Uv; + +const vec2 positions[6] = vec2[6]( + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2(1.0, 1.0), + vec2(-1.0, -1.0), + vec2(1.0, -1.0), + vec2(1.0, 1.0) +); + +const vec2 uvs[6] = vec2[6]( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + o_Uv = uvs[gl_VertexIndex]; + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} diff --git a/wgpu/src/shader/blit.vert.spv b/wgpu/src/shader/blit.vert.spv Binary files differnew file mode 100644 index 00000000..ad697d48 --- /dev/null +++ b/wgpu/src/shader/blit.vert.spv diff --git a/wgpu/src/shader/image.frag b/wgpu/src/shader/image.frag index e35e455a..2809e9e6 100644 --- a/wgpu/src/shader/image.frag +++ b/wgpu/src/shader/image.frag @@ -1,12 +1,12 @@ #version 450 -layout(location = 0) in vec2 v_Uv; +layout(location = 0) in vec3 v_Uv; layout(set = 0, binding = 1) uniform sampler u_Sampler; -layout(set = 1, binding = 0) uniform texture2D u_Texture; +layout(set = 1, binding = 0) uniform texture2DArray u_Texture; layout(location = 0) out vec4 o_Color; void main() { - o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv); + o_Color = texture(sampler2DArray(u_Texture, u_Sampler), v_Uv); } diff --git a/wgpu/src/shader/image.frag.spv b/wgpu/src/shader/image.frag.spv Binary files differindex ebee82ac..65b08aa3 100644 --- a/wgpu/src/shader/image.frag.spv +++ b/wgpu/src/shader/image.frag.spv diff --git a/wgpu/src/shader/image.vert b/wgpu/src/shader/image.vert index 688c2311..dab53cfe 100644 --- a/wgpu/src/shader/image.vert +++ b/wgpu/src/shader/image.vert @@ -3,15 +3,18 @@ layout(location = 0) in vec2 v_Pos; layout(location = 1) in vec2 i_Pos; layout(location = 2) in vec2 i_Scale; +layout(location = 3) in vec2 i_Atlas_Pos; +layout(location = 4) in vec2 i_Atlas_Scale; +layout(location = 5) in uint i_Layer; layout (set = 0, binding = 0) uniform Globals { mat4 u_Transform; }; -layout(location = 0) out vec2 o_Uv; +layout(location = 0) out vec3 o_Uv; void main() { - o_Uv = v_Pos; + o_Uv = vec3(v_Pos * i_Atlas_Scale + i_Atlas_Pos, i_Layer); mat4 i_Transform = mat4( vec4(i_Scale.x, 0.0, 0.0, 0.0), diff --git a/wgpu/src/shader/image.vert.spv b/wgpu/src/shader/image.vert.spv Binary files differindex 9ba702bc..21f5db2d 100644 --- a/wgpu/src/shader/image.vert.spv +++ b/wgpu/src/shader/image.vert.spv diff --git a/wgpu/src/shader/quad.frag b/wgpu/src/shader/quad.frag index 2ee77e71..ad1af1ad 100644 --- a/wgpu/src/shader/quad.frag +++ b/wgpu/src/shader/quad.frag @@ -1,14 +1,17 @@ #version 450 layout(location = 0) in vec4 v_Color; -layout(location = 1) in vec2 v_Pos; -layout(location = 2) in vec2 v_Scale; -layout(location = 3) in float v_BorderRadius; +layout(location = 1) in vec4 v_BorderColor; +layout(location = 2) in vec2 v_Pos; +layout(location = 3) in vec2 v_Scale; +layout(location = 4) in float v_BorderRadius; +layout(location = 5) in float v_BorderWidth; layout(location = 0) out vec4 o_Color; -float rounded(in vec2 frag_coord, in vec2 position, in vec2 size, float radius, float s) +float distance(in vec2 frag_coord, in vec2 position, in vec2 size, float radius) { + // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN vec2 inner_size = size - vec2(radius, radius) * 2.0; vec2 top_left = position + vec2(radius, radius); vec2 bottom_right = top_left + inner_size; @@ -21,13 +24,43 @@ float rounded(in vec2 frag_coord, in vec2 position, in vec2 size, float radius, max(max(top_left_distance.y, bottom_right_distance.y), 0) ); - float d = sqrt(distance.x * distance.x + distance.y * distance.y); - - return 1.0 - smoothstep(radius - s, radius + s, d); + return sqrt(distance.x * distance.x + distance.y * distance.y); } void main() { - float radius_alpha = rounded(gl_FragCoord.xy, v_Pos, v_Scale, v_BorderRadius, 0.5); + vec4 mixed_color; + + // TODO: Remove branching (?) + if(v_BorderWidth > 0) { + float internal_border = max(v_BorderRadius - v_BorderWidth, 0); + + float internal_distance = distance( + gl_FragCoord.xy, + v_Pos + vec2(v_BorderWidth), + v_Scale - vec2(v_BorderWidth * 2.0), + internal_border + ); + + float border_mix = smoothstep( + max(internal_border - 0.5, 0.0), + internal_border + 0.5, + internal_distance + ); + + mixed_color = mix(v_Color, v_BorderColor, border_mix); + } else { + mixed_color = v_Color; + } + + float d = distance( + gl_FragCoord.xy, + v_Pos, + v_Scale, + v_BorderRadius + ); + + float radius_alpha = + 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0), v_BorderRadius + 0.5, d); - o_Color = vec4(v_Color.xyz, v_Color.w * radius_alpha); + o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); } diff --git a/wgpu/src/shader/quad.frag.spv b/wgpu/src/shader/quad.frag.spv Binary files differindex 17bd8f46..519f5f01 100644 --- a/wgpu/src/shader/quad.frag.spv +++ b/wgpu/src/shader/quad.frag.spv diff --git a/wgpu/src/shader/quad.vert b/wgpu/src/shader/quad.vert index 539755cb..1d9a4fd2 100644 --- a/wgpu/src/shader/quad.vert +++ b/wgpu/src/shader/quad.vert @@ -4,7 +4,9 @@ layout(location = 0) in vec2 v_Pos; layout(location = 1) in vec2 i_Pos; layout(location = 2) in vec2 i_Scale; layout(location = 3) in vec4 i_Color; -layout(location = 4) in float i_BorderRadius; +layout(location = 4) in vec4 i_BorderColor; +layout(location = 5) in float i_BorderRadius; +layout(location = 6) in float i_BorderWidth; layout (set = 0, binding = 0) uniform Globals { mat4 u_Transform; @@ -12,9 +14,11 @@ layout (set = 0, binding = 0) uniform Globals { }; layout(location = 0) out vec4 o_Color; -layout(location = 1) out vec2 o_Pos; -layout(location = 2) out vec2 o_Scale; -layout(location = 3) out float o_BorderRadius; +layout(location = 1) out vec4 o_BorderColor; +layout(location = 2) out vec2 o_Pos; +layout(location = 3) out vec2 o_Scale; +layout(location = 4) out float o_BorderRadius; +layout(location = 5) out float o_BorderWidth; void main() { vec2 p_Pos = i_Pos * u_Scale; @@ -28,9 +32,11 @@ void main() { ); o_Color = i_Color; + o_BorderColor = i_BorderColor; o_Pos = p_Pos; o_Scale = p_Scale; o_BorderRadius = i_BorderRadius * u_Scale; + o_BorderWidth = i_BorderWidth * u_Scale; gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0); } diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv Binary files differindex 9050adfb..7059b51b 100644 --- a/wgpu/src/shader/quad.vert.spv +++ b/wgpu/src/shader/quad.vert.spv diff --git a/wgpu/src/shader/triangle.frag b/wgpu/src/shader/triangle.frag new file mode 100644 index 00000000..e39c45e7 --- /dev/null +++ b/wgpu/src/shader/triangle.frag @@ -0,0 +1,8 @@ +#version 450 + +layout(location = 0) in vec4 i_Color; +layout(location = 0) out vec4 o_Color; + +void main() { + o_Color = i_Color; +} diff --git a/wgpu/src/shader/triangle.frag.spv b/wgpu/src/shader/triangle.frag.spv Binary files differnew file mode 100644 index 00000000..11201872 --- /dev/null +++ b/wgpu/src/shader/triangle.frag.spv diff --git a/wgpu/src/shader/triangle.vert b/wgpu/src/shader/triangle.vert new file mode 100644 index 00000000..1f2c009b --- /dev/null +++ b/wgpu/src/shader/triangle.vert @@ -0,0 +1,15 @@ +#version 450 + +layout(location = 0) in vec2 i_Position; +layout(location = 1) in vec4 i_Color; + +layout(location = 0) out vec4 o_Color; + +layout (set = 0, binding = 0) uniform Globals { + mat4 u_Transform; +}; + +void main() { + gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); + o_Color = i_Color; +} diff --git a/wgpu/src/shader/triangle.vert.spv b/wgpu/src/shader/triangle.vert.spv Binary files differnew file mode 100644 index 00000000..871f4f55 --- /dev/null +++ b/wgpu/src/shader/triangle.vert.spv diff --git a/wgpu/src/target.rs b/wgpu/src/target.rs new file mode 100644 index 00000000..1e72c0c3 --- /dev/null +++ b/wgpu/src/target.rs @@ -0,0 +1,14 @@ +use crate::Viewport; + +/// A rendering target. +#[derive(Debug)] +pub struct Target<'a> { + /// The texture where graphics will be rendered. + pub texture: &'a wgpu::TextureView, + + /// The viewport of the target. + /// + /// Most of the time, you will want this to match the dimensions of the + /// texture. + pub viewport: &'a Viewport, +} diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index bf37abe5..c5670102 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -11,6 +11,8 @@ pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External { pub const CHECKMARK_ICON: char = '\u{F00C}'; +const FALLBACK_FONT: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); + #[derive(Debug)] pub struct Pipeline { draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>, @@ -20,29 +22,43 @@ pub struct Pipeline { } impl Pipeline { - pub fn new(device: &mut wgpu::Device) -> Self { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + default_font: Option<&[u8]>, + ) -> Self { // TODO: Font customization let font_source = font::Source::new(); - let default_font = font_source - .load(&[font::Family::SansSerif, font::Family::Serif]) - .expect("Find sans-serif or serif font"); + let default_font = + default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| { + font_source + .load(&[font::Family::SansSerif, font::Family::Serif]) + .unwrap_or_else(|_| FALLBACK_FONT.to_vec()) + }); + + let load_glyph_brush = |font: Vec<u8>| { + let builder = + wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ + font.clone() + ])?; + + Ok(( + builder, + glyph_brush::GlyphBrushBuilder::using_font_bytes(font).build(), + )) + }; - let mono_font = font_source - .load(&[font::Family::Monospace]) - .expect("Find monospace font"); + let (brush_builder, measure_brush) = load_glyph_brush(default_font) + .unwrap_or_else(|_: wgpu_glyph::rusttype::Error| { + log::warn!("System font failed to load. Falling back to embedded font..."); - let draw_brush = - wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![ - mono_font, - default_font.clone(), - ]) - .initial_cache_size((2048, 2048)) - .build(device, wgpu::TextureFormat::Bgra8UnormSrgb); + load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font") + }); - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font_bytes(default_font) - .build(); + let draw_brush = brush_builder + .initial_cache_size((2048, 2048)) + .build(device, format); Pipeline { draw_brush: RefCell::new(draw_brush), @@ -95,14 +111,7 @@ impl Pipeline { text: content, scale: wgpu_glyph::Scale { x: size, y: size }, bounds: (bounds.width, bounds.height), - - // TODO: This is a bit hacky. We are loading the debug font as the - // first font in the `draw_brush`. The `measure_brush` does not - // contain this, hence we subtract 1. - // - // This should go away once we unify `draw_brush` and - // `measure_brush`. - font_id: wgpu_glyph::FontId(font_id - 1), + font_id: wgpu_glyph::FontId(font_id), ..Default::default() }; @@ -134,16 +143,28 @@ impl Pipeline { // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. // This makes stuff quite inconvenient. A manual method for trimming the // cache would make our lives easier. - let _ = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}) - .expect("Trim text measurements"); + loop { + let action = self + .measure_brush + .borrow_mut() + .process_queued(|_, _| {}, |_| {}); + + match action { + Ok(_) => break, + Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { + let (width, height) = suggested; + + self.measure_brush + .borrow_mut() + .resize_texture(width, height); + } + } + } } pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId { match font { - iced_native::Font::Default => wgpu_glyph::FontId(1), + iced_native::Font::Default => wgpu_glyph::FontId(0), iced_native::Font::External { name, bytes } => { if let Some(font_id) = self.draw_font_map.borrow().get(name) { return *font_id; diff --git a/wgpu/src/text/font.rs b/wgpu/src/text/font.rs index 31df5bf4..7346ccdb 100644 --- a/wgpu/src/text/font.rs +++ b/wgpu/src/text/font.rs @@ -1,5 +1,6 @@ -pub use font_kit::error::SelectionError as LoadError; -pub use font_kit::family_name::FamilyName as Family; +pub use font_kit::{ + error::SelectionError as LoadError, family_name::FamilyName as Family, +}; pub struct Source { raw: font_kit::source::SystemSource, diff --git a/wgpu/src/transformation.rs b/wgpu/src/transformation.rs index c8a7ee75..666696f3 100644 --- a/wgpu/src/transformation.rs +++ b/wgpu/src/transformation.rs @@ -13,10 +13,10 @@ impl Transformation { /// Creates an orthographic projection. #[rustfmt::skip] - pub fn orthographic(width: u16, height: u16) -> Transformation { + pub fn orthographic(width: u32, height: u32) -> Transformation { Transformation(Mat4::from_cols( - Vec4::new(2.0 / f32::from(width), 0.0, 0.0, 0.0), - Vec4::new(0.0, 2.0 / f32::from(height), 0.0, 0.0), + Vec4::new(2.0 / width as f32, 0.0, 0.0, 0.0), + Vec4::new(0.0, 2.0 / height as f32, 0.0, 0.0), Vec4::new(0.0, 0.0, -1.0, 0.0), Vec4::new(-1.0, -1.0, 0.0, 1.0) )) diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs new file mode 100644 index 00000000..85ed4bd5 --- /dev/null +++ b/wgpu/src/triangle.rs @@ -0,0 +1,391 @@ +//! Draw meshes of triangles. +use crate::{settings, Transformation}; +use iced_native::{Point, Rectangle}; +use std::{mem, sync::Arc}; + +mod msaa; + +const UNIFORM_BUFFER_SIZE: usize = 100; +const VERTEX_BUFFER_SIZE: usize = 100_000; +const INDEX_BUFFER_SIZE: usize = 100_000; + +#[derive(Debug)] +pub(crate) struct Pipeline { + pipeline: wgpu::RenderPipeline, + blit: Option<msaa::Blit>, + constants: wgpu::BindGroup, + uniforms_buffer: Buffer<Uniforms>, + vertex_buffer: Buffer<Vertex2D>, + index_buffer: Buffer<u32>, +} + +#[derive(Debug)] +struct Buffer<T> { + raw: wgpu::Buffer, + size: usize, + usage: wgpu::BufferUsage, + _type: std::marker::PhantomData<T>, +} + +impl<T> Buffer<T> { + pub fn new( + device: &wgpu::Device, + size: usize, + usage: wgpu::BufferUsage, + ) -> Self { + let raw = device.create_buffer(&wgpu::BufferDescriptor { + size: (std::mem::size_of::<T>() * size) as u64, + usage, + }); + + Buffer { + raw, + size, + usage, + _type: std::marker::PhantomData, + } + } + + pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) { + if self.size < size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + size: (std::mem::size_of::<T>() * size) as u64, + usage: self.usage, + }); + + self.size = size; + } + } +} + +impl Pipeline { + pub fn new( + device: &mut wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option<settings::Antialiasing>, + ) -> Pipeline { + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::VERTEX, + ty: wgpu::BindingType::UniformBuffer { dynamic: true }, + }], + }); + + let constants_buffer = Buffer::new( + device, + UNIFORM_BUFFER_SIZE, + wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + ); + + let constant_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &constant_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Buffer { + buffer: &constants_buffer.raw, + range: 0..std::mem::size_of::<Uniforms>() as u64, + }, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&constant_layout], + }); + + let vs = include_bytes!("shader/triangle.vert.spv"); + let vs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) + .expect("Read triangle vertex shader as SPIR-V"), + ); + + let fs = include_bytes!("shader/triangle.frag.spv"); + let fs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) + .expect("Read triangle fragment shader as SPIR-V"), + ); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Cw, + cull_mode: wgpu::CullMode::None, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format, + color_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint32, + vertex_buffers: &[wgpu::VertexBufferDescriptor { + stride: mem::size_of::<Vertex2D>() as u64, + step_mode: wgpu::InputStepMode::Vertex, + attributes: &[ + // Position + wgpu::VertexAttributeDescriptor { + shader_location: 0, + format: wgpu::VertexFormat::Float2, + offset: 0, + }, + // Color + wgpu::VertexAttributeDescriptor { + shader_location: 1, + format: wgpu::VertexFormat::Float4, + offset: 4 * 2, + }, + ], + }], + sample_count: antialiasing + .map(|a| a.sample_count()) + .unwrap_or(1), + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Pipeline { + pipeline, + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + constants: constant_bind_group, + uniforms_buffer: constants_buffer, + vertex_buffer: Buffer::new( + device, + VERTEX_BUFFER_SIZE, + wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, + ), + index_buffer: Buffer::new( + device, + INDEX_BUFFER_SIZE, + wgpu::BufferUsage::INDEX | wgpu::BufferUsage::COPY_DST, + ), + } + } + + pub fn draw( + &mut self, + device: &mut wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_width: u32, + target_height: u32, + transformation: Transformation, + meshes: &Vec<(Point, Arc<Mesh2D>)>, + bounds: Rectangle<u32>, + ) { + // This looks a bit crazy, but we are just counting how many vertices + // and indices we will need to handle. + // TODO: Improve readability + let (total_vertices, total_indices) = meshes + .iter() + .map(|(_, mesh)| (mesh.vertices.len(), mesh.indices.len())) + .fold((0, 0), |(total_v, total_i), (v, i)| { + (total_v + v, total_i + i) + }); + + // Then we ensure the current buffers are big enough, resizing if + // necessary + self.uniforms_buffer.ensure_capacity(device, meshes.len()); + self.vertex_buffer.ensure_capacity(device, total_vertices); + self.index_buffer.ensure_capacity(device, total_indices); + + let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len()); + let mut offsets: Vec<( + wgpu::BufferAddress, + wgpu::BufferAddress, + usize, + )> = Vec::with_capacity(meshes.len()); + let mut last_vertex = 0; + let mut last_index = 0; + + // We upload everything upfront + for (origin, mesh) in meshes { + let transform = Uniforms { + transform: (transformation + * Transformation::translate(origin.x, origin.y)) + .into(), + }; + + let vertex_buffer = device + .create_buffer_mapped( + mesh.vertices.len(), + wgpu::BufferUsage::COPY_SRC, + ) + .fill_from_slice(&mesh.vertices); + + let index_buffer = device + .create_buffer_mapped( + mesh.indices.len(), + wgpu::BufferUsage::COPY_SRC, + ) + .fill_from_slice(&mesh.indices); + + encoder.copy_buffer_to_buffer( + &vertex_buffer, + 0, + &self.vertex_buffer.raw, + (std::mem::size_of::<Vertex2D>() * last_vertex) as u64, + (std::mem::size_of::<Vertex2D>() * mesh.vertices.len()) as u64, + ); + + encoder.copy_buffer_to_buffer( + &index_buffer, + 0, + &self.index_buffer.raw, + (std::mem::size_of::<u32>() * last_index) as u64, + (std::mem::size_of::<u32>() * mesh.indices.len()) as u64, + ); + + uniforms.push(transform); + offsets.push(( + last_vertex as u64, + last_index as u64, + mesh.indices.len(), + )); + + last_vertex += mesh.vertices.len(); + last_index += mesh.indices.len(); + } + + let uniforms_buffer = device + .create_buffer_mapped(uniforms.len(), wgpu::BufferUsage::COPY_SRC) + .fill_from_slice(&uniforms); + + encoder.copy_buffer_to_buffer( + &uniforms_buffer, + 0, + &self.uniforms_buffer.raw, + 0, + (std::mem::size_of::<Uniforms>() * uniforms.len()) as u64, + ); + + { + let (attachment, resolve_target, load_op) = + if let Some(blit) = &mut self.blit { + let (attachment, resolve_target) = + blit.targets(device, target_width, target_height); + + (attachment, Some(resolve_target), wgpu::LoadOp::Clear) + } else { + (target, None, wgpu::LoadOp::Load) + }; + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment, + resolve_target, + load_op, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + }, + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + for (i, (vertex_offset, index_offset, indices)) in + offsets.into_iter().enumerate() + { + render_pass.set_bind_group( + 0, + &self.constants, + &[(std::mem::size_of::<Uniforms>() * i) as u64], + ); + + render_pass.set_index_buffer( + &self.index_buffer.raw, + index_offset * std::mem::size_of::<u32>() as u64, + ); + + render_pass.set_vertex_buffers( + 0, + &[( + &self.vertex_buffer.raw, + vertex_offset * std::mem::size_of::<Vertex2D>() as u64, + )], + ); + + render_pass.draw_indexed(0..indices as u32, 0, 0..1); + } + } + + if let Some(blit) = &mut self.blit { + blit.draw(encoder, target); + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Uniforms { + transform: [f32; 16], +} + +impl Default for Uniforms { + fn default() -> Self { + Self { + transform: *Transformation::identity().as_ref(), + } + } +} + +/// A two-dimensional vertex with some color in __linear__ RGBA. +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct Vertex2D { + /// The vertex position + pub position: [f32; 2], + /// The vertex color in __linear__ RGBA. + pub color: [f32; 4], +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +/// +/// [`Vertex2D`]: struct.Vertex2D.html +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec<Vertex2D>, + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec<u32>, +} diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs new file mode 100644 index 00000000..7ccfb062 --- /dev/null +++ b/wgpu/src/triangle/msaa.rs @@ -0,0 +1,264 @@ +use crate::settings; + +#[derive(Debug)] +pub struct Blit { + format: wgpu::TextureFormat, + pipeline: wgpu::RenderPipeline, + constants: wgpu::BindGroup, + texture_layout: wgpu::BindGroupLayout, + sample_count: u32, + targets: Option<Targets>, +} + +impl Blit { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: settings::Antialiasing, + ) -> Blit { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: -100.0, + lod_max_clamp: 100.0, + compare_function: wgpu::CompareFunction::Always, + }); + + let constant_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler, + }], + }); + + let constant_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &constant_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }], + }); + + let texture_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + bindings: &[wgpu::BindGroupLayoutBinding { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + multisampled: false, + dimension: wgpu::TextureViewDimension::D2, + }, + }], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&constant_layout, &texture_layout], + }); + + let vs = include_bytes!("../shader/blit.vert.spv"); + let vs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&vs[..])) + .expect("Read blit vertex shader as SPIR-V"), + ); + + let fs = include_bytes!("../shader/blit.frag.spv"); + let fs_module = device.create_shader_module( + &wgpu::read_spirv(std::io::Cursor::new(&fs[..])) + .expect("Read blit fragment shader as SPIR-V"), + ); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Cw, + cull_mode: wgpu::CullMode::None, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format, + color_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha_blend: wgpu::BlendDescriptor { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Blit { + format, + pipeline, + constants: constant_bind_group, + texture_layout: texture_layout, + sample_count: antialiasing.sample_count(), + targets: None, + } + } + + pub fn targets( + &mut self, + device: &wgpu::Device, + width: u32, + height: u32, + ) -> (&wgpu::TextureView, &wgpu::TextureView) { + match &mut self.targets { + None => { + self.targets = Some(Targets::new( + &device, + self.format, + &self.texture_layout, + self.sample_count, + width, + height, + )); + } + Some(targets) => { + if targets.width != width || targets.height != height { + self.targets = Some(Targets::new( + &device, + self.format, + &self.texture_layout, + self.sample_count, + width, + height, + )); + } + } + } + + let targets = self.targets.as_ref().unwrap(); + + (&targets.attachment, &targets.resolve) + } + + pub fn draw( + &self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[ + wgpu::RenderPassColorAttachmentDescriptor { + attachment: target, + resolve_target: None, + load_op: wgpu::LoadOp::Load, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }, + }, + ], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_bind_group( + 1, + &self.targets.as_ref().unwrap().bind_group, + &[], + ); + render_pass.draw(0..6, 0..1); + } +} + +#[derive(Debug)] +struct Targets { + attachment: wgpu::TextureView, + resolve: wgpu::TextureView, + bind_group: wgpu::BindGroup, + width: u32, + height: u32, +} + +impl Targets { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + texture_layout: &wgpu::BindGroupLayout, + sample_count: u32, + width: u32, + height: u32, + ) -> Targets { + let extent = wgpu::Extent3d { + width, + height, + depth: 1, + }; + + let attachment = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 1, + mip_level_count: 1, + sample_count, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + }); + + let resolve = device.create_texture(&wgpu::TextureDescriptor { + size: extent, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT + | wgpu::TextureUsage::SAMPLED, + }); + + let attachment = attachment.create_default_view(); + let resolve = resolve.create_default_view(); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: texture_layout, + bindings: &[wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(&resolve), + }], + }); + + Targets { + attachment, + resolve, + bind_group, + width, + height, + } + } +} diff --git a/wgpu/src/viewport.rs b/wgpu/src/viewport.rs new file mode 100644 index 00000000..66242468 --- /dev/null +++ b/wgpu/src/viewport.rs @@ -0,0 +1,29 @@ +use crate::Transformation; + +/// A viewing region for displaying computer graphics. +#[derive(Debug)] +pub struct Viewport { + width: u32, + height: u32, + transformation: Transformation, +} + +impl Viewport { + /// Creates a new [`Viewport`] with the given dimensions. + pub fn new(width: u32, height: u32) -> Viewport { + Viewport { + width, + height, + transformation: Transformation::orthographic(width, height), + } + } + + /// Returns the dimensions of the [`Viewport`]. + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + pub(crate) fn transformation(&self) -> Transformation { + self.transformation + } +} diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs new file mode 100644 index 00000000..73cce7e2 --- /dev/null +++ b/wgpu/src/widget.rs @@ -0,0 +1,41 @@ +//! Use the widgets supported out-of-the-box. +//! +//! # Re-exports +//! For convenience, the contents of this module are available at the root +//! module. Therefore, you can directly type: +//! +//! ``` +//! use iced_wgpu::{button, Button}; +//! ``` +pub mod button; +pub mod checkbox; +pub mod container; +pub mod progress_bar; +pub mod radio; +pub mod scrollable; +pub mod slider; +pub mod text_input; + +#[doc(no_inline)] +pub use button::Button; +#[doc(no_inline)] +pub use checkbox::Checkbox; +#[doc(no_inline)] +pub use container::Container; +#[doc(no_inline)] +pub use progress_bar::ProgressBar; +#[doc(no_inline)] +pub use radio::Radio; +#[doc(no_inline)] +pub use scrollable::Scrollable; +#[doc(no_inline)] +pub use slider::Slider; +#[doc(no_inline)] +pub use text_input::TextInput; + +#[cfg(feature = "canvas")] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs new file mode 100644 index 00000000..b738c55e --- /dev/null +++ b/wgpu/src/widget/button.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::button::State; +pub use iced_style::button::{Style, StyleSheet}; + +/// A widget that produces a message when clicked. +/// +/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`. +pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs new file mode 100644 index 00000000..38c1ce62 --- /dev/null +++ b/wgpu/src/widget/canvas.rs @@ -0,0 +1,149 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +//! +//! [`Canvas`]: struct.Canvas.html +//! [`Frame`]: struct.Frame.html +use crate::{Defaults, Primitive, Renderer}; + +use iced_native::{ + layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget, +}; +use std::hash::Hash; + +pub mod layer; +pub mod path; + +mod drawable; +mod fill; +mod frame; +mod stroke; + +pub use drawable::Drawable; +pub use fill::Fill; +pub use frame::Frame; +pub use layer::Layer; +pub use path::Path; +pub use stroke::{LineCap, LineJoin, Stroke}; + +/// A widget capable of drawing 2D graphics. +/// +/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the +/// painter's algorithm. In other words, layers will be drawn on top of each in +/// the same order they are pushed into the [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +/// [`Layer`]: layer/trait.Layer.html +#[derive(Debug)] +pub struct Canvas<'a> { + width: Length, + height: Length, + layers: Vec<Box<dyn Layer + 'a>>, +} + +impl<'a> Canvas<'a> { + const DEFAULT_SIZE: u16 = 100; + + /// Creates a new [`Canvas`] with no layers. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn new() -> Self { + Canvas { + width: Length::Units(Self::DEFAULT_SIZE), + height: Length::Units(Self::DEFAULT_SIZE), + layers: Vec::new(), + } + } + + /// Sets the width of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`Canvas`]. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Adds a [`Layer`] to the [`Canvas`]. + /// + /// It will be drawn on top of previous layers. + /// + /// [`Layer`]: layer/trait.Layer.html + /// [`Canvas`]: struct.Canvas.html + pub fn push(mut self, layer: impl Layer + 'a) -> Self { + self.layers.push(Box::new(layer)); + self + } +} + +impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> { + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn draw( + &self, + _renderer: &mut Renderer, + _defaults: &Defaults, + layout: Layout<'_>, + _cursor_position: Point, + ) -> (Primitive, MouseCursor) { + let bounds = layout.bounds(); + let origin = Point::new(bounds.x, bounds.y); + let size = Size::new(bounds.width, bounds.height); + + ( + Primitive::Group { + primitives: self + .layers + .iter() + .map(|layer| Primitive::Mesh2D { + origin, + buffers: layer.draw(size), + }) + .collect(), + }, + MouseCursor::Idle, + ) + } + + fn hash_layout(&self, state: &mut Hasher) { + std::any::TypeId::of::<Canvas<'static>>().hash(state); + + self.width.hash(state); + self.height.hash(state); + } +} + +impl<'a, Message> From<Canvas<'a>> for Element<'a, Message, Renderer> +where + Message: 'static, +{ + fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/wgpu/src/widget/canvas/drawable.rs b/wgpu/src/widget/canvas/drawable.rs new file mode 100644 index 00000000..6c74071c --- /dev/null +++ b/wgpu/src/widget/canvas/drawable.rs @@ -0,0 +1,12 @@ +use crate::canvas::Frame; + +/// A type that can be drawn on a [`Frame`]. +/// +/// [`Frame`]: struct.Frame.html +pub trait Drawable { + /// Draws the [`Drawable`] on the given [`Frame`]. + /// + /// [`Drawable`]: trait.Drawable.html + /// [`Frame`]: struct.Frame.html + fn draw(&self, frame: &mut Frame); +} diff --git a/wgpu/src/widget/canvas/fill.rs b/wgpu/src/widget/canvas/fill.rs new file mode 100644 index 00000000..5ce24cf3 --- /dev/null +++ b/wgpu/src/widget/canvas/fill.rs @@ -0,0 +1,14 @@ +use iced_native::Color; + +/// The style used to fill geometry. +#[derive(Debug, Clone, Copy)] +pub enum Fill { + /// Fill with a color. + Color(Color), +} + +impl Default for Fill { + fn default() -> Fill { + Fill::Color(Color::BLACK) + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs new file mode 100644 index 00000000..fa6d8c0a --- /dev/null +++ b/wgpu/src/widget/canvas/frame.rs @@ -0,0 +1,255 @@ +use iced_native::{Point, Size, Vector}; + +use crate::{ + canvas::{Fill, Path, Stroke}, + triangle, +}; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: struct.Canvas.html +#[derive(Debug)] +pub struct Frame { + width: f32, + height: f32, + buffers: lyon::tessellation::VertexBuffers<triangle::Vertex2D, u32>, + transforms: Transforms, +} + +#[derive(Debug)] +struct Transforms { + previous: Vec<Transform>, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Frame { + /// Creates a new empty [`Frame`] with the given dimensions. + /// + /// The default coordinate system of a [`Frame`] has its origin at the + /// top-left corner of its bounds. + /// + /// [`Frame`]: struct.Frame.html + pub fn new(width: f32, height: f32) -> Frame { + Frame { + width, + height, + buffers: lyon::tessellation::VertexBuffers::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + } + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn width(&self) -> f32 { + self.width + } + + /// Returns the width of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn height(&self) -> f32 { + self.height + } + + /// Returns the dimensions of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + /// Returns the coordinate of the center of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn center(&self) -> Point { + Point::new(self.width / 2.0, self.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn fill(&mut self, path: &Path, fill: Fill) { + use lyon::tessellation::{ + BuffersBuilder, FillOptions, FillTessellator, + }; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + FillVertex(match fill { + Fill::Color(color) => color.into_linear(), + }), + ); + + let mut tessellator = FillTessellator::new(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path( + path.raw(), + &FillOptions::default(), + &mut buffers, + ) + }; + + let _ = result.expect("Tessellate path"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + /// + /// [`Path`]: path/struct.Path.html + /// [`Frame`]: struct.Frame.html + pub fn stroke(&mut self, path: &Path, stroke: Stroke) { + use lyon::tessellation::{ + BuffersBuilder, StrokeOptions, StrokeTessellator, + }; + + let mut buffers = BuffersBuilder::new( + &mut self.buffers, + StrokeVertex(stroke.color.into_linear()), + ); + + let mut tessellator = StrokeTessellator::new(); + + let mut options = StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = stroke.line_cap.into(); + options.end_cap = stroke.line_cap.into(); + options.line_join = stroke.line_join.into(); + + let result = if self.transforms.current.is_identity { + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + } else { + let path = path.transformed(&self.transforms.current.raw); + + tessellator.tessellate_path(path.raw(), &options, &mut buffers) + }; + + let _ = result.expect("Stroke path"); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.transforms.previous.push(self.transforms.current); + + f(self); + + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Applies a translation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(-angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the geometry that has been drawn on the [`Frame`]. + /// + /// [`Frame`]: struct.Frame.html + pub fn into_mesh(self) -> triangle::Mesh2D { + triangle::Mesh2D { + vertices: self.buffers.vertices, + indices: self.buffers.indices, + } + } +} + +struct FillVertex([f32; 4]); + +impl lyon::tessellation::FillVertexConstructor<triangle::Vertex2D> + for FillVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::FillAttributes<'_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +struct StrokeVertex([f32; 4]); + +impl lyon::tessellation::StrokeVertexConstructor<triangle::Vertex2D> + for StrokeVertex +{ + fn new_vertex( + &mut self, + position: lyon::math::Point, + _attributes: lyon::tessellation::StrokeAttributes<'_, '_>, + ) -> triangle::Vertex2D { + triangle::Vertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs new file mode 100644 index 00000000..82d647bb --- /dev/null +++ b/wgpu/src/widget/canvas/layer.rs @@ -0,0 +1,25 @@ +//! Produce, store, and reuse geometry. +mod cache; + +pub use cache::Cache; + +use crate::triangle; + +use iced_native::Size; +use std::sync::Arc; + +/// A layer that can be presented at a [`Canvas`]. +/// +/// [`Canvas`]: ../struct.Canvas.html +pub trait Layer: std::fmt::Debug { + /// Draws the [`Layer`] in the given bounds and produces [`Mesh2D`] as a + /// result. + /// + /// The [`Layer`] may choose to store the produced [`Mesh2D`] locally and + /// only recompute it when the bounds change, its contents change, or is + /// otherwise explicitly cleared by other means. + /// + /// [`Layer`]: trait.Layer.html + /// [`Mesh2D`]: ../../../triangle/struct.Mesh2D.html + fn draw(&self, bounds: Size) -> Arc<triangle::Mesh2D>; +} diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs new file mode 100644 index 00000000..3071cce0 --- /dev/null +++ b/wgpu/src/widget/canvas/layer/cache.rs @@ -0,0 +1,101 @@ +use crate::{ + canvas::{Drawable, Frame, Layer}, + triangle, +}; + +use iced_native::Size; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::sync::Arc; + +/// A simple cache that stores generated geometry to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +/// +/// [`Layer`]: ../trait.Layer.html +/// [`Cached`]: struct.Cached.html +#[derive(Debug)] +pub struct Cache<T: Drawable> { + input: PhantomData<T>, + state: RefCell<State>, +} + +#[derive(Debug)] +enum State { + Empty, + Filled { + mesh: Arc<triangle::Mesh2D>, + bounds: Size, + }, +} + +impl<T> Cache<T> +where + T: Drawable + std::fmt::Debug, +{ + /// Creates a new empty [`Cache`]. + /// + /// [`Cache`]: struct.Cache.html + pub fn new() -> Self { + Cache { + input: PhantomData, + state: RefCell::new(State::Empty), + } + } + + /// Clears the cache, forcing a redraw the next time it is used. + /// + /// [`Cached`]: struct.Cached.html + pub fn clear(&mut self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Binds the [`Cache`] with some data, producing a [`Layer`] that can be + /// added to a [`Canvas`]. + /// + /// [`Cache`]: struct.Cache.html + /// [`Layer`]: ../trait.Layer.html + /// [`Canvas`]: ../../struct.Canvas.html + pub fn with<'a>(&'a self, input: &'a T) -> impl Layer + 'a { + Bind { + cache: self, + input: input, + } + } +} + +#[derive(Debug)] +struct Bind<'a, T: Drawable> { + cache: &'a Cache<T>, + input: &'a T, +} + +impl<'a, T> Layer for Bind<'a, T> +where + T: Drawable + std::fmt::Debug, +{ + fn draw(&self, current_bounds: Size) -> Arc<triangle::Mesh2D> { + use std::ops::Deref; + + if let State::Filled { mesh, bounds } = + self.cache.state.borrow().deref() + { + if *bounds == current_bounds { + return mesh.clone(); + } + } + + let mut frame = Frame::new(current_bounds.width, current_bounds.height); + self.input.draw(&mut frame); + + let mesh = Arc::new(frame.into_mesh()); + + *self.cache.state.borrow_mut() = State::Filled { + mesh: mesh.clone(), + bounds: current_bounds, + }; + + mesh + } +} diff --git a/wgpu/src/widget/canvas/path.rs b/wgpu/src/widget/canvas/path.rs new file mode 100644 index 00000000..15c2e853 --- /dev/null +++ b/wgpu/src/widget/canvas/path.rs @@ -0,0 +1,49 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +pub use arc::Arc; +pub use builder::Builder; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +/// +/// [`Path`]: struct.Path.html +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon::path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + #[inline] + pub(crate) fn raw(&self) -> &lyon::path::Path { + &self.raw + } + + #[inline] + pub(crate) fn transformed( + &self, + transform: &lyon::math::Transform, + ) -> Path { + Path { + raw: self.raw.transformed(transform), + } + } +} diff --git a/wgpu/src/widget/canvas/path/arc.rs b/wgpu/src/widget/canvas/path/arc.rs new file mode 100644 index 00000000..343191f1 --- /dev/null +++ b/wgpu/src/widget/canvas/path/arc.rs @@ -0,0 +1,44 @@ +//! Build and draw curves. +use iced_native::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +/// +/// [`Arc`]: struct.Arc.html +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From<Arc> for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/wgpu/src/widget/canvas/path/builder.rs b/wgpu/src/widget/canvas/path/builder.rs new file mode 100644 index 00000000..a013149e --- /dev/null +++ b/wgpu/src/widget/canvas/path/builder.rs @@ -0,0 +1,177 @@ +use crate::canvas::path::{arc, Arc, Path}; + +use iced_native::{Point, Size}; +use lyon::path::builder::{Build, FlatPathBuilder, PathBuilder, SvgBuilder}; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +/// +/// [`Path`]: struct.Path.html +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: lyon::path::builder::SvgPathBuilder<lyon::path::Builder>, +} + +impl Builder { + /// Creates a new [`Builder`]. + /// + /// [`Builder`]: struct.Builder.html + pub fn new() -> Builder { + Builder { + raw: lyon::path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + /// + /// [`Arc`]: struct.Arc.html + /// [`Path`]: struct.Path.html + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// The arc is connected to the previous point by a straight line, if + /// necessary. + /// + /// [`Path`]: struct.Path.html + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + use lyon::{math, path}; + + let a = math::Point::new(a.x, a.y); + + if self.raw.current_position() != a { + let _ = self.raw.line_to(a); + } + + let _ = self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + path::ArcFlags::default(), + math::Point::new(b.x, b.y), + ); + } + + /// Adds an [`Ellipse`] to the [`Path`] using a clockwise direction. + /// + /// [`Ellipse`]: struct.Arc.html + /// [`Path`]: struct.Path.html + pub fn ellipse(&mut self, arc: arc::Elliptical) { + use lyon::{geom, math}; + + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + use lyon::math; + + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + use lyon::math; + + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn rectangle(&mut self, p: Point, size: Size) { + self.move_to(p); + self.line_to(Point::new(p.x + size.width, p.y)); + self.line_to(Point::new(p.x + size.width, p.y + size.height)); + self.line_to(Point::new(p.x, p.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + /// + /// [`Path`]: struct.Path.html + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + /// + /// [`Path`]: struct.Path.html + /// [`Builder`]: struct.Builder.html + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} diff --git a/wgpu/src/widget/canvas/stroke.rs b/wgpu/src/widget/canvas/stroke.rs new file mode 100644 index 00000000..46d669c4 --- /dev/null +++ b/wgpu/src/widget/canvas/stroke.rs @@ -0,0 +1,83 @@ +use iced_native::Color; + +/// The style of a stroke. +#[derive(Debug, Clone, Copy)] +pub struct Stroke { + /// The color of the stroke. + pub color: Color, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, +} + +impl Default for Stroke { + fn default() -> Stroke { + Stroke { + color: Color::BLACK, + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +impl From<LineCap> for lyon::tessellation::LineCap { + fn from(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +impl From<LineJoin> for lyon::tessellation::LineJoin { + fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } + } +} diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs new file mode 100644 index 00000000..da0d7a84 --- /dev/null +++ b/wgpu/src/widget/checkbox.rs @@ -0,0 +1,9 @@ +//! Show toggle controls using checkboxes. +use crate::Renderer; + +pub use iced_style::checkbox::{Style, StyleSheet}; + +/// A box that can be checked. +/// +/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`. +pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>; diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs new file mode 100644 index 00000000..9a93a246 --- /dev/null +++ b/wgpu/src/widget/container.rs @@ -0,0 +1,10 @@ +//! Decorate content and apply alignment. +use crate::Renderer; + +pub use iced_style::container::{Style, StyleSheet}; + +/// An element decorating some content. +/// +/// This is an alias of an `iced_native` container with a default +/// `Renderer`. +pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>; diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs new file mode 100644 index 00000000..34450b5e --- /dev/null +++ b/wgpu/src/widget/progress_bar.rs @@ -0,0 +1,15 @@ +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Button`] has some local [`State`]. +//! +//! [`Button`]: type.Button.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_style::progress_bar::{Style, StyleSheet}; + +/// A bar that displays progress. +/// +/// This is an alias of an `iced_native` progress bar with an +/// `iced_wgpu::Renderer`. +pub type ProgressBar = iced_native::ProgressBar<Renderer>; diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs new file mode 100644 index 00000000..6e5cf042 --- /dev/null +++ b/wgpu/src/widget/radio.rs @@ -0,0 +1,10 @@ +//! Create choices using radio buttons. +use crate::Renderer; + +pub use iced_style::radio::{Style, StyleSheet}; + +/// A circular button representing a choice. +/// +/// This is an alias of an `iced_native` radio button with an +/// `iced_wgpu::Renderer`. +pub type Radio<Message> = iced_native::Radio<Message, Renderer>; diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs new file mode 100644 index 00000000..1d236105 --- /dev/null +++ b/wgpu/src/widget/scrollable.rs @@ -0,0 +1,13 @@ +//! Navigate an endless amount of content with a scrollbar. +use crate::Renderer; + +pub use iced_native::scrollable::State; +pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet}; + +/// A widget that can vertically display an infinite amount of content +/// with a scrollbar. +/// +/// This is an alias of an `iced_native` scrollable with a default +/// `Renderer`. +pub type Scrollable<'a, Message> = + iced_native::Scrollable<'a, Message, Renderer>; diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs new file mode 100644 index 00000000..4e47978f --- /dev/null +++ b/wgpu/src/widget/slider.rs @@ -0,0 +1,16 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`Slider`] has some local [`State`]. +//! +//! [`Slider`]: struct.Slider.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::slider::State; +pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet}; + +/// An horizontal bar and a handle that selects a single value from a range of +/// values. +/// +/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`. +pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Renderer>; diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs new file mode 100644 index 00000000..260fe3a6 --- /dev/null +++ b/wgpu/src/widget/text_input.rs @@ -0,0 +1,15 @@ +//! Display fields that can be filled with text. +//! +//! A [`TextInput`] has some local [`State`]. +//! +//! [`TextInput`]: struct.TextInput.html +//! [`State`]: struct.State.html +use crate::Renderer; + +pub use iced_native::text_input::State; +pub use iced_style::text_input::{Style, StyleSheet}; + +/// A field that can be filled with text. +/// +/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`. +pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>; diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs new file mode 100644 index 00000000..b7adad82 --- /dev/null +++ b/wgpu/src/window.rs @@ -0,0 +1,6 @@ +//! Display rendering results on windows. +mod backend; +mod swap_chain; + +pub use backend::Backend; +pub use swap_chain::SwapChain; diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs new file mode 100644 index 00000000..5b269f36 --- /dev/null +++ b/wgpu/src/window/backend.rs @@ -0,0 +1,113 @@ +use crate::{window::SwapChain, Renderer, Settings, Target}; + +use iced_native::MouseCursor; +use raw_window_handle::HasRawWindowHandle; + +/// A window graphics backend for iced powered by `wgpu`. +#[derive(Debug)] +pub struct Backend { + device: wgpu::Device, + queue: wgpu::Queue, + format: wgpu::TextureFormat, +} + +impl iced_native::window::Backend for Backend { + type Settings = Settings; + type Renderer = Renderer; + type Surface = wgpu::Surface; + type SwapChain = SwapChain; + + fn new(settings: Self::Settings) -> (Backend, Renderer) { + let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions { + power_preference: if settings.antialiasing.is_none() { + wgpu::PowerPreference::Default + } else { + wgpu::PowerPreference::HighPerformance + }, + backends: wgpu::BackendBit::all(), + }) + .expect("Request adapter"); + + let (mut device, queue) = + adapter.request_device(&wgpu::DeviceDescriptor { + extensions: wgpu::Extensions { + anisotropic_filtering: false, + }, + limits: wgpu::Limits { max_bind_groups: 2 }, + }); + + let renderer = Renderer::new(&mut device, settings); + + ( + Backend { + device, + queue, + format: settings.format, + }, + renderer, + ) + } + + fn create_surface<W: HasRawWindowHandle>( + &mut self, + window: &W, + ) -> wgpu::Surface { + wgpu::Surface::create(window) + } + + fn create_swap_chain( + &mut self, + surface: &Self::Surface, + width: u32, + height: u32, + ) -> SwapChain { + SwapChain::new(&self.device, surface, self.format, width, height) + } + + fn draw<T: AsRef<str>>( + &mut self, + renderer: &mut Self::Renderer, + swap_chain: &mut SwapChain, + output: &<Self::Renderer as iced_native::Renderer>::Output, + scale_factor: f64, + overlay: &[T], + ) -> MouseCursor { + let (frame, viewport) = swap_chain.next_frame(); + + let mut encoder = self.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { todo: 0 }, + ); + + let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }, + }], + depth_stencil_attachment: None, + }); + + let mouse_cursor = renderer.draw( + &mut self.device, + &mut encoder, + Target { + texture: &frame.view, + viewport, + }, + output, + scale_factor, + overlay, + ); + + self.queue.submit(&[encoder.finish()]); + + mouse_cursor + } +} diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs new file mode 100644 index 00000000..4ca2901b --- /dev/null +++ b/wgpu/src/window/swap_chain.rs @@ -0,0 +1,57 @@ +use crate::Viewport; + +/// The rendering target of a window. +/// +/// It represents a series of virtual framebuffers with a scale factor. +#[derive(Debug)] +pub struct SwapChain { + raw: wgpu::SwapChain, + viewport: Viewport, +} + +impl SwapChain {} + +impl SwapChain { + /// Creates a new [`SwapChain`] for the given surface. + /// + /// [`SwapChain`]: struct.SwapChain.html + pub fn new( + device: &wgpu::Device, + surface: &wgpu::Surface, + format: wgpu::TextureFormat, + width: u32, + height: u32, + ) -> SwapChain { + SwapChain { + raw: new_swap_chain(surface, format, width, height, device), + viewport: Viewport::new(width, height), + } + } + + /// Returns the next frame of the [`SwapChain`] alongside its [`Viewport`]. + /// + /// [`SwapChain`]: struct.SwapChain.html + /// [`Viewport`]: ../struct.Viewport.html + pub fn next_frame(&mut self) -> (wgpu::SwapChainOutput<'_>, &Viewport) { + (self.raw.get_next_texture(), &self.viewport) + } +} + +fn new_swap_chain( + surface: &wgpu::Surface, + format: wgpu::TextureFormat, + width: u32, + height: u32, + device: &wgpu::Device, +) -> wgpu::SwapChain { + device.create_swap_chain( + &surface, + &wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format, + width, + height, + present_mode: wgpu::PresentMode::Vsync, + }, + ) +} |