diff --git a/core/src/border.rs b/core/src/border.rs index cf73284c..232211db 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -241,9 +241,9 @@ impl From for Radius { } } -impl From for Radius { - fn from(w: u16) -> Self { - Self::from(f32::from(w)) +impl From for Radius { + fn from(w: u32) -> Self { + Self::from(w as f32) } } diff --git a/core/src/image.rs b/core/src/image.rs index 4b72004e..b92fdb0d 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,6 +1,7 @@ //! Load and draw raster graphics. pub use bytes::Bytes; +use crate::border; use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; @@ -15,6 +16,16 @@ pub struct Image { /// The handle of the image. pub handle: H, + /// The clip bounds of the [`Image`]. + /// + /// Anything outside this [`Rectangle`] will not be drawn. + pub clip_bounds: Rectangle, + + /// The border radius of the [`Image`]. + /// + /// Currently, this will only be applied to the `clip_bounds`. + pub border_radius: border::Radius, + /// The filter method of the image. pub filter_method: FilterMethod, @@ -38,6 +49,8 @@ impl Image { pub fn new(handle: impl Into) -> Self { Self { handle: handle.into(), + clip_bounds: Rectangle::INFINITE, + border_radius: border::Radius::default(), filter_method: FilterMethod::default(), rotation: Radians(0.0), opacity: 1.0, diff --git a/examples/gallery/src/civitai.rs b/examples/gallery/src/civitai.rs index 322716f9..4cca8b47 100644 --- a/examples/gallery/src/civitai.rs +++ b/examples/gallery/src/civitai.rs @@ -81,7 +81,9 @@ impl Image { .url .split("/") .map(|part| { - if part.starts_with("width=") { + if part.starts_with("width=") + || part.starts_with("original=") + { format!("width={}", width * 2) // High DPI } else { part.to_owned() diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index ccd03589..10f1704f 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -7,6 +7,7 @@ mod civitai; use crate::civitai::{Bytes, Error, Id, Image, Rgba, Size}; use iced::animation; +use iced::border; use iced::time::{Instant, milliseconds}; use iced::widget::{ button, container, float, grid, image, mouse_area, opaque, scrollable, @@ -283,7 +284,8 @@ fn card<'a>( image(allocation.handle()) .width(Fill) .content_fit(ContentFit::Cover) - .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)), + .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) + .border_radius(BORDER_RADIUS), ) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) .translate(move |bounds, viewport| { @@ -298,7 +300,7 @@ fn card<'a>( blur_radius: thumbnail.zoom.interpolate(0.0, 20.0, now), ..Shadow::default() }, - ..float::Style::default() + shadow_border_radius: border::radius(BORDER_RADIUS), }) .into() } else { @@ -309,7 +311,8 @@ fn card<'a>( let blurhash = image(&blurhash.handle) .width(Fill) .content_fit(ContentFit::Cover) - .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now)); + .opacity(blurhash.fade_in.interpolate(0.0, 1.0, now)) + .border_radius(BORDER_RADIUS); stack![blurhash, thumbnail].into() } else { @@ -319,7 +322,7 @@ fn card<'a>( space::horizontal().into() }; - let card = mouse_area(container(image).style(container::dark)) + let card = mouse_area(container(image).style(rounded)) .on_enter(Message::ThumbnailHovered(metadata.id, true)) .on_exit(Message::ThumbnailHovered(metadata.id, false)); @@ -342,7 +345,7 @@ fn card<'a>( } fn placeholder<'a>() -> Element<'a, Message> { - container(space()).style(container::dark).into() + container(space()).style(rounded).into() } enum Preview { @@ -554,10 +557,8 @@ impl Viewer { } fn to_rgba(bytes: Bytes) -> Task { - use tokio::task; - Task::future(async move { - task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || { match ::image::load_from_memory(bytes.as_slice()) { Ok(image) => { let rgba = image.to_rgba8(); @@ -575,3 +576,9 @@ fn to_rgba(bytes: Bytes) -> Task { .unwrap() }) } + +fn rounded(theme: &Theme) -> container::Style { + container::dark(theme).border(border::rounded(BORDER_RADIUS)) +} + +const BORDER_RADIUS: u32 = 20; diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index dba35e48..172922da 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -42,7 +42,7 @@ impl Buffer { } pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool { - let new_size = (std::mem::size_of::() * new_count) as u64; + let new_size = next_copy_size::(new_count); if self.size < new_size { self.raw = device.create_buffer(&wgpu::BufferDescriptor { diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index 6cc28ce0..5f7265ae 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -16,13 +16,15 @@ pub const MAX_SIZE: u32 = 2048; use crate::core::Size; use crate::graphics::color; +use std::sync::Arc; + #[derive(Debug)] pub struct Atlas { size: u32, backend: wgpu::Backend, texture: wgpu::Texture, texture_view: wgpu::TextureView, - texture_bind_group: wgpu::BindGroup, + texture_bind_group: Arc, texture_layout: wgpu::BindGroupLayout, layers: Vec, } @@ -95,13 +97,13 @@ impl Atlas { backend, texture, texture_view, - texture_bind_group, + texture_bind_group: Arc::new(texture_bind_group), texture_layout, layers, } } - pub fn bind_group(&self) -> &wgpu::BindGroup { + pub fn bind_group(&self) -> &Arc { &self.texture_bind_group } @@ -466,7 +468,7 @@ impl Atlas { }); self.texture_bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { + Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image texture atlas bind group"), layout: &self.texture_layout, entries: &[wgpu::BindGroupEntry { @@ -475,6 +477,6 @@ impl Atlas { &self.texture_view, ), }], - }); + })); } } diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs index f3253e63..46caa227 100644 --- a/wgpu/src/image/cache.rs +++ b/wgpu/src/image/cache.rs @@ -8,6 +8,8 @@ use std::collections::HashMap; #[cfg(feature = "image")] use std::sync::mpsc; +use std::sync::Arc; + pub struct Cache { atlas: Atlas, #[cfg(feature = "image")] @@ -127,7 +129,7 @@ impl Cache { encoder: &mut wgpu::CommandEncoder, belt: &mut wgpu::util::StagingBelt, handle: &core::image::Handle, - ) -> Option<(&atlas::Entry, &wgpu::BindGroup)> { + ) -> Option<(&atlas::Entry, &Arc)> { use crate::image::raster::Memory; self.receive(); @@ -198,9 +200,9 @@ impl Cache { belt: &mut wgpu::util::StagingBelt, handle: &core::svg::Handle, color: Option, - size: [f32; 2], + size: Size, scale: f32, - ) -> Option<(&atlas::Entry, &wgpu::BindGroup)> { + ) -> Option<(&atlas::Entry, &Arc)> { // TODO: Concurrency self.vector .upload( @@ -321,6 +323,7 @@ fn load_image<'a>( } #[cfg(feature = "image")] +#[derive(Debug)] enum Job { Load(core::image::Handle), Upload { @@ -329,7 +332,7 @@ enum Job { width: u32, height: u32, }, - Drop(wgpu::BindGroup), + Drop(Arc), } #[cfg(feature = "image")] @@ -337,7 +340,7 @@ enum Work { Upload { handle: core::image::Handle, entry: atlas::Entry, - bind_group: wgpu::BindGroup, + bind_group: Arc, }, Error { handle: core::image::Handle, diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index d530d9b4..d1e39c92 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -10,12 +10,14 @@ mod raster; mod vector; use crate::Buffer; +use crate::core::border; use crate::core::{Rectangle, Size, Transformation}; use crate::graphics::Shell; use bytemuck::{Pod, Zeroable}; use std::mem; +use std::sync::Arc; pub use crate::graphics::Image; @@ -131,24 +133,26 @@ impl Pipeline { array_stride: mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, // Center - 1 => Float32x2, - // Scale - 2 => Float32x2, + 0 => Float32x2, + // Clip bounds + 1 => Float32x4, + // Border radius + 2 => Float32x4, + // Tile + 3 => Float32x4, // Rotation - 3 => Float32, - // Opacity 4 => Float32, + // Opacity + 5 => Float32, // Atlas position - 5 => Float32x2, - // Atlas scale 6 => Float32x2, + // Atlas scale + 7 => Float32x2, // Layer - 7 => Sint32, + 8 => Sint32, // Snap - 8 => Uint32, + 9 => Uint32, ), }], compilation_options: @@ -221,6 +225,8 @@ impl Pipeline { pub struct State { layers: Vec, prepare_layer: usize, + nearest_instances: Vec, + linear_instances: Vec, } impl State { @@ -249,11 +255,8 @@ impl State { } let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, encoder, belt, transformation, scale); - let mut atlas = None; - let nearest_instances = &mut Vec::new(); - let linear_instances = &mut Vec::new(); + let mut atlas: Option> = None; for image in images { match &image { @@ -268,34 +271,30 @@ impl State { } Some(atlas) if atlas != bind_group => { layer.push( - device, - encoder, - belt, atlas, - nearest_instances, - linear_instances, + &self.nearest_instances, + &self.linear_instances, ); - *atlas = bind_group.clone(); - nearest_instances.clear(); - linear_instances.clear(); + *atlas = Arc::clone(bind_group); } _ => {} } add_instances( - [bounds.x, bounds.y], - [bounds.width, bounds.height], + *bounds, + image.clip_bounds, + image.border_radius, f32::from(image.rotation), image.opacity, image.snap, atlas_entry, match image.filter_method { crate::core::image::FilterMethod::Nearest => { - nearest_instances + &mut self.nearest_instances } crate::core::image::FilterMethod::Linear => { - linear_instances + &mut self.linear_instances } }, ); @@ -306,8 +305,6 @@ impl State { #[cfg(feature = "svg")] Image::Vector(svg, bounds) => { - let size = [bounds.width, bounds.height]; - if let Some((atlas_entry, bind_group)) = cache .upload_vector( device, @@ -315,7 +312,7 @@ impl State { belt, &svg.handle, svg.color, - size, + bounds.size(), scale, ) { @@ -325,29 +322,25 @@ impl State { } Some(atlas) if atlas != bind_group => { layer.push( - device, - encoder, - belt, atlas, - nearest_instances, - linear_instances, + &self.nearest_instances, + &self.linear_instances, ); *atlas = bind_group.clone(); - nearest_instances.clear(); - linear_instances.clear(); } _ => {} } add_instances( - [bounds.x, bounds.y], - size, + *bounds, + Rectangle::INFINITE, + border::radius(0), f32::from(svg.rotation), svg.opacity, true, atlas_entry, - nearest_instances, + &mut self.nearest_instances, ); } } @@ -356,18 +349,23 @@ impl State { } } - if !nearest_instances.is_empty() || !linear_instances.is_empty() { - layer.push( - device, - encoder, - belt, - &atlas.expect("atlas should be defined"), - nearest_instances, - linear_instances, - ); + if let Some(atlas) = &atlas { + layer.push(atlas, &self.nearest_instances, &self.linear_instances); } + layer.prepare( + device, + encoder, + belt, + transformation, + scale, + &self.nearest_instances, + &self.linear_instances, + ); + self.prepare_layer += 1; + self.nearest_instances.clear(); + self.linear_instances.clear(); } pub fn render<'a>( @@ -404,16 +402,17 @@ impl State { struct Layer { uniforms: wgpu::Buffer, instances: Buffer, - total: usize, nearest: Vec, nearest_layout: wgpu::BindGroup, + nearest_total: usize, linear: Vec, linear_layout: wgpu::BindGroup, + linear_total: usize, } #[derive(Debug)] struct Group { - atlas: wgpu::BindGroup, + atlas: Arc, instance_count: usize, } @@ -489,11 +488,12 @@ impl Layer { Self { uniforms, instances, - total: 0, nearest: Vec::new(), nearest_layout, + nearest_total: 0, linear: Vec::new(), linear_layout, + linear_total: 0, } } @@ -504,6 +504,8 @@ impl Layer { belt: &mut wgpu::util::StagingBelt, transformation: Transformation, scale_factor: f32, + nearest: &[Instance], + linear: &[Instance], ) { let uniforms = Uniforms { transform: transformation.into(), @@ -521,41 +523,48 @@ impl Layer { device, ) .copy_from_slice(bytes); + + let _ = self + .instances + .resize(device, self.nearest_total + self.linear_total); + + let mut offset = 0; + + if !nearest.is_empty() { + offset += self.instances.write(device, encoder, belt, 0, nearest); + } + + if !linear.is_empty() { + let _ = self.instances.write(device, encoder, belt, offset, linear); + } } fn push( &mut self, - device: &wgpu::Device, - encoder: &mut wgpu::CommandEncoder, - belt: &mut wgpu::util::StagingBelt, - atlas: &wgpu::BindGroup, + atlas: &Arc, nearest: &[Instance], linear: &[Instance], ) { - let new = nearest.len() + linear.len(); - - let _ = self.instances.resize(device, self.total + new); - - if !nearest.is_empty() { - self.total += self - .instances - .write(device, encoder, belt, self.total, nearest); + let new_nearest = nearest.len() - self.nearest_total; + if new_nearest > 0 { self.nearest.push(Group { atlas: atlas.clone(), - instance_count: nearest.len(), + instance_count: new_nearest, }); + + self.nearest_total = nearest.len(); } - if !linear.is_empty() { - self.total += self - .instances - .write(device, encoder, belt, self.total, linear); + let new_linear = linear.len() - self.linear_total; + if new_linear > 0 { self.linear.push(Group { atlas: atlas.clone(), - instance_count: linear.len(), + instance_count: new_linear, }); + + self.linear_total = linear.len(); } } @@ -568,7 +577,7 @@ impl Layer { render_pass.set_bind_group(0, &self.nearest_layout, &[]); for group in &self.nearest { - render_pass.set_bind_group(1, &group.atlas, &[]); + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); render_pass .draw(0..6, offset..offset + group.instance_count as u32); @@ -580,7 +589,7 @@ impl Layer { render_pass.set_bind_group(0, &self.linear_layout, &[]); for group in &self.linear { - render_pass.set_bind_group(1, &group.atlas, &[]); + render_pass.set_bind_group(1, group.atlas.as_ref(), &[]); render_pass .draw(0..6, offset..offset + group.instance_count as u32); @@ -590,19 +599,21 @@ impl Layer { } fn clear(&mut self) { - self.instances.clear(); self.nearest.clear(); + self.nearest_total = 0; + self.linear.clear(); - self.total = 0; + self.linear_total = 0; } } #[repr(C)] #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { - _position: [f32; 2], _center: [f32; 2], - _size: [f32; 2], + _clip_bounds: [f32; 4], + _border_radius: [f32; 4], + _tile: [f32; 4], _rotation: f32, _opacity: f32, _position_in_atlas: [f32; 2], @@ -626,8 +637,9 @@ struct Uniforms { } fn add_instances( - image_position: [f32; 2], - image_size: [f32; 2], + bounds: Rectangle, + clip_bounds: Rectangle, + border_radius: border::Radius, rotation: f32, opacity: f32, snap: bool, @@ -635,16 +647,26 @@ fn add_instances( instances: &mut Vec, ) { let center = [ - image_position[0] + image_size[0] / 2.0, - image_position[1] + image_size[1] / 2.0, + bounds.x + bounds.width / 2.0, + bounds.y + bounds.height / 2.0, ]; + let clip_bounds = [ + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ]; + + let border_radius = border_radius.into(); + match entry { atlas::Entry::Contiguous(allocation) => { add_instance( - image_position, center, - image_size, + clip_bounds, + border_radius, + [bounds.x, bounds.y, bounds.width, bounds.height], rotation, opacity, snap, @@ -653,32 +675,35 @@ fn add_instances( ); } atlas::Entry::Fragmented { fragments, size } => { - let scaling_x = image_size[0] / size.width as f32; - let scaling_y = image_size[1] / size.height as f32; + let scaling_x = bounds.width / size.width as f32; + let scaling_y = bounds.height / size.height as f32; for fragment in fragments { let allocation = &fragment.allocation; - - let [x, y] = image_position; let (fragment_x, fragment_y) = fragment.position; + let Size { width: fragment_width, height: fragment_height, } = allocation.size(); - let position = [ - x + fragment_x as f32 * scaling_x, - y + fragment_y as f32 * scaling_y, - ]; - - let size = [ + let tile = [ + bounds.x + fragment_x as f32 * scaling_x, + bounds.y + fragment_y as f32 * scaling_y, fragment_width as f32 * scaling_x, fragment_height as f32 * scaling_y, ]; add_instance( - position, center, size, rotation, opacity, snap, - allocation, instances, + center, + clip_bounds, + border_radius, + tile, + rotation, + opacity, + snap, + allocation, + instances, ); } } @@ -687,9 +712,10 @@ fn add_instances( #[inline] fn add_instance( - position: [f32; 2], center: [f32; 2], - size: [f32; 2], + clip_bounds: [f32; 4], + border_radius: [f32; 4], + tile: [f32; 4], rotation: f32, opacity: f32, snap: bool, @@ -702,9 +728,10 @@ fn add_instance( let atlas_size = allocation.atlas_size(); let instance = Instance { - _position: position, _center: center, - _size: size, + _clip_bounds: clip_bounds, + _border_radius: border_radius, + _tile: tile, _rotation: rotation, _opacity: opacity, _position_in_atlas: [ diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 5d54196e..0cccad06 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -5,7 +5,7 @@ use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; use rustc_hash::{FxHashMap, FxHashSet}; -use std::sync::Weak; +use std::sync::{Arc, Weak}; type Image = image_rs::ImageBuffer, image::Bytes>; @@ -17,7 +17,7 @@ pub enum Memory { /// Storage entry Device { entry: atlas::Entry, - bind_group: Option, + bind_group: Option>, allocation: Option>, }, /// Image not found @@ -90,7 +90,7 @@ impl Cache { pub fn trim( &mut self, atlas: &mut Atlas, - on_drop: impl Fn(wgpu::BindGroup), + on_drop: impl Fn(Arc), ) { // Only trim if new entries have landed in the `Cache` if !self.should_trim { diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 2fa6ba39..31fb2fa6 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -97,15 +97,15 @@ impl Cache { belt: &mut wgpu::util::StagingBelt, handle: &svg::Handle, color: Option, - [width, height]: [f32; 2], + size: Size, scale: f32, atlas: &mut Atlas, ) -> Option<&atlas::Entry> { let id = handle.id(); let (width, height) = ( - (scale * width).ceil() as u32, - (scale * height).ceil() as u32, + (scale * size.width).ceil() as u32, + (scale * size.height).ceil() as u32, ); let color = color.map(Color::into_rgba8); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 5ddb8461..7a0e57b8 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -143,7 +143,13 @@ impl Layer { bounds: Rectangle, transformation: Transformation, ) { - let image = Image::Raster(image, bounds * transformation); + let image = Image::Raster( + core::Image { + clip_bounds: image.clip_bounds * transformation, + ..image + }, + bounds * transformation, + ); self.images.push(image); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6991564f..f78998bb 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -311,8 +311,10 @@ impl Renderer { self.layers.merge(); for layer in self.layers.iter() { + let clip_bounds = layer.bounds * scale_factor; + if physical_bounds - .intersection(&(layer.bounds * scale_factor)) + .intersection(&clip_bounds) .and_then(Rectangle::snap) .is_none() { diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index bc922838..b2754d61 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,22 +9,25 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, - @location(0) pos: vec2, - @location(1) center: vec2, - @location(2) scale: vec2, - @location(3) rotation: f32, - @location(4) opacity: f32, - @location(5) atlas_pos: vec2, - @location(6) atlas_scale: vec2, - @location(7) layer: i32, - @location(8) snap: u32, + @location(0) center: vec2, + @location(1) clip_bounds: vec4, + @location(2) border_radius: vec4, + @location(3) tile: vec4, + @location(4) rotation: f32, + @location(5) opacity: f32, + @location(6) atlas_pos: vec2, + @location(7) atlas_scale: vec2, + @location(8) layer: i32, + @location(9) snap: u32, } struct VertexOutput { @builtin(position) position: vec4, - @location(0) uv: vec2, - @location(1) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. - @location(2) opacity: f32, + @location(0) clip_bounds: vec4, + @location(1) border_radius: vec4, + @location(2) uv: vec2, + @location(3) layer: f32, // this should be an i32, but naga currently reads that as requiring interpolation. + @location(4) opacity: f32, } @vertex @@ -39,8 +42,11 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.layer = f32(input.layer); out.opacity = input.opacity; + let tile = input.tile; + let center = input.center; + // Calculate the vertex position and move the center to the origin - v_pos = input.pos + v_pos * input.scale - input.center; + v_pos = tile.xy + v_pos * tile.zw - center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -53,19 +59,39 @@ fn vs_main(input: VertexInput) -> VertexOutput { ); // Calculate the final position of the vertex - out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4(input.center, 0.0, 0.0) + rotate * vec4(v_pos, 0.0, 1.0)); + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4(center, 0.0, 0.0) + rotate * vec4(v_pos, 0.0, 1.0)); if bool(input.snap) { out.position = round(out.position); } out.position = globals.transform * out.position; + out.clip_bounds = globals.scale_factor * input.clip_bounds; + out.border_radius = globals.scale_factor * input.border_radius; return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { - // Sample the texture at the given UV coordinate and layer. - return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4(1.0, 1.0, 1.0, input.opacity); + let fragment = input.position.xy; + let position = input.clip_bounds.xy; + let scale = input.clip_bounds.zw; + + let d = rounded_box_sdf( + 2.0 * (fragment - position - scale / 2.0), + scale, + input.border_radius * 2.0, + ); + + let antialias: f32 = clamp(0.5 - d, 0.0, 1.0); + + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4(1.0, 1.0, 1.0, antialias * input.opacity); +} + +fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 { + var box_half = select(corners.yz, corners.xw, p.x > 0.0); + var corner = select(box_half.y, box_half.x, p.y > 0.0); + var q = abs(p) - size + corner; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner; } diff --git a/widget/src/image.rs b/widget/src/image.rs index 18b7a3a3..709915aa 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -19,6 +19,7 @@ pub mod viewer; pub use viewer::Viewer; +use crate::core::border; use crate::core::image; use crate::core::layout; use crate::core::mouse; @@ -59,6 +60,7 @@ pub struct Image { width: Length, height: Length, crop: Option>, + border_radius: border::Radius, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, @@ -75,6 +77,7 @@ impl Image { width: Length::Shrink, height: Length::Shrink, crop: None, + border_radius: border::Radius::default(), content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), @@ -164,6 +167,18 @@ impl Image { self.crop = Some(region); self } + + /// Sets the [`border::Radius`] of the [`Image`]. + /// + /// Currently, it will only be applied around the rectangular bounding box + /// of the [`Image`]. + pub fn border_radius( + mut self, + border_radius: impl Into, + ) -> Self { + self.border_radius = border_radius.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -281,10 +296,6 @@ where Rectangle::new(position + crop_offset, final_size) } -fn must_clip(bounds: Rectangle, drawing_bounds: Rectangle) -> bool { - drawing_bounds.width > bounds.width || drawing_bounds.height > bounds.height -} - fn crop(size: Size, region: Option>) -> Size { if let Some(region) = region { Size::new( @@ -300,9 +311,9 @@ fn crop(size: Size, region: Option>) -> Size { pub fn draw( renderer: &mut Renderer, layout: Layout<'_>, - viewport: &Rectangle, handle: &Handle, crop: Option>, + border_radius: border::Radius, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, @@ -323,45 +334,11 @@ pub fn draw( scale, ); - if must_clip(bounds, drawing_bounds) { - if let Some(bounds) = bounds.intersection(viewport) { - renderer.with_layer(bounds, |renderer| { - render( - renderer, - handle, - filter_method, - rotation, - opacity, - drawing_bounds, - ); - }); - } - } else { - render( - renderer, - handle, - filter_method, - rotation, - opacity, - drawing_bounds, - ); - } -} - -fn render( - renderer: &mut Renderer, - handle: &Handle, - filter_method: FilterMethod, - rotation: Rotation, - opacity: f32, - drawing_bounds: Rectangle, -) where - Renderer: image::Renderer, - Handle: Clone, -{ renderer.draw_image( image::Image { handle: handle.clone(), + clip_bounds: bounds, + border_radius, filter_method, rotation: rotation.radians(), opacity, @@ -411,14 +388,14 @@ where _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, - viewport: &Rectangle, + _viewport: &Rectangle, ) { draw( renderer, layout, - viewport, &self.handle, self.crop, + self.border_radius, self.content_fit, self.filter_method, self.rotation, diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 29da5d6d..e9b153e5 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,4 +1,5 @@ //! Zoom and pan on an image. +use crate::core::border; use crate::core::image::{self, FilterMethod}; use crate::core::layout; use crate::core::mouse; @@ -347,6 +348,8 @@ where renderer.draw_image( Image { handle: self.handle.clone(), + clip_bounds: Rectangle::INFINITE, + border_radius: border::Radius::default(), filter_method: self.filter_method, rotation: Radians(0.0), opacity: 1.0,