1use iced_runtime::core::widget::Id;
20
21use crate::core::layout;
22use crate::core::mouse;
23use crate::core::renderer;
24use crate::core::svg;
25use crate::core::widget::Tree;
26use crate::core::{
27 Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
28 Size, Theme, Vector, Widget,
29};
30
31#[cfg(feature = "a11y")]
32use std::borrow::Cow;
33use std::marker::PhantomData;
34use std::path::PathBuf;
35
36pub use crate::core::svg::Handle;
37
38#[allow(missing_debug_implementations)]
61pub struct Svg<'a, Theme = crate::Theme>
62where
63 Theme: Catalog,
64{
65 id: Id,
66 #[cfg(feature = "a11y")]
67 name: Option<Cow<'a, str>>,
68 #[cfg(feature = "a11y")]
69 description: Option<iced_accessibility::Description<'a>>,
70 #[cfg(feature = "a11y")]
71 label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
72 handle: Handle,
73 width: Length,
74 height: Length,
75 content_fit: ContentFit,
76 class: Theme::Class<'a>,
77 rotation: Rotation,
78 opacity: f32,
79 symbolic: bool,
80 _phantom_data: PhantomData<&'a ()>,
81}
82
83impl<'a, Theme> Svg<'a, Theme>
84where
85 Theme: Catalog,
86{
87 pub fn new(handle: impl Into<Handle>) -> Self {
89 Svg {
90 id: Id::unique(),
91 #[cfg(feature = "a11y")]
92 name: None,
93 #[cfg(feature = "a11y")]
94 description: None,
95 #[cfg(feature = "a11y")]
96 label: None,
97 handle: handle.into(),
98 width: Length::Fill,
99 height: Length::Shrink,
100 content_fit: ContentFit::Contain,
101 class: Theme::default(),
102 rotation: Rotation::default(),
103 opacity: 1.0,
104 symbolic: false,
105 _phantom_data: PhantomData::default(),
106 }
107 }
108
109 #[must_use]
112 pub fn from_path(path: impl Into<PathBuf>) -> Self {
113 Self::new(Handle::from_path(path))
114 }
115
116 #[must_use]
118 pub fn width(mut self, width: impl Into<Length>) -> Self {
119 self.width = width.into();
120 self
121 }
122
123 #[must_use]
125 pub fn height(mut self, height: impl Into<Length>) -> Self {
126 self.height = height.into();
127 self
128 }
129
130 #[must_use]
134 pub fn content_fit(self, content_fit: ContentFit) -> Self {
135 Self {
136 content_fit,
137 ..self
138 }
139 }
140
141 #[must_use]
143 pub fn symbolic(mut self, symbolic: bool) -> Self {
144 self.symbolic = symbolic;
145 self
146 }
147
148 #[must_use]
150 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
151 where
152 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
153 {
154 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
155 self
156 }
157
158 #[cfg(feature = "advanced")]
160 #[must_use]
161 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
162 self.class = class.into();
163 self
164 }
165
166 pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
168 self.rotation = rotation.into();
169 self
170 }
171
172 pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
177 self.opacity = opacity.into();
178 self
179 }
180
181 #[cfg(feature = "a11y")]
182 pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
184 self.name = Some(name.into());
185 self
186 }
187
188 #[cfg(feature = "a11y")]
189 pub fn description_widget<T: iced_accessibility::Describes>(
191 mut self,
192 description: &T,
193 ) -> Self {
194 self.description = Some(iced_accessibility::Description::Id(
195 description.description(),
196 ));
197 self
198 }
199
200 #[cfg(feature = "a11y")]
201 pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
203 self.description =
204 Some(iced_accessibility::Description::Text(description.into()));
205 self
206 }
207
208 #[cfg(feature = "a11y")]
209 pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
211 self.label =
212 Some(label.label().into_iter().map(|l| l.into()).collect());
213 self
214 }
215}
216
217impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
218 for Svg<'a, Theme>
219where
220 Renderer: svg::Renderer,
221 Theme: Catalog,
222{
223 fn size(&self) -> Size<Length> {
224 Size {
225 width: self.width,
226 height: self.height,
227 }
228 }
229
230 fn layout(
231 &self,
232 _tree: &mut Tree,
233 renderer: &Renderer,
234 limits: &layout::Limits,
235 ) -> layout::Node {
236 let Size { width, height } = renderer.measure_svg(&self.handle);
238 let image_size = Size::new(width as f32, height as f32);
239
240 let rotated_size = self.rotation.apply(image_size);
242
243 let raw_size = limits.resolve(self.width, self.height, rotated_size);
245
246 let full_size = self.content_fit.fit(rotated_size, raw_size);
248
249 let final_size = Size {
251 width: match self.width {
252 Length::Shrink => f32::min(raw_size.width, full_size.width),
253 _ => raw_size.width,
254 },
255 height: match self.height {
256 Length::Shrink => f32::min(raw_size.height, full_size.height),
257 _ => raw_size.height,
258 },
259 };
260
261 layout::Node::new(final_size)
262 }
263
264 fn draw(
265 &self,
266 _state: &Tree,
267 renderer: &mut Renderer,
268 theme: &Theme,
269 renderer_style: &renderer::Style,
270 layout: Layout<'_>,
271 cursor: mouse::Cursor,
272 _viewport: &Rectangle,
273 ) {
274 let Size { width, height } = renderer.measure_svg(&self.handle);
275 let image_size = Size::new(width as f32, height as f32);
276 let rotated_size = self.rotation.apply(image_size);
277
278 let bounds = layout.bounds();
279 let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
280 let scale = Vector::new(
281 adjusted_fit.width / rotated_size.width,
282 adjusted_fit.height / rotated_size.height,
283 );
284
285 let final_size = image_size * scale;
286
287 let position = match self.content_fit {
288 ContentFit::None => Point::new(
289 bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
290 bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
291 ),
292 _ => Point::new(
293 bounds.center_x() - final_size.width / 2.0,
294 bounds.center_y() - final_size.height / 2.0,
295 ),
296 };
297
298 let drawing_bounds = Rectangle::new(position, final_size);
299
300 let is_mouse_over = cursor.is_over(bounds);
301
302 let status = if is_mouse_over {
303 Status::Hovered
304 } else {
305 Status::Idle
306 };
307
308 let mut style = theme.style(&self.class, status);
309 if self.symbolic && style.color.is_none() {
310 style.color = Some(renderer_style.icon_color);
311 }
312
313 let render = |renderer: &mut Renderer| {
314 renderer.draw_svg(
315 svg::Svg {
316 handle: self.handle.clone(),
317 color: style.color,
318 rotation: self.rotation.radians(),
319 opacity: self.opacity,
320 border_radius: [0.0; 4],
321 },
322 drawing_bounds,
323 );
324 };
325
326 if adjusted_fit.width > bounds.width
327 || adjusted_fit.height > bounds.height
328 {
329 renderer.with_layer(bounds, render);
330 } else {
331 render(renderer);
332 }
333 }
334
335 #[cfg(feature = "a11y")]
336 fn a11y_nodes(
337 &self,
338 layout: Layout<'_>,
339 _state: &Tree,
340 _cursor: mouse::Cursor,
341 ) -> iced_accessibility::A11yTree {
342 use iced_accessibility::{
343 accesskit::{NodeBuilder, NodeId, Rect, Role},
344 A11yTree,
345 };
346
347 let bounds = layout.bounds();
348 let Rectangle {
349 x,
350 y,
351 width,
352 height,
353 } = bounds;
354 let bounds = Rect::new(
355 x as f64,
356 y as f64,
357 (x + width) as f64,
358 (y + height) as f64,
359 );
360 let mut node = NodeBuilder::new(Role::Image);
361 node.set_bounds(bounds);
362 if let Some(name) = self.name.as_ref() {
363 node.set_name(name.clone());
364 }
365 match self.description.as_ref() {
366 Some(iced_accessibility::Description::Id(id)) => {
367 node.set_described_by(
368 id.iter()
369 .cloned()
370 .map(|id| NodeId::from(id))
371 .collect::<Vec<_>>(),
372 );
373 }
374 Some(iced_accessibility::Description::Text(text)) => {
375 node.set_description(text.clone());
376 }
377 None => {}
378 }
379
380 if let Some(label) = self.label.as_ref() {
381 node.set_labelled_by(label.clone());
382 }
383
384 A11yTree::leaf(node, self.id.clone())
385 }
386
387 fn id(&self) -> Option<Id> {
388 Some(self.id.clone())
389 }
390
391 fn set_id(&mut self, id: Id) {
392 self.id = id;
393 }
394}
395
396impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
397 for Element<'a, Message, Theme, Renderer>
398where
399 Theme: Catalog + 'a,
400 Renderer: svg::Renderer + 'a,
401{
402 fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
403 Element::new(icon)
404 }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum Status {
410 Idle,
412 Hovered,
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Default)]
418pub struct Style {
419 pub color: Option<Color>,
425}
426
427pub trait Catalog {
429 type Class<'a>;
431
432 fn default<'a>() -> Self::Class<'a>;
434
435 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
437}
438
439impl Catalog for Theme {
440 type Class<'a> = StyleFn<'a, Self>;
441
442 fn default<'a>() -> Self::Class<'a> {
443 Box::new(|_theme, _status| Style::default())
444 }
445
446 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
447 class(self, status)
448 }
449}
450
451pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
455
456impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
457 fn from(style: Style) -> Self {
458 Box::new(move |_theme, _status| style)
459 }
460}