diff --git a/core/src/image.rs b/core/src/image.rs index 91a7fd36ac..82ecdd0f59 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -174,5 +174,6 @@ pub trait Renderer: crate::Renderer { filter_method: FilterMethod, bounds: Rectangle, rotation: Radians, + opacity: f32, ); } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 91519b4072..e8709dbc93 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -173,6 +173,7 @@ impl image::Renderer for () { _filter_method: image::FilterMethod, _bounds: Rectangle, _rotation: Radians, + _opacity: f32, ) { } } @@ -188,6 +189,7 @@ impl svg::Renderer for () { _color: Option, _bounds: Rectangle, _rotation: Radians, + _opacity: f32, ) { } } diff --git a/core/src/svg.rs b/core/src/svg.rs index 01f102e320..946b8156b1 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -101,5 +101,6 @@ pub trait Renderer: crate::Renderer { color: Option, bounds: Rectangle, rotation: Radians, + opacity: f32, ); } diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 13d746dd8c..0400c3765d 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -17,6 +17,7 @@ pub fn main() -> iced::Result { struct Image { width: f32, + opacity: f32, rotation: Rotation, content_fit: ContentFit, spin: bool, @@ -26,6 +27,7 @@ struct Image { #[derive(Debug, Clone, Copy)] enum Message { WidthChanged(f32), + OpacityChanged(f32), RotationStrategyChanged(RotationStrategy), RotationChanged(Degrees), ContentFitChanged(ContentFit), @@ -39,6 +41,9 @@ impl Image { Message::WidthChanged(width) => { self.width = width; } + Message::OpacityChanged(opacity) => { + self.opacity = opacity; + } Message::RotationStrategyChanged(strategy) => { self.rotation = match strategy { RotationStrategy::Floating => { @@ -97,6 +102,7 @@ impl Image { .width(self.width) .content_fit(self.content_fit) .rotation(self.rotation) + .opacity(self.opacity) ) .explain(Color::WHITE), "I am Ferris!" @@ -104,7 +110,7 @@ impl Image { .spacing(20) .align_items(Alignment::Center); - let sizing = row![ + let fit = row![ pick_list( [ ContentFit::Contain, @@ -117,19 +123,6 @@ impl Image { Message::ContentFitChanged ) .width(Length::Fill), - column![ - slider(100.0..=500.0, self.width, Message::WidthChanged), - text(format!("Width: {}px", self.width)) - .size(12) - .line_height(1.0) - ] - .spacing(2) - .align_items(Alignment::Center) - ] - .spacing(10) - .align_items(Alignment::End); - - let rotation = row![ pick_list( [RotationStrategy::Floating, RotationStrategy::Solid], Some(match self.rotation { @@ -139,7 +132,21 @@ impl Image { Message::RotationStrategyChanged, ) .width(Length::Fill), - column![ + ] + .spacing(10) + .align_items(Alignment::End); + + let properties = row![ + with_value( + slider(100.0..=500.0, self.width, Message::WidthChanged), + format!("Width: {}px", self.width) + ), + with_value( + slider(0.0..=1.0, self.opacity, Message::OpacityChanged) + .step(0.01), + format!("Opacity: {:.2}", self.opacity) + ), + with_value( row![ slider( Degrees::RANGE, @@ -153,20 +160,13 @@ impl Image { ] .spacing(10) .align_items(Alignment::Center), - text(format!( - "Rotation: {:.0}°", - f32::from(self.rotation.degrees()) - )) - .size(12) - .line_height(1.0) - ] - .spacing(2) - .align_items(Alignment::Center) + format!("Rotation: {:.0}°", f32::from(self.rotation.degrees())) + ) ] .spacing(10) .align_items(Alignment::End); - container(column![center(i_am_ferris), sizing, rotation].spacing(10)) + container(column![fit, center(i_am_ferris), properties].spacing(10)) .padding(10) .into() } @@ -176,6 +176,7 @@ impl Default for Image { fn default() -> Self { Self { width: 300.0, + opacity: 1.0, rotation: Rotation::default(), content_fit: ContentFit::default(), spin: false, @@ -198,3 +199,13 @@ impl std::fmt::Display for RotationStrategy { }) } } + +fn with_value<'a>( + control: impl Into>, + value: String, +) -> Element<'a, Message> { + column![control.into(), text(value).size(12).line_height(1.0)] + .spacing(2) + .align_items(Alignment::Center) + .into() +} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 9d09bf4c56..318592bec1 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -18,8 +18,11 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, - /// The rotation of the image in radians + /// The rotation of the image. rotation: Radians, + + /// The opacity of the image. + opacity: f32, }, /// A vector image. Vector { @@ -32,8 +35,11 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, - /// The rotation of the image in radians + /// The rotation of the image. rotation: Radians, + + /// The opacity of the image. + opacity: f32, }, } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index a077031bc5..6a169692db 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -155,11 +155,18 @@ where filter_method: image::FilterMethod, bounds: Rectangle, rotation: Radians, + opacity: f32, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds, rotation) + renderer.draw_image( + handle, + filter_method, + bounds, + rotation, + opacity + ) ); } } @@ -179,11 +186,12 @@ where color: Option, bounds: Rectangle, rotation: Radians, + opacity: f32, ) { delegate!( self, renderer, - renderer.draw_svg(handle, color, bounds, rotation) + renderer.draw_svg(handle, color, bounds, rotation, opacity) ); } } diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 544ff61407..028b304fb3 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -551,6 +551,7 @@ impl Engine { filter_method, bounds, rotation, + opacity, } => { let physical_bounds = *bounds * _transformation; @@ -574,6 +575,7 @@ impl Engine { handle, *filter_method, *bounds, + *opacity, _pixels, transform, clip_mask, @@ -585,6 +587,7 @@ impl Engine { color, bounds, rotation, + opacity, } => { let physical_bounds = *bounds * _transformation; @@ -608,6 +611,7 @@ impl Engine { handle, *color, physical_bounds, + *opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index c907c93c2c..48fca1d80e 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -122,12 +122,14 @@ impl Layer { bounds: Rectangle, transformation: Transformation, rotation: Radians, + opacity: f32, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, + opacity, }; self.images.push(image); @@ -140,12 +142,14 @@ impl Layer { bounds: Rectangle, transformation: Transformation, rotation: Radians, + opacity: f32, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, + opacity, }; self.images.push(svg); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index e0cbfa0d0c..1aabff00c0 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -378,6 +378,7 @@ impl core::image::Renderer for Renderer { filter_method: core::image::FilterMethod, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -386,6 +387,7 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, + opacity, ); } } @@ -405,9 +407,17 @@ impl core::svg::Renderer for Renderer { color: Option, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation, rotation); + layer.draw_svg( + handle, + color, + bounds, + transformation, + rotation, + opacity, + ); } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 907fce7c40..c40f55b2ff 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -31,6 +31,7 @@ impl Pipeline { handle: &raster::Handle, filter_method: raster::FilterMethod, bounds: Rectangle, + opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, @@ -56,6 +57,7 @@ impl Pipeline { image, &tiny_skia::PixmapPaint { quality, + opacity, ..Default::default() }, transform, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 8e3463f26a..bbe08cb806 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -34,6 +34,7 @@ impl Pipeline { handle: &Handle, color: Option, bounds: Rectangle, + opacity: f32, pixels: &mut tiny_skia::PixmapMut<'_>, transform: Transform, clip_mask: Option<&tiny_skia::Mask>, @@ -47,7 +48,10 @@ impl Pipeline { bounds.x as i32, bounds.y as i32, image, - &tiny_skia::PixmapPaint::default(), + &tiny_skia::PixmapPaint { + opacity, + ..tiny_skia::PixmapPaint::default() + }, transform, clip_mask, ); diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 285eb2f6e7..063822aa82 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -137,16 +137,18 @@ impl Pipeline { 0 => Float32x2, // Center 1 => Float32x2, - // Image size + // Scale 2 => Float32x2, // Rotation 3 => Float32, + // Opacity + 4 => Float32, // Atlas position - 4 => Float32x2, - // Atlas scale 5 => Float32x2, + // Atlas scale + 6 => Float32x2, // Layer - 6 => Sint32, + 7 => Sint32, ), }], }, @@ -229,6 +231,7 @@ impl Pipeline { filter_method, bounds, rotation, + opacity, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -237,6 +240,7 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], f32::from(*rotation), + *opacity, atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -258,6 +262,7 @@ impl Pipeline { color, bounds, rotation, + opacity, } => { let size = [bounds.width, bounds.height]; @@ -268,6 +273,7 @@ impl Pipeline { [bounds.x, bounds.y], size, f32::from(*rotation), + *opacity, atlas_entry, nearest_instances, ); @@ -498,6 +504,7 @@ struct Instance { _center: [f32; 2], _size: [f32; 2], _rotation: f32, + _opacity: f32, _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -517,6 +524,7 @@ fn add_instances( image_position: [f32; 2], image_size: [f32; 2], rotation: f32, + opacity: f32, entry: &atlas::Entry, instances: &mut Vec, ) { @@ -532,6 +540,7 @@ fn add_instances( center, image_size, rotation, + opacity, allocation, instances, ); @@ -561,7 +570,8 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, allocation, instances, + position, center, size, rotation, opacity, allocation, + instances, ); } } @@ -574,6 +584,7 @@ fn add_instance( center: [f32; 2], size: [f32; 2], rotation: f32, + opacity: f32, allocation: &atlas::Allocation, instances: &mut Vec, ) { @@ -586,6 +597,7 @@ fn add_instance( _center: center, _size: size, _rotation: rotation, + _opacity: opacity, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index e0242c5938..9551311d3f 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -119,12 +119,14 @@ impl Layer { bounds: Rectangle, transformation: Transformation, rotation: Radians, + opacity: f32, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, rotation, + opacity, }; self.images.push(image); @@ -137,12 +139,14 @@ impl Layer { bounds: Rectangle, transformation: Transformation, rotation: Radians, + opacity: f32, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, rotation, + opacity, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index eb600dde77..4c16802996 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -518,6 +518,7 @@ impl core::image::Renderer for Renderer { filter_method: core::image::FilterMethod, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); layer.draw_image( @@ -526,6 +527,7 @@ impl core::image::Renderer for Renderer { bounds, transformation, rotation, + opacity, ); } } @@ -542,9 +544,17 @@ impl core::svg::Renderer for Renderer { color_filter: Option, bounds: Rectangle, rotation: core::Radians, + opacity: f32, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color_filter, bounds, transformation, rotation); + layer.draw_svg( + handle, + color_filter, + bounds, + transformation, + rotation, + opacity, + ); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 71bf939c4b..accefc1797 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -12,15 +12,17 @@ struct VertexInput { @location(1) center: vec2, @location(2) scale: vec2, @location(3) rotation: f32, - @location(4) atlas_pos: vec2, - @location(5) atlas_scale: vec2, - @location(6) layer: i32, + @location(4) opacity: f32, + @location(5) atlas_pos: vec2, + @location(6) atlas_scale: vec2, + @location(7) layer: i32, } 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, } @vertex @@ -33,6 +35,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { // Map the vertex position to the atlas texture. out.uv = vec2(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); + out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin v_pos = input.pos + v_pos * input.scale - input.center; @@ -56,5 +59,5 @@ fn vs_main(input: VertexInput) -> VertexOutput { @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)); + return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)) * vec4(1.0, 1.0, 1.0, input.opacity); } diff --git a/widget/src/image.rs b/widget/src/image.rs index 45209a9107..80e17263b2 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -38,6 +38,7 @@ pub struct Image { content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, } impl Image { @@ -50,6 +51,7 @@ impl Image { content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -84,6 +86,15 @@ impl Image { self.rotation = rotation.into(); self } + + /// Sets the opacity of the [`Image`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into) -> Self { + self.opacity = opacity.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -136,6 +147,7 @@ pub fn draw( content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, + opacity: f32, ) where Renderer: image::Renderer, Handle: Clone, @@ -173,6 +185,7 @@ pub fn draw( filter_method, drawing_bounds, rotation.radians(), + opacity, ); }; @@ -231,6 +244,7 @@ where self.content_fit, self.filter_method, self.rotation, + self.opacity, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5eb76452ca..8fe6f02109 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -342,6 +342,7 @@ where ..Rectangle::with_size(image_size) }, Radians(0.0), + 1.0, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index c1fccba1b8..4551bcadcd 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -30,6 +30,7 @@ where content_fit: ContentFit, class: Theme::Class<'a>, rotation: Rotation, + opacity: f32, } impl<'a, Theme> Svg<'a, Theme> @@ -45,6 +46,7 @@ where content_fit: ContentFit::Contain, class: Theme::default(), rotation: Rotation::default(), + opacity: 1.0, } } @@ -103,6 +105,15 @@ where self.rotation = rotation.into(); self } + + /// Sets the opacity of the [`Svg`]. + /// + /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent, + /// and `1.0` meaning completely opaque. + pub fn opacity(mut self, opacity: impl Into) -> Self { + self.opacity = opacity.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -204,6 +215,7 @@ where style.color, drawing_bounds, self.rotation.radians(), + self.opacity, ); };