diff --git a/core/src/color.rs b/core/src/color.rs index 212c12148a..fe0a18569b 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,17 @@ impl Color { } } + /// Converts the [`Color`] into its RGBA8 equivalent. + #[must_use] + pub fn into_rgba8(self) -> [u8; 4] { + [ + (self.r * 255.0).round() as u8, + (self.g * 255.0).round() as u8, + (self.b * 255.0).round() as u8, + (self.a * 255.0).round() as u8, + ] + } + /// Converts the [`Color`] into its linear values. pub fn into_linear(self) -> [f32; 4] { // As described in: @@ -148,24 +159,26 @@ impl From<[f32; 4]> for Color { #[macro_export] macro_rules! color { ($r:expr, $g:expr, $b:expr) => { - Color::from_rgb8($r, $g, $b) + $crate::Color::from_rgb8($r, $g, $b) }; ($r:expr, $g:expr, $b:expr, $a:expr) => { - Color::from_rgba8($r, $g, $b, $a) + $crate::Color::from_rgba8($r, $g, $b, $a) }; ($hex:expr) => {{ let hex = $hex as u32; let r = (hex & 0xff0000) >> 16; let g = (hex & 0xff00) >> 8; let b = (hex & 0xff); - Color::from_rgb8(r as u8, g as u8, b as u8) + + $crate::Color::from_rgb8(r as u8, g as u8, b as u8) }}; ($hex:expr, $a:expr) => {{ let hex = $hex as u32; let r = (hex & 0xff0000) >> 16; let g = (hex & 0xff00) >> 8; let b = (hex & 0xff); - Color::from_rgba8(r as u8, g as u8, b as u8, $a) + + $crate::Color::from_rgba8(r as u8, g as u8, b as u8, $a) }}; } diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 27d175da7d..4dc92416f1 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,39 +1,76 @@ -use iced::widget::{container, svg}; -use iced::{Element, Length, Sandbox, Settings}; +use iced::theme; +use iced::widget::{checkbox, column, container, svg}; +use iced::{color, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { Tiger::run(Settings::default()) } -struct Tiger; +#[derive(Debug, Default)] +struct Tiger { + apply_color_filter: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + ToggleColorFilter(bool), +} impl Sandbox for Tiger { - type Message = (); + type Message = Message; fn new() -> Self { - Tiger + Tiger::default() } fn title(&self) -> String { String::from("SVG - Iced") } - fn update(&mut self, _message: ()) {} + fn update(&mut self, message: Self::Message) { + match message { + Message::ToggleColorFilter(apply_color_filter) => { + self.apply_color_filter = apply_color_filter; + } + } + } - fn view(&self) -> Element<()> { - let svg = svg(svg::Handle::from_path(format!( + fn view(&self) -> Element { + let handle = svg::Handle::from_path(format!( "{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR") - ))) - .width(Length::Fill) - .height(Length::Fill); + )); + + let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( + if self.apply_color_filter { + theme::Svg::custom_fn(|_theme| svg::Appearance { + color: Some(color!(0x0000ff)), + }) + } else { + theme::Svg::Default + }, + ); - container(svg) + let apply_color_filter = checkbox( + "Apply a color filter", + self.apply_color_filter, + Message::ToggleColorFilter, + ); + + container( + column![ + svg, + container(apply_color_filter).width(Length::Fill).center_x() + ] + .spacing(20) .width(Length::Fill) - .height(Length::Fill) - .padding(20) - .center_x() - .center_y() - .into() + .height(Length::Fill), + ) + .width(Length::Fill) + .height(Length::Fill) + .padding(20) + .center_x() + .center_y() + .into() } } diff --git a/glow/src/image.rs b/glow/src/image.rs index 955fd1abfb..c32b216291 100644 --- a/glow/src/image.rs +++ b/glow/src/image.rs @@ -172,11 +172,16 @@ impl Pipeline { layer::Image::Raster { handle: _, bounds } => (None, bounds), #[cfg(feature = "svg")] - layer::Image::Vector { handle, bounds } => { + layer::Image::Vector { + handle, + color, + bounds, + } => { let size = [bounds.width, bounds.height]; ( vector_cache.upload( handle, + *color, size, _scale_factor, &mut gl, diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index 42f4b50005..82d77aff64 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -1,5 +1,6 @@ //! Vector image loading and caching use crate::image::Storage; +use crate::Color; use iced_native::svg; use iced_native::Size; @@ -33,11 +34,13 @@ impl Svg { #[derive(Debug)] pub struct Cache { svgs: HashMap, - rasterized: HashMap<(u64, u32, u32), T::Entry>, + rasterized: HashMap<(u64, u32, u32, ColorFilter), T::Entry>, svg_hits: HashSet, - rasterized_hits: HashSet<(u64, u32, u32)>, + rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>, } +type ColorFilter = Option<[u8; 4]>; + impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { @@ -76,6 +79,7 @@ impl Cache { pub fn upload( &mut self, handle: &svg::Handle, + color: Option, [width, height]: [f32; 2], scale: f32, state: &mut T::State<'_>, @@ -88,15 +92,18 @@ impl Cache { (scale * height).ceil() as u32, ); + let color = color.map(Color::into_rgba8); + let key = (id, width, height, color); + // TODO: Optimize! // We currently rerasterize the SVG when its size changes. This is slow // as heck. A GPU rasterizer like `pathfinder` may perform better. // It would be cool to be able to smooth resize the `svg` example. - if self.rasterized.contains_key(&(id, width, height)) { + if self.rasterized.contains_key(&key) { let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized_hits.insert(key); - return self.rasterized.get(&(id, width, height)); + return self.rasterized.get(&key); } match self.load(handle) { @@ -121,15 +128,26 @@ impl Cache { img.as_mut(), )?; - let allocation = - storage.upload(width, height, img.data(), state)?; + let mut rgba = img.take(); + + if let Some(color) = color { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + rgba[0] = color[0]; + rgba[1] = color[1]; + rgba[2] = color[2]; + } + }); + } + + let allocation = storage.upload(width, height, &rgba, state)?; log::debug!("allocating {} {}x{}", id, width, height); let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert((id, width, height)); - let _ = self.rasterized.insert((id, width, height), allocation); + let _ = self.rasterized_hits.insert(key); + let _ = self.rasterized.insert(key, allocation); - self.rasterized.get(&(id, width, height)) + self.rasterized.get(&key) } Svg::NotFound => None, } diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index fd670f4833..1d453caa39 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -251,11 +251,16 @@ impl<'a> Layer<'a> { bounds: *bounds + translation, }); } - Primitive::Svg { handle, bounds } => { + Primitive::Svg { + handle, + color, + bounds, + } => { let layer = &mut layers[current_layer]; layer.images.push(Image::Vector { handle: handle.clone(), + color: *color, bounds: *bounds + translation, }); } diff --git a/graphics/src/layer/image.rs b/graphics/src/layer/image.rs index 045ec665f4..3eff239779 100644 --- a/graphics/src/layer/image.rs +++ b/graphics/src/layer/image.rs @@ -1,4 +1,5 @@ -use crate::Rectangle; +use crate::{Color, Rectangle}; + use iced_native::{image, svg}; /// A raster or vector image. @@ -17,6 +18,9 @@ pub enum Image { /// The handle of a vector image. handle: svg::Handle, + /// The [`Color`] filter + color: Option, + /// The bounds of the image. bounds: Rectangle, }, diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 6f1b6f26fc..5a163a2f5f 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -60,6 +60,9 @@ pub enum Primitive { /// The path of the SVG file handle: svg::Handle, + /// The [`Color`] filter + color: Option, + /// The bounds of the viewport bounds: Rectangle, }, diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 65350037b6..aabdf7fc45 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -6,7 +6,7 @@ use iced_native::layout; use iced_native::renderer; use iced_native::svg; use iced_native::text::{self, Text}; -use iced_native::{Background, Element, Font, Point, Rectangle, Size}; +use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; pub use iced_native::renderer::Style; @@ -200,7 +200,16 @@ where self.backend().viewport_dimensions(handle) } - fn draw(&mut self, handle: svg::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Svg { handle, bounds }) + fn draw( + &mut self, + handle: svg::Handle, + color: Option, + bounds: Rectangle, + ) { + self.draw_primitive(Primitive::Svg { + handle, + color, + bounds, + }) } } diff --git a/native/src/svg.rs b/native/src/svg.rs index a8e481d2f7..2168e40937 100644 --- a/native/src/svg.rs +++ b/native/src/svg.rs @@ -1,5 +1,5 @@ //! Load and draw vector graphics. -use crate::{Hasher, Rectangle, Size}; +use crate::{Color, Hasher, Rectangle, Size}; use std::borrow::Cow; use std::hash::{Hash, Hasher as _}; @@ -84,6 +84,6 @@ pub trait Renderer: crate::Renderer { /// Returns the default dimensions of an SVG for the given [`Handle`]. fn dimensions(&self, handle: &Handle) -> Size; - /// Draws an SVG with the given [`Handle`] and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, bounds: Rectangle); + /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); } diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 3bce9e604b..0bde288fca 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -285,6 +285,12 @@ where /// /// [`Svg`]: widget::Svg /// [`Handle`]: widget::svg::Handle -pub fn svg(handle: impl Into) -> widget::Svg { +pub fn svg( + handle: impl Into, +) -> widget::Svg +where + Renderer: crate::svg::Renderer, + Renderer::Theme: widget::svg::StyleSheet, +{ widget::Svg::new(handle) } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 1015ed0ada..f83f5acfe4 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -9,6 +9,7 @@ use crate::{ use std::path::PathBuf; +pub use iced_style::svg::{Appearance, StyleSheet}; pub use svg::Handle; /// A vector graphics image. @@ -17,15 +18,24 @@ pub use svg::Handle; /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. -#[derive(Debug, Clone)] -pub struct Svg { +#[allow(missing_debug_implementations)] +pub struct Svg +where + Renderer: svg::Renderer, + Renderer::Theme: StyleSheet, +{ handle: Handle, width: Length, height: Length, content_fit: ContentFit, + style: ::Style, } -impl Svg { +impl Svg +where + Renderer: svg::Renderer, + Renderer::Theme: StyleSheet, +{ /// Creates a new [`Svg`] from the given [`Handle`]. pub fn new(handle: impl Into) -> Self { Svg { @@ -33,22 +43,26 @@ impl Svg { width: Length::Fill, height: Length::Shrink, content_fit: ContentFit::Contain, + style: Default::default(), } } /// Creates a new [`Svg`] that will display the contents of the file at the /// provided path. + #[must_use] pub fn from_path(path: impl Into) -> Self { Self::new(Handle::from_path(path)) } /// Sets the width of the [`Svg`]. + #[must_use] pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the height of the [`Svg`]. + #[must_use] pub fn height(mut self, height: Length) -> Self { self.height = height; self @@ -57,17 +71,29 @@ impl Svg { /// Sets the [`ContentFit`] of the [`Svg`]. /// /// Defaults to [`ContentFit::Contain`] + #[must_use] pub fn content_fit(self, content_fit: ContentFit) -> Self { Self { content_fit, ..self } } + + /// Sets the style variant of this [`Svg`]. + #[must_use] + pub fn style( + mut self, + style: ::Style, + ) -> Self { + self.style = style; + self + } } -impl Widget for Svg +impl Widget for Svg where Renderer: svg::Renderer, + Renderer::Theme: iced_style::svg::StyleSheet, { fn width(&self) -> Length { self.width @@ -114,7 +140,7 @@ where &self, _state: &Tree, renderer: &mut Renderer, - _theme: &Renderer::Theme, + theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor_position: Point, @@ -138,7 +164,13 @@ where ..bounds }; - renderer.draw(self.handle.clone(), drawing_bounds + offset) + let appearance = theme.appearance(&self.style); + + renderer.draw( + self.handle.clone(), + appearance.color, + drawing_bounds + offset, + ); }; if adjusted_fit.width > bounds.width @@ -146,16 +178,18 @@ where { renderer.with_layer(bounds, render); } else { - render(renderer) + render(renderer); } } } -impl<'a, Message, Renderer> From for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> where - Renderer: svg::Renderer, + Renderer: svg::Renderer + 'a, + Renderer::Theme: iced_style::svg::StyleSheet, { - fn from(icon: Svg) -> Element<'a, Message, Renderer> { + fn from(icon: Svg) -> Element<'a, Message, Renderer> { Element::new(icon) } } diff --git a/src/widget.rs b/src/widget.rs index 7c67a59963..e7df6e4e1f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -194,7 +194,7 @@ pub use iced_graphics::widget::qr_code; pub mod svg { //! Display vector graphics in your application. pub use iced_native::svg::Handle; - pub use iced_native::widget::Svg; + pub use iced_native::widget::svg::{Appearance, StyleSheet, Svg}; } #[cfg(feature = "canvas")] diff --git a/style/src/lib.rs b/style/src/lib.rs index 3242602cd8..59eb1eb8f9 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -32,6 +32,7 @@ pub mod radio; pub mod rule; pub mod scrollable; pub mod slider; +pub mod svg; pub mod text; pub mod text_input; pub mod theme; diff --git a/style/src/svg.rs b/style/src/svg.rs new file mode 100644 index 0000000000..9378c1a728 --- /dev/null +++ b/style/src/svg.rs @@ -0,0 +1,23 @@ +//! Change the appearance of a svg. + +use iced_core::Color; + +/// The appearance of an SVG. +#[derive(Debug, Default, Clone, Copy)] +pub struct Appearance { + /// The [`Color`] filter of an SVG. + /// + /// Useful for coloring a symbolic icon. + /// + /// `None` keeps the original color. + pub color: Option, +} + +/// The stylesheet of a svg. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the [`Appearance`] of the svg. + fn appearance(&self, style: &Self::Style) -> Appearance; +} diff --git a/style/src/theme.rs b/style/src/theme.rs index dde0df5df4..271d9a29f2 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -16,6 +16,7 @@ use crate::radio; use crate::rule; use crate::scrollable; use crate::slider; +use crate::svg; use crate::text; use crate::text_input; use crate::toggler; @@ -823,6 +824,44 @@ impl rule::StyleSheet for fn(&Theme) -> rule::Appearance { } } +/** + * Svg + */ +#[derive(Default)] +pub enum Svg { + /// No filtering to the rendered SVG. + #[default] + Default, + /// A custom style. + Custom(Box>), +} + +impl Svg { + /// Creates a custom [`Svg`] style. + pub fn custom_fn(f: fn(&Theme) -> svg::Appearance) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl svg::StyleSheet for Theme { + type Style = Svg; + + fn appearance(&self, style: &Self::Style) -> svg::Appearance { + match style { + Svg::Default => Default::default(), + Svg::Custom(custom) => custom.appearance(self), + } + } +} + +impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { + type Style = Theme; + + fn appearance(&self, style: &Self::Style) -> svg::Appearance { + (self)(style) + } +} + /// The style of a scrollable. #[derive(Default)] pub enum Scrollable { diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index d06815bb9d..390bad90df 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -318,11 +318,16 @@ impl Pipeline { layer::Image::Raster { .. } => {} #[cfg(feature = "svg")] - layer::Image::Vector { handle, bounds } => { + layer::Image::Vector { + handle, + color, + bounds, + } => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = vector_cache.upload( handle, + *color, size, _scale, &mut (device, encoder),