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.as_str(),
445 loading: props.loading.as_str(),
446 // TODO
447 // node_ref: node_ref,
448 style: "{full_style}",
449 onerror: on_error,
450 aria_current: "{props.aria_current}",
451 aria_describedby: "{props.aria_describedby}",
452 aria_expanded: "{props.aria_expanded}",
453 aria_hidden: "{props.aria_hidden}",
454 aria_live: "{props.aria_live.as_str()}",
455 aria_pressed: "{props.aria_pressed.as_str()}",
456 aria_controls: "{props.aria_controls}",
457 aria_labelledby: "{props.aria_labelledby}",
458 role: "img",
459 crossorigin: props.crossorigin.as_str(),
460 referrerpolicy: props.referrerpolicy.as_str(),
461 // TODO
462 // fetchpriority: "{props.fetchpriority.as_str()}",
463 // TODO
464 // attributionsrc: "{props.attributionsrc}",
465 onload: onload,
466 // TODO
467 // elementtiming: "{props.elementtiming}",
468 srcset: "{props.srcset}",
469 ismap: "{props.ismap}",
470 usemap: "{props.usemap}"
471 }
472 };
473
474 match props.layout {
475 Layout::Fill => rsx! {
476 span {
477 style: "display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;",
478 {img_element},
479 }
480 },
481 Layout::Responsive => {
482 let quotient = props.height.parse::<f64>().unwrap_or(1.0)
483 / props.width.parse::<f64>().unwrap_or(1.0);
484 let padding_top = if quotient.is_finite() {
485 format!("{}%", quotient * 100.0)
486 } else {
487 "100%".to_string()
488 };
489 rsx! {
490 span {
491 style: "display: block; position: relative;",
492 span {
493 style: "padding-top: {padding_top};",
494 },
495 {img_element},
496 },
497 }
498 }
499 Layout::Intrinsic => rsx! {
500 span {
501 style: "display: inline-block; position: relative; max-width: 100%;",
502 span {
503 style: "max-width: 100%;",
504 {img_element},
505 },
506 img {
507 src: "{props.blur_data_url}",
508 style: "display: none;",
509 alt: "{props.alt}",
510 aria_hidden: "true",
511 },
512 }
513 },
514 Layout::Fixed => rsx! {
515 span {
516 style: "display: inline-block; position: relative;",
517 {img_element},
518 }
519 },
520 Layout::Auto => rsx! {
521 span {
522 style: "display: inline-block; position: relative;",
523 {img_element},
524 }
525 },
526 Layout::Stretch => rsx! {
527 span {
528 style: "display: block; width: 100%; height: 100%; position: relative;",
529 {img_element},
530 }
531 },
532 Layout::ScaleDown => rsx! {
533 span {
534 style: "display: inline-block; position: relative; max-width: 100%; max-height: 100%;",
535 {img_element},
536 }
537 },
538 }
539}