image_rs/dioxus.rs
1#![doc = include_str!("../DIOXUS.md")]
2
3use crate::common::{
4 AriaLive, AriaPressed, CrossOrigin, Decoding, FetchPriority, Layout, Loading, ObjectFit,
5 Position, ReferrerPolicy,
6};
7use dioxus::prelude::*;
8use gloo_net::http::Request;
9use web_sys::IntersectionObserverEntry;
10use web_sys::js_sys;
11use web_sys::wasm_bindgen::JsCast;
12use web_sys::wasm_bindgen::prelude::*;
13use web_sys::{IntersectionObserver, IntersectionObserverInit};
14
15/// Properties for the `Image` component.
16///
17/// The `Image` component allows you to display an image with various customization options
18/// for layout, styling, and behavior. It supports fallback images, lazy loading, and custom
19/// callbacks for error handling and loading completion.
20///
21/// This component is highly flexible, providing support for multiple image layouts,
22/// object-fit, object-position, ARIA attributes, and more.
23///
24/// # See Also
25/// - [MDN img Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img)
26#[derive(Props, Clone, PartialEq)]
27pub struct ImageProps {
28 /// The source URL of the image.
29 ///
30 /// This is the URL of the image to be displayed. This property is required for loading
31 /// an image. If not provided, the image will not be displayed.
32 #[props(default = "")]
33 pub src: &'static str,
34
35 /// The alternative text for the image.
36 ///
37 /// This is the alt text for the image, which is used for accessibility purposes.
38 /// If not provided, the alt text will be empty.
39 #[props(default = "")]
40 pub alt: &'static str,
41
42 /// Optional fallback image.
43 ///
44 /// This image will be displayed if the main image fails to load. If not provided,
45 /// the image will attempt to load without a fallback.
46 #[props(default = "")]
47 pub fallback_src: &'static str,
48
49 /// The width of the image.
50 ///
51 /// Specifies the width of the image in pixels. It is typically used for responsive
52 /// layouts. Defaults to an empty string if not provided.
53 #[props(default = "")]
54 pub width: &'static str,
55
56 /// The height of the image.
57 ///
58 /// Specifies the height of the image in pixels. Like `width`, it is often used for
59 /// responsive layouts. Defaults to an empty string if not provided.
60 #[props(default = "")]
61 pub height: &'static str,
62
63 // Common props
64 /// The style attribute for the image.
65 ///
66 /// Allows you to apply custom inline CSS styles to the image. Defaults to an empty string.
67 #[props(default = "")]
68 pub style: &'static str,
69
70 /// The CSS class for the image.
71 ///
72 /// This can be used to apply custom CSS classes to the image for styling purposes.
73 /// Defaults to an empty string if not provided.
74 #[props(default = "")]
75 pub class: &'static str,
76
77 /// The sizes attribute for the image.
78 ///
79 /// This is used to define different image sizes for different viewport widths, helping
80 /// with responsive images. Defaults to an empty string if not provided.
81 #[props(default = "")]
82 pub sizes: &'static str,
83
84 /// The quality attribute for the image.
85 ///
86 /// Allows you to set the quality of the image (e.g., "low", "medium", "high"). Defaults
87 /// to an empty string if not provided.
88 #[props(default = "")]
89 pub quality: &'static str,
90
91 /// Indicates if the image should have priority loading.
92 ///
93 /// This controls whether the image should be loaded eagerly (immediately) or lazily
94 #[props(default)]
95 pub loading: Loading,
96
97 /// The placeholder attribute for the image.
98 ///
99 /// Allows you to specify a placeholder image URL or data URL to show while the main
100 /// image is loading. Defaults to an empty string.
101 #[props(default = "")]
102 pub placeholder: &'static str,
103
104 /// Callback function for handling loading completion.
105 ///
106 /// This callback is triggered once the image has finished loading. This is useful for
107 /// actions that should happen after the image has been fully loaded, such as hiding
108 /// a loading spinner. Defaults to a no-op.
109 #[props(default)]
110 pub on_load: Callback<()>,
111
112 // Advanced Props
113 /// The object-fit attribute for the image.
114 ///
115 /// Determines how the image should be resized to fit its container. Common values include
116 /// "contain", "cover", "fill", etc. Defaults to an empty string.
117 #[props(default)]
118 pub object_fit: ObjectFit,
119
120 /// The object-position attribute for the image.
121 ///
122 /// Specifies how the image should be positioned within its container when `object-fit` is set.
123 /// The available options are:
124 /// - `Position::Center`: Centers the image within the container.
125 /// - `Position::Top`: Aligns the image to the top of the container.
126 /// - `Position::Bottom`: Aligns the image to the bottom of the container.
127 /// - `Position::Left`: Aligns the image to the left of the container.
128 /// - `Position::Right`: Aligns the image to the right of the container.
129 /// - `Position::TopLeft`: Aligns the image to the top-left of the container.
130 /// - `Position::TopRight`: Aligns the image to the top-right of the container.
131 /// - `Position::BottomLeft`: Aligns the image to the bottom-left of the container.
132 /// - `Position::BottomRight`: Aligns the image to the bottom-right of the container.
133 ///
134 /// Defaults to `Position::Center`.
135 #[props(default)]
136 pub object_position: Position,
137
138 /// Callback function for handling errors during image loading.
139 ///
140 /// This callback is triggered if the image fails to load, allowing you to handle
141 /// error states (e.g., displaying a fallback image or showing an error message).
142 #[props(default)]
143 pub on_error: Callback<String>,
144
145 /// The decoding attribute for the image.
146 ///
147 /// Specifies how the image should be decoded. The available options are:
148 /// - `Decoding::Auto`: The image decoding behavior is automatically decided by the browser.
149 /// - `Decoding::Sync`: The image is decoded synchronously (blocking other tasks).
150 /// - `Decoding::Async`: The image is decoded asynchronously (non-blocking).
151 ///
152 /// Defaults to `Decoding::Auto`.
153 #[props(default)]
154 pub decoding: Decoding,
155
156 /// The blur data URL for placeholder image.
157 ///
158 /// This is used to display a low-quality blurred version of the image while the full
159 /// image is loading. Defaults to an empty string.
160 #[props(default = "")]
161 pub blur_data_url: &'static str,
162
163 /// The lazy boundary for lazy loading.
164 ///
165 /// Defines the distance (in pixels) from the viewport at which the image should start
166 /// loading. Defaults to an empty string.
167 #[props(default = "")]
168 pub lazy_boundary: &'static str,
169
170 /// Indicates if the image should be unoptimized.
171 ///
172 /// If set to `true`, the image will be loaded without any optimization applied (e.g.,
173 /// no resizing or compression). Defaults to `false`.
174 #[props(default = false)]
175 pub unoptimized: bool,
176
177 /// Image layout.
178 ///
179 /// Specifies how the image should be laid out within its container. Possible values
180 /// include `Layout::Fill`, `Layout::Responsive`, `Layout::Intrinsic`, `Layout::Fixed`,
181 /// `Layout::Auto`, `Layout::Stretch`, and `Layout::ScaleDown`. Defaults to `Layout::Responsive`.
182 #[props(default)]
183 pub layout: Layout,
184
185 // /// Reference to the DOM node.
186 // ///
187 // /// This is used to create a reference to the actual DOM element of the image. It is
188 // /// useful for directly manipulating the image element via JavaScript if needed.
189 // // TODO: Figure out how to pass a node ref
190 // #[prop_or_default]
191 // pub node_ref: Node,
192 /// A list of one or more image sources for responsive loading.
193 ///
194 /// Defines multiple image resources for the browser to choose from, depending on screen size, resolution,
195 /// and other factors. Each source can include width (`w`) or pixel density (`x`) descriptors.
196 #[props(default)]
197 pub srcset: &'static str,
198
199 /// Cross-origin policy to use when fetching the image.
200 ///
201 /// Determines whether the image should be fetched with CORS enabled. Useful when the image needs to be accessed
202 /// in a `<canvas>` element. Accepts `anonymous` or `use-credentials`.
203 #[props(default)]
204 pub crossorigin: CrossOrigin,
205
206 /// Referrer policy to apply when fetching the image.
207 ///
208 /// Controls how much referrer information should be included with requests made for the image resource.
209 /// Common values include `no-referrer`, `origin`, `strict-origin-when-cross-origin`, etc.
210 #[props(default)]
211 pub referrerpolicy: ReferrerPolicy,
212
213 /// The fragment identifier of the image map to use.
214 ///
215 /// Associates the image with a `<map>` element, enabling clickable regions within the image. The value
216 /// should begin with `#` and match the `name` of the corresponding map element.
217 #[props(default)]
218 pub usemap: &'static str,
219
220 /// Indicates that the image is part of a server-side image map.
221 ///
222 /// When set, clicking the image will send the click coordinates to the server. Only allowed when the image
223 /// is inside an `<a>` element with a valid `href`.
224 #[props(default)]
225 pub ismap: bool,
226
227 /// Hints the browser about the priority of fetching this image.
228 ///
229 /// Helps the browser prioritize network resource loading. Accepts `high`, `low`, or `auto` (default).
230 /// See `HTMLImageElement.fetchPriority` for more.
231 #[props(default)]
232 pub fetchpriority: FetchPriority,
233
234 /// Identifier for tracking image performance timing.
235 ///
236 /// Registers the image with the `PerformanceElementTiming` API using the given string as its ID. Useful for
237 /// performance monitoring and analytics.
238 #[props(default)]
239 pub elementtiming: &'static str,
240
241 /// URL(s) to send Attribution Reporting requests for the image.
242 ///
243 /// Indicates that the browser should send an `Attribution-Reporting-Eligible` header with the image request.
244 /// Can be a boolean or a list of URLs for attribution registration on specified servers. Experimental feature.
245 #[props(default)]
246 pub attributionsrc: &'static str,
247
248 /// Indicates the current state of the image in a navigation menu.
249 ///
250 /// Valid values are "page", "step", "location", "date", "time", "true", "false".
251 /// This is useful for enhancing accessibility in navigation menus.
252 #[props(default = "")]
253 pub aria_current: &'static str,
254
255 /// Describes the image using the ID of the element that provides a description.
256 ///
257 /// The ID of the element that describes the image. This is used for accessibility
258 /// purposes, particularly for screen readers.
259 #[props(default = "")]
260 pub aria_describedby: &'static str,
261
262 /// Indicates whether the content associated with the image is currently expanded or collapsed.
263 ///
264 /// This is typically used for ARIA-based accessibility and is represented as "true" or "false".
265 #[props(default = "")]
266 pub aria_expanded: &'static str,
267
268 /// Indicates whether the image is currently hidden from the user.
269 ///
270 /// This attribute is used for accessibility and indicates whether the image is visible
271 /// to the user or not. Valid values are "true" or "false".
272 #[props(default = "")]
273 pub aria_hidden: &'static str,
274
275 /// Indicates whether the content associated with the image is live and dynamic.
276 ///
277 /// The value can be "off", "assertive", or "polite", helping assistive technologies
278 /// determine how to handle updates to the content.
279 #[props(default)]
280 pub aria_live: AriaLive,
281
282 /// Indicates whether the image is currently pressed or selected.
283 ///
284 /// This attribute can have values like "true", "false", "mixed", or "undefined".
285 #[props(default)]
286 pub aria_pressed: AriaPressed,
287
288 /// ID of the element that the image controls or owns.
289 ///
290 /// Specifies the ID of the element that the image controls or is associated with.
291 #[props(default = "")]
292 pub aria_controls: &'static str,
293
294 /// ID of the element that labels the image.
295 ///
296 /// Specifies the ID of the element that labels the image for accessibility purposes.
297 #[props(default = "")]
298 pub aria_labelledby: &'static str,
299}
300
301impl Default for ImageProps {
302 fn default() -> Self {
303 ImageProps {
304 src: "",
305 alt: "Image",
306 width: "",
307 height: "",
308 style: "",
309 class: "",
310 sizes: "",
311 quality: "",
312 placeholder: "empty",
313 on_load: Callback::default(),
314 object_fit: ObjectFit::default(),
315 object_position: Position::default(),
316 on_error: Callback::default(),
317 decoding: Decoding::default(),
318 blur_data_url: "",
319 lazy_boundary: "100px",
320 unoptimized: false,
321 layout: Layout::default(),
322 fallback_src: "",
323 srcset: "",
324 crossorigin: CrossOrigin::default(),
325 loading: Loading::default(),
326 referrerpolicy: ReferrerPolicy::default(),
327 usemap: "",
328 ismap: false,
329 fetchpriority: FetchPriority::default(),
330 elementtiming: "",
331 attributionsrc: "",
332 aria_current: "",
333 aria_describedby: "",
334 aria_expanded: "",
335 aria_hidden: "",
336 aria_live: AriaLive::default(),
337 aria_pressed: AriaPressed::default(),
338 aria_controls: "",
339 aria_labelledby: "",
340 }
341 }
342}
343
344#[component]
345pub fn Image(props: ImageProps) -> Element {
346 // TODO: Figure out how to create a node in dioxus
347 let node_ref = Some(5);
348 let mut src = use_signal(|| props.src);
349 let on_load = props.on_load;
350 let on_error_callback = props.on_error;
351
352 // Intersection Observer effect
353 use_effect(move || {
354 // TODO: el.cast::<HtmlImageElement>()
355 let node = node_ref.as_ref();
356 if let Some(_img) = node {
357 let closure = Closure::wrap(Box::new(
358 move |entries: js_sys::Array, _: IntersectionObserver| {
359 if let Some(entry) = entries.get(0).dyn_ref::<IntersectionObserverEntry>() {
360 if entry.is_intersecting() {
361 // img.set_src(props.src);
362 on_load.call(());
363 }
364 }
365 },
366 )
367 as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
368
369 let options = IntersectionObserverInit::new();
370 options.set_threshold(&js_sys::Array::of1(&0.1.into()));
371 options.set_root_margin(props.lazy_boundary);
372
373 if let Ok(observer) =
374 IntersectionObserver::new_with_options(closure.as_ref().unchecked_ref(), &options)
375 {
376 // observer.observe(&img);
377 closure.forget();
378 {
379 observer.disconnect();
380 }
381 }
382 }
383 });
384
385 // On error handler
386 let on_error = move |_| {
387 let fallback_src = props.fallback_src;
388
389 if fallback_src.is_empty() {
390 on_error_callback.call("Image failed to load and no fallback provided.".to_string());
391 return;
392 }
393
394 spawn(async move {
395 match Request::get(fallback_src).send().await {
396 Ok(resp) if resp.ok() => {
397 src.set(fallback_src);
398 on_load.call(());
399 }
400 Ok(resp) => {
401 let status = resp.status();
402 let body = resp.text().await.unwrap_or_default();
403 on_error_callback.call(format!(
404 "Fallback image load failed: status {}, body {}",
405 status, body
406 ));
407 }
408 Err(e) => {
409 on_error_callback.call(format!("Network error while loading fallback: {}", e));
410 }
411 }
412 });
413 };
414
415 let img_style = format!(
416 "object-fit: {:?}; object-position: {:?}; {};",
417 props.object_fit, props.object_position, props.style
418 );
419
420 let blur_style = if props.placeholder == "blur" {
421 format!(
422 "background-size: {}; background-position: {:?}; filter: blur(20px); background-image: url('{}');",
423 props.sizes, props.object_position, props.blur_data_url
424 )
425 } else {
426 "".to_string()
427 };
428
429 let full_style = format!("{img_style} {blur_style}");
430
431 let onload = move |_| {
432 props.on_load.call(());
433 };
434
435 let img_element = rsx! {
436 img {
437 src: "{src()}",
438 alt: "{props.alt}",
439 width: "{props.width}",
440 height: "{props.height}",
441 class: "{props.class}",
442 // TODO: Till Dioxus support this attribute
443 // sizes: "{props.sizes}",
444 // decoding: "{props.decoding}",
445 // TODO:
446 // loading: "{props.loading}",
447 // TODO
448 // node_ref: node_ref,
449 style: "{full_style}",
450 onerror: on_error,
451 aria_current: "{props.aria_current}",
452 aria_describedby: "{props.aria_describedby}",
453 aria_expanded: "{props.aria_expanded}",
454 aria_hidden: "{props.aria_hidden}",
455 aria_live: "{props.aria_live.as_str()}",
456 aria_pressed: "{props.aria_pressed.as_str()}",
457 aria_controls: "{props.aria_controls}",
458 aria_labelledby: "{props.aria_labelledby}",
459 role: "img",
460 style: "{blur_style}",
461 crossorigin: props.crossorigin.as_str(),
462 referrerpolicy: props.referrerpolicy.as_str(),
463 // TODO
464 // fetchpriority: "{props.fetchpriority.as_str()}",
465 // TODO
466 // attributionsrc: "{props.attributionsrc}",
467 onload: onload,
468 // TODO
469 // elementtiming: "{props.elementtiming}",
470 srcset: "{props.srcset}",
471 ismap: "{props.ismap}",
472 usemap: "{props.usemap}"
473 }
474 };
475
476 match props.layout {
477 Layout::Fill => rsx! {
478 span {
479 style: "display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;",
480 {img_element},
481 }
482 },
483 Layout::Responsive => {
484 let quotient = props.height.parse::<f64>().unwrap_or(1.0)
485 / props.width.parse::<f64>().unwrap_or(1.0);
486 let padding_top = if quotient.is_finite() {
487 format!("{}%", quotient * 100.0)
488 } else {
489 "100%".to_string()
490 };
491 rsx! {
492 span {
493 style: "display: block; position: relative;",
494 span {
495 style: "padding-top: {padding_top};",
496 },
497 {img_element},
498 },
499 }
500 }
501 Layout::Intrinsic => rsx! {
502 span {
503 style: "display: inline-block; position: relative; max-width: 100%;",
504 span {
505 style: "max-width: 100%;",
506 {img_element},
507 },
508 img {
509 src: "{props.blur_data_url}",
510 style: "display: none;",
511 alt: "{props.alt}",
512 aria_hidden: "true",
513 },
514 }
515 },
516 Layout::Fixed => rsx! {
517 span {
518 style: "display: inline-block; position: relative;",
519 {img_element},
520 }
521 },
522 Layout::Auto => rsx! {
523 span {
524 style: "display: inline-block; position: relative;",
525 {img_element},
526 }
527 },
528 Layout::Stretch => rsx! {
529 span {
530 style: "display: block; width: 100%; height: 100%; position: relative;",
531 {img_element},
532 }
533 },
534 Layout::ScaleDown => rsx! {
535 span {
536 style: "display: inline-block; position: relative; max-width: 100%; max-height: 100%;",
537 {img_element},
538 }
539 },
540 }
541}