diff --git a/crates/components/src/network_image.rs b/crates/components/src/network_image.rs index 80f20081f..a3d566101 100644 --- a/crates/components/src/network_image.rs +++ b/crates/components/src/network_image.rs @@ -15,12 +15,16 @@ use crate::Loader; /// Properties for the [`NetworkImage`] component. #[derive(Props, Clone, PartialEq)] pub struct NetworkImageProps { - /// Width of the image container. Default to `fill`. - #[props(default = "fill".into())] + /// Width of the image container. Default to `auto`. + #[props(default = "auto".into())] pub width: String, - /// Height of the image container. Default to `fill`. - #[props(default = "fill".into())] + /// Height of the image container. Default to `auto`. + #[props(default = "auto".into())] pub height: String, + /// Min width of the image container. + pub min_width: Option, + /// Min height of the image container. + pub min_height: Option, /// URL of the image. pub url: ReadOnlySignal, /// Fallback element. @@ -88,6 +92,8 @@ pub fn NetworkImage( NetworkImageProps { width, height, + min_width, + min_height, url, fallback, loading, @@ -156,6 +162,8 @@ pub fn NetworkImage( rsx!(image { height, width, + min_width, + min_height, a11y_id, image_data, a11y_role: "image", @@ -174,6 +182,8 @@ pub fn NetworkImage( rect { height, width, + min_width, + min_height, main_align: "center", cross_align: "center", Loader {} @@ -189,6 +199,8 @@ pub fn NetworkImage( rect { height, width, + min_width, + min_height, main_align: "center", cross_align: "center", label { diff --git a/crates/core/src/elements/image.rs b/crates/core/src/elements/image.rs index 9d35289f5..a434a517c 100644 --- a/crates/core/src/elements/image.rs +++ b/crates/core/src/elements/image.rs @@ -2,16 +2,20 @@ use freya_common::ImagesCache; use freya_engine::prelude::*; use freya_native_core::real_dom::NodeImmutable; use freya_node_state::{ - AspectRatio, ImageCover, - ReferencesState, SamplingMode, StyleState, TransformState, }; use super::utils::ElementUtils; -use crate::dom::DioxusNode; +use crate::{ + dom::DioxusNode, + render::{ + get_or_create_image, + ImageData, + }, +}; pub struct ImageElement; @@ -28,98 +32,57 @@ impl ElementUtils for ImageElement { _scale_factor: f32, ) { let area = layout_node.visible_area(); - let node_style = node_ref.get::().unwrap(); - let node_references = node_ref.get::().unwrap(); - let node_transform = node_ref.get::().unwrap(); - - let mut draw_img = |bytes: &[u8]| { - let image = if let Some(image_cache_key) = &node_style.image_cache_key { - images_cache.get(image_cache_key).cloned().or_else(|| { - Image::from_encoded(unsafe { Data::new_bytes(bytes) }).inspect(|image| { - images_cache.insert(image_cache_key.clone(), image.clone()); - }) - }) - } else { - Image::from_encoded(unsafe { Data::new_bytes(bytes) }) - }; - - let Some(image) = image else { - return; - }; - - let width_ratio = area.width() / image.width() as f32; - let height_ratio = area.height() / image.height() as f32; - - let (width, height) = match node_transform.aspect_ratio { - AspectRatio::Max => { - let ratio = width_ratio.max(height_ratio); - (image.width() as f32 * ratio, image.height() as f32 * ratio) - } - AspectRatio::Min => { - let ratio = width_ratio.min(height_ratio); - - (image.width() as f32 * ratio, image.height() as f32 * ratio) - } - AspectRatio::None => (area.width(), area.height()), - }; + let Some(ImageData { image, size }) = + get_or_create_image(node_ref, &area.size, images_cache) + else { + return; + }; - let mut rect = Rect::new( - area.min_x(), - area.min_y(), - area.min_x() + width, - area.min_y() + height, - ); + let node_transform = node_ref.get::().unwrap(); + let node_style = node_ref.get::().unwrap(); - if node_transform.image_cover == ImageCover::Center { - let width_offset = (width - area.width()) / 2.; - let height_offset = (height - area.height()) / 2.; + let mut rect = Rect::new( + area.min_x(), + area.min_y(), + area.min_x() + size.width, + area.min_y() + size.height, + ); - let clip_rect = Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()); + let clip_rect = Rect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()); - rect.left -= width_offset; - rect.right -= width_offset; - rect.top -= height_offset; - rect.bottom -= height_offset; + if node_transform.image_cover == ImageCover::Center { + let width_offset = (size.width - area.width()) / 2.; + let height_offset = (size.height - area.height()) / 2.; - canvas.save(); - canvas.clip_rect(clip_rect, ClipOp::Intersect, true); - } + rect.left -= width_offset; + rect.right -= width_offset; + rect.top -= height_offset; + rect.bottom -= height_offset; + } - let sampling = match node_style.image_sampling { - SamplingMode::Nearest => { - SamplingOptions::new(FilterMode::Nearest, MipmapMode::None) - } - SamplingMode::Bilinear => { - SamplingOptions::new(FilterMode::Linear, MipmapMode::None) - } - SamplingMode::Trilinear => { - SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear) - } - SamplingMode::Mitchell => SamplingOptions::from(CubicResampler::mitchell()), - SamplingMode::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()), - }; + canvas.save(); + canvas.clip_rect(clip_rect, ClipOp::Intersect, true); - canvas.draw_image_rect_with_sampling_options( - image, - None, - rect, - sampling, - &Paint::default(), - ); + let mut paint = Paint::default(); + paint.set_anti_alias(true); - if node_transform.image_cover == ImageCover::Center { - canvas.restore(); - } + let sampling = match node_style.image_sampling { + SamplingMode::Nearest => SamplingOptions::new(FilterMode::Nearest, MipmapMode::None), + SamplingMode::Bilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::None), + SamplingMode::Trilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear), + SamplingMode::Mitchell => SamplingOptions::from(CubicResampler::mitchell()), + SamplingMode::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()), }; - if let Some(image_ref) = &node_references.image_ref { - let image_data = image_ref.0.lock().unwrap(); - if let Some(image_data) = image_data.as_ref() { - draw_img(image_data) - } - } else if let Some(image_data) = &node_style.image_data { - draw_img(image_data.as_slice()) - } + canvas.draw_image_rect_with_sampling_options( + image, + None, + rect, + sampling, + &Paint::default(), + ); + + canvas.restore(); } } diff --git a/crates/core/src/layout.rs b/crates/core/src/layout.rs index ca7b9c64a..cf41fdf8a 100644 --- a/crates/core/src/layout.rs +++ b/crates/core/src/layout.rs @@ -22,8 +22,15 @@ pub fn process_layout( ) { { let rdom = fdom.rdom(); + let mut images_cache = fdom.images_cache(); let mut dom_adapter = DioxusDOMAdapter::new(rdom, scale_factor); - let skia_measurer = SkiaMeasurer::new(rdom, font_collection, default_fonts, scale_factor); + let skia_measurer = SkiaMeasurer::new( + rdom, + font_collection, + default_fonts, + scale_factor, + &mut images_cache, + ); let mut layout = fdom.layout(); diff --git a/crates/core/src/render/skia_measurer.rs b/crates/core/src/render/skia_measurer.rs index 13462e7e6..559aeff1b 100644 --- a/crates/core/src/render/skia_measurer.rs +++ b/crates/core/src/render/skia_measurer.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use freya_common::{ CachedParagraph, + ImagesCache, NodeReferenceLayout, }; use freya_engine::prelude::*; @@ -26,6 +27,8 @@ use torin::prelude::{ use super::{ create_label, create_paragraph, + get_or_create_image, + ImageData, }; use crate::{ dom::*, @@ -38,6 +41,7 @@ pub struct SkiaMeasurer<'a> { pub rdom: &'a DioxusDOM, pub default_fonts: &'a [String], pub scale_factor: f32, + pub images_cache: &'a mut ImagesCache, } impl<'a> SkiaMeasurer<'a> { @@ -46,12 +50,14 @@ impl<'a> SkiaMeasurer<'a> { font_collection: &'a FontCollection, default_fonts: &'a [String], scale_factor: f32, + images_cache: &'a mut ImagesCache, ) -> Self { Self { font_collection, rdom, default_fonts, scale_factor, + images_cache, } } } @@ -93,6 +99,14 @@ impl<'a> LayoutMeasurer for SkiaMeasurer<'a> { map.insert(CachedParagraph(paragraph, size.height)); Some((size, Arc::new(map))) } + NodeType::Element(ElementNode { tag, .. }) if tag == &TagName::Image => { + let Some(ImageData { size, .. }) = + get_or_create_image(&node, area_size, self.images_cache) + else { + return Some((*area_size, Arc::default())); + }; + Some((size, Arc::default())) + } _ => None, } } diff --git a/crates/core/src/render/utils/image.rs b/crates/core/src/render/utils/image.rs new file mode 100644 index 000000000..16c1e1108 --- /dev/null +++ b/crates/core/src/render/utils/image.rs @@ -0,0 +1,79 @@ +use freya_common::ImagesCache; +use freya_engine::prelude::{ + Data, + Image, +}; +use freya_native_core::prelude::NodeImmutable; +use freya_node_state::{ + AspectRatio, + ReferencesState, + StyleState, + TransformState, +}; +use torin::prelude::Size2D; + +use crate::dom::DioxusNode; + +pub struct ImageData { + pub image: Image, + pub size: Size2D, +} + +pub fn get_or_create_image( + node_ref: &DioxusNode, + area_size: &Size2D, + images_cache: &mut ImagesCache, +) -> Option { + let node_style = node_ref.get::().unwrap(); + let node_references = node_ref.get::().unwrap(); + + let mut get_or_create_image = |bytes: &[u8]| -> Option { + if let Some(image_cache_key) = &node_style.image_cache_key { + images_cache.get(image_cache_key).cloned().or_else(|| { + Image::from_encoded(unsafe { Data::new_bytes(bytes) }).inspect(|image| { + images_cache.insert(image_cache_key.clone(), image.clone()); + }) + }) + } else { + Image::from_encoded(unsafe { Data::new_bytes(bytes) }) + } + }; + + let image = if let Some(image_ref) = &node_references.image_ref { + let image_data = image_ref.0.lock().unwrap(); + if let Some(bytes) = image_data.as_ref() { + get_or_create_image(bytes) + } else { + None + } + } else if let Some(image_data) = &node_style.image_data { + get_or_create_image(image_data.as_slice()) + } else { + None + }?; + + let node_transform = node_ref.get::().unwrap(); + + let image_width = image.width() as f32; + let image_height = image.height() as f32; + + let width_ratio = area_size.width / image.width() as f32; + let height_ratio = area_size.height / image.height() as f32; + + let size = match node_transform.aspect_ratio { + AspectRatio::Max => { + let ratio = width_ratio.max(height_ratio); + + Size2D::new(image_width * ratio, image_height * ratio) + } + AspectRatio::Min => { + let ratio = width_ratio.min(height_ratio); + + Size2D::new(image_width * ratio, image_height * ratio) + } + AspectRatio::Fit => Size2D::new(image_width, image_height), + AspectRatio::None => *area_size, + }; + + Some(ImageData { image, size }) +} diff --git a/crates/core/src/render/utils/mod.rs b/crates/core/src/render/utils/mod.rs index 5a3a0fbf8..006f2ceb2 100644 --- a/crates/core/src/render/utils/mod.rs +++ b/crates/core/src/render/utils/mod.rs @@ -1,9 +1,11 @@ mod borders; +mod image; mod label; mod paragraph; mod shadows; pub use borders::*; +pub use image::*; pub use label::*; pub use paragraph::*; pub use shadows::*; diff --git a/crates/elements/src/attributes/image_attributes.rs b/crates/elements/src/attributes/image_attributes.rs index d1485a241..a8d909453 100644 --- a/crates/elements/src/attributes/image_attributes.rs +++ b/crates/elements/src/attributes/image_attributes.rs @@ -7,11 +7,11 @@ def_attribute!( /// `aspect_ratio` controls how an `image` element is rendered when facing unexpected dimensions. /// /// Accepted values: - /// - `none` (default): The image will be rendered with its original dimensions. - /// - `min`: The image will be rendered with the minimum dimensions possible. + /// - `fit`: The image will be rendered with its original dimensions. + /// - `none`: The image will be rendered stretching in all the maximum dimensions. + /// - `min` (default): The image will be rendered with the minimum dimensions possible. /// - `max`: The image will be rendered with the maximum dimensions possible. /// - /// /// ```rust, no_run /// # use freya::prelude::*; /// static RUST_LOGO: &[u8] = include_bytes!("../_docs/rust_logo.png"); @@ -20,7 +20,7 @@ def_attribute!( /// let image_data = static_bytes(RUST_LOGO); /// rsx!( /// image { - /// image_data: image_data, + /// image_data, /// width: "100%", /// height: "100%", /// aspect_ratio: "max" @@ -45,7 +45,7 @@ def_attribute!( /// let image_data = static_bytes(RUST_LOGO); /// rsx!( /// image { - /// image_data: image_data, + /// image_data, /// width: "100%", /// height: "100%", /// cover: "center" diff --git a/crates/elements/src/elements.rs b/crates/elements/src/elements.rs index 20a3fe27e..7fcf302f0 100644 --- a/crates/elements/src/elements.rs +++ b/crates/elements/src/elements.rs @@ -458,7 +458,7 @@ def_element!( /// let image_data = static_bytes(RUST_LOGO); /// rsx!( /// image { - /// image_data: image_data, + /// image_data, /// width: "100%", // You must specify size otherwhise it will default to 0 /// height: "100%", /// } diff --git a/crates/freya/src/_docs/elements.rs b/crates/freya/src/_docs/elements.rs index 1ea08e8e7..51b96eff3 100644 --- a/crates/freya/src/_docs/elements.rs +++ b/crates/freya/src/_docs/elements.rs @@ -80,7 +80,7 @@ //! fn app() -> Element { //! let image_data = static_bytes(RUST_LOGO); //! rsx!(image { -//! image_data: image_data, +//! image_data, //! width: "100%", //! height: "100%", //! }) diff --git a/crates/state/src/style.rs b/crates/state/src/style.rs index 9ff4773ab..48cd7511e 100644 --- a/crates/state/src/style.rs +++ b/crates/state/src/style.rs @@ -20,9 +20,11 @@ use freya_native_core::{ NodeMaskBuilder, State, }, + NodeId, SendAnyMap, }; use freya_native_core_macro::partial_derive_state; +use torin::torin::Torin; use crate::{ parsing::ExtSplit, @@ -191,8 +193,6 @@ impl State for StyleState { _children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> bool { - let compositor_dirty_nodes = context.get::>>().unwrap(); - let images_cache = context.get::>>().unwrap(); let mut style = StyleState::default(); if let Some(attributes) = node_view.attributes() { @@ -202,16 +202,24 @@ impl State for StyleState { } let changed = &style != self; + let changed_image_cache_key = style.image_cache_key != self.image_cache_key; if changed { + let compositor_dirty_nodes = context.get::>>().unwrap(); compositor_dirty_nodes .lock() .unwrap() .invalidate(node_view.node_id()); + } + if changed_image_cache_key { if let Some(image_cache_key) = &self.image_cache_key { + let images_cache = context.get::>>().unwrap(); images_cache.lock().unwrap().remove(image_cache_key); } + + let torin_layout = context.get::>>>().unwrap(); + torin_layout.lock().unwrap().invalidate(node_view.node_id()); } *self = style; diff --git a/crates/state/src/values/aspect_ratio.rs b/crates/state/src/values/aspect_ratio.rs index 3cfffec42..226f87217 100644 --- a/crates/state/src/values/aspect_ratio.rs +++ b/crates/state/src/values/aspect_ratio.rs @@ -5,9 +5,10 @@ use crate::{ #[derive(Default, Clone, Debug, PartialEq)] pub enum AspectRatio { + #[default] Min, Max, - #[default] + Fit, None, } @@ -16,6 +17,7 @@ impl Parse for AspectRatio { match value { "min" => Ok(Self::Min), "max" => Ok(Self::Max), + "fit" => Ok(Self::Fit), "none" => Ok(Self::None), _ => Err(ParseError), } diff --git a/examples/app_dog.rs b/examples/app_dog.rs index 1f28c24ac..58b88d416 100644 --- a/examples/app_dog.rs +++ b/examples/app_dog.rs @@ -45,9 +45,13 @@ fn app() -> Element { overflow: "clip", width: "100%", height: "calc(100% - 60)", + main_align: "center", + cross_align: "center", {dog_url.read().as_ref().map(|dog_url| rsx!( NetworkImage { - url: dog_url.clone() + url: dog_url.clone(), + min_width: "300", + min_height: "300", } ))} } diff --git a/examples/camera.rs b/examples/camera.rs index 3d5d2f247..937d5ada5 100644 --- a/examples/camera.rs +++ b/examples/camera.rs @@ -33,6 +33,7 @@ fn app() -> Element { image { width: "100%", height: "100%", + aspect_ratio: "none", reference: image.attribute(), image_reference: image.image_attribute() } diff --git a/examples/image.rs b/examples/image.rs index c5be31aed..d76f5a744 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -29,9 +29,9 @@ fn app() -> Element { padding: "50", main_align: "center", cross_align: "center", - onwheel: onwheel, + onwheel, image { - image_data: image_data, + image_data, width: "{size}", height: "{size}", } diff --git a/examples/image_aspect_ratio.rs b/examples/image_aspect_ratio.rs new file mode 100644 index 000000000..996620549 --- /dev/null +++ b/examples/image_aspect_ratio.rs @@ -0,0 +1,61 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] +use std::fmt::Display; + +use freya::prelude::*; +fn main() { + launch(app); +} + +static RUST_LOGO: &[u8] = include_bytes!("./rust_logo.png"); + +#[derive(Clone, Copy, PartialEq)] +enum AspectRatio { + Max, + Min, + None, + Fit, +} + +impl Display for AspectRatio { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Max => f.write_str("max"), + Self::Min => f.write_str("min"), + Self::None => f.write_str("none"), + Self::Fit => f.write_str("fit"), + } + } +} + +fn app() -> Element { + let image_data = static_bytes(RUST_LOGO); + let mut aspect_ratio = use_signal(|| AspectRatio::Min); + + rsx!( + rect { + width: "100%", + height: "100%", + padding: "25", + main_align: "center", + cross_align: "center", + Dropdown { + value: aspect_ratio(), + for ar in [AspectRatio::Max, AspectRatio::Min, AspectRatio::None, AspectRatio::Fit] { + DropdownItem { + value: ar, + onpress: move |_| aspect_ratio.set(ar), + label { "{ar}" } + } + } + } + image { + max_height: "fill", + image_data, + aspect_ratio: "{aspect_ratio}" + } + } + ) +} diff --git a/examples/image_sampling.rs b/examples/image_sampling.rs index f8984c64b..0bbe8b928 100644 --- a/examples/image_sampling.rs +++ b/examples/image_sampling.rs @@ -38,6 +38,7 @@ fn app() -> Element { image_data: image_data.clone(), width: "96", height: "96", + cache_key: "{sampling}", sampling, } diff --git a/examples/image_viewer.rs b/examples/image_viewer.rs index b396b41ad..14be5e660 100644 --- a/examples/image_viewer.rs +++ b/examples/image_viewer.rs @@ -3,6 +3,8 @@ windows_subsystem = "windows" )] +use std::path::PathBuf; + use bytes::Bytes; use freya::prelude::*; @@ -11,11 +13,7 @@ fn main() { } fn app() -> Element { - let mut image_bytes = use_signal::>(|| None); - let image_data = image_bytes - .read() - .as_ref() - .map(|bytes| dynamic_bytes(bytes.clone())); + let mut image = use_signal::>(|| None); let open_image = move |_| { spawn(async move { @@ -23,7 +21,7 @@ fn app() -> Element { if let Some(file) = file { let file_content = tokio::fs::read(file.path()).await; if let Ok(file_content) = file_content { - image_bytes.set(Some(Bytes::from(file_content))); + image.set(Some((Bytes::from(file_content), file.path().into()))); } } }); @@ -42,11 +40,10 @@ fn app() -> Element { height: "90%", main_align: "center", cross_align: "center", - if let Some(image_data) = image_data { + if let Some((bytes, path)) = &*image.read() { image { - width: "fill", - height: "fill", - image_data, + image_data: dynamic_bytes(bytes.clone()), + cache_key: "{path:?}" } } else { label { diff --git a/examples/images_slideshow.rs b/examples/images_slideshow.rs index b1a5863b8..56010ff4b 100644 --- a/examples/images_slideshow.rs +++ b/examples/images_slideshow.rs @@ -65,7 +65,9 @@ fn app() -> Element { NetworkImage { url: url.parse::().unwrap(), aspect_ratio: "max", - cover: "center" + cover: "center", + width: "fill", + height: "fill" } } } diff --git a/examples/infinite_list.rs b/examples/infinite_list.rs index 343c7de9b..c1b2928e0 100644 --- a/examples/infinite_list.rs +++ b/examples/infinite_list.rs @@ -71,6 +71,8 @@ fn RandomImage() -> Element { corner_radius: "8", if let Some(url) = url.read().clone().flatten() { NetworkImage { + width: "fill", + height: "fill", aspect_ratio: "max", url }