use crate::{animated::Mode, AnimatedState};
use iced_core::{
layout,
mouse::{self, Cursor},
renderer, svg,
widget::{tree, Tree},
window, ContentFit, Element, Event, Layout, Length, Point, Rectangle, Rotation, Size, Vector,
Widget,
};
use std::path::PathBuf;
pub use iced_widget::svg::{Catalog, Handle, Status, Style, StyleFn};
#[allow(missing_debug_implementations)]
pub struct Svg<'a, Theme = iced_core::Theme>
where
Theme: Catalog,
{
handle: Handle,
width: Length,
height: Length,
content_fit: ContentFit,
class: Theme::Class<'a>,
rotation: Rotation,
opacity: f32,
mode: Mode,
}
#[derive(Debug)]
struct State {
animated_state: AnimatedState<Status, Style>,
}
impl<'a, Theme> Svg<'a, Theme>
where
Theme: Catalog,
{
pub fn new(handle: impl Into<Handle>) -> Self {
Self {
handle: handle.into(),
width: Length::Fill,
height: Length::Shrink,
content_fit: ContentFit::Contain,
class: Theme::default(),
rotation: Rotation::default(),
opacity: 1.0,
mode: Mode::default(),
}
}
#[must_use]
pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self::new(Handle::from_path(path))
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn content_fit(self, content_fit: ContentFit) -> Self {
Self {
content_fit,
..self
}
}
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
self.rotation = rotation.into();
self
}
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
self.opacity = opacity.into();
self
}
pub fn animation(mut self, mode: impl Into<Mode>) -> Self {
self.mode = mode.into();
self
}
fn get_initial_status(&self) -> Status {
Status::Idle
}
fn get_status(&self, cursor: Cursor, layout: Layout<'_>) -> Status {
let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over {
Status::Hovered
} else {
Status::Idle
}
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<'a, Theme>
where
Renderer: iced_core::svg::Renderer,
Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
let status = self.get_initial_status();
let state = State {
animated_state: AnimatedState::new(status, self.mode),
};
tree::State::new(state)
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
state.animated_state.diff(self.mode);
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
let rotated_size = self.rotation.apply(image_size);
let raw_size = limits.resolve(self.width, self.height, rotated_size);
let full_size = self.content_fit.fit(rotated_size, raw_size);
let final_size = Size {
width: match self.width {
Length::Shrink => f32::min(raw_size.width, full_size.width),
_ => raw_size.width,
},
height: match self.height {
Length::Shrink => f32::min(raw_size.height, full_size.height),
_ => raw_size.height,
},
};
layout::Node::new(final_size)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.measure_svg(&self.handle);
let image_size = Size::new(width as f32, height as f32);
let rotated_size = self.rotation.apply(image_size);
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
let scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
let final_size = image_size * scale;
let position = match self.content_fit {
ContentFit::None => Point::new(
bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
),
_ => Point::new(
bounds.center_x() - final_size.width / 2.0,
bounds.center_y() - final_size.height / 2.0,
),
};
let drawing_bounds = Rectangle::new(position, final_size);
let state = tree.state.downcast_ref::<State>();
let style = state
.animated_state
.current_value(|status| theme.style(&self.class, *status));
let render = |renderer: &mut Renderer| {
renderer.draw_svg(
svg::Svg {
handle: self.handle.clone(),
color: style.color,
rotation: self.rotation.radians(),
opacity: self.opacity,
},
drawing_bounds,
bounds,
);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height {
renderer.with_layer(bounds, render);
} else {
render(renderer);
}
}
fn update(
&mut self,
tree: &mut Tree,
event: &iced_core::Event,
layout: Layout<'_>,
cursor: iced_core::mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn iced_core::Clipboard,
shell: &mut iced_core::Shell<'_, Message>,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
let status = self.get_status(cursor, layout);
let needs_redraw = state.animated_state.needs_redraw(status);
if needs_redraw {
shell.request_redraw();
}
if let Event::Window(window::Event::RedrawRequested(now)) = event {
state.animated_state.tick(*now);
}
}
}
impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>> for Element<'a, Message, Theme, Renderer>
where
Theme: Catalog + 'a,
Renderer: svg::Renderer + 'a,
{
fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(icon)
}
}
pub fn svg<'a, Theme>(handle: impl Into<Handle>) -> Svg<'a, Theme>
where
Theme: Catalog,
{
Svg::new(handle)
}