diff --git a/Cargo.toml b/Cargo.toml index a427c5ee1ac..287d9d7f89e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,7 +123,7 @@ homepage = "https://slint.dev" keywords = ["gui", "toolkit", "graphics", "design", "ui"] license = "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0" repository = "https://github.com/slint-ui/slint" -rust-version = "1.92" +rust-version = "1.95" version = "1.17.0" [workspace.dependencies] diff --git a/api/cpp/cbindgen.rs b/api/cpp/cbindgen.rs index 06e958b2998..9e8465fa39b 100644 --- a/api/cpp/cbindgen.rs +++ b/api/cpp/cbindgen.rs @@ -515,6 +515,8 @@ fn gen_corelib( "slint_image_to_rgb8", "slint_image_to_rgba8", "slint_image_to_rgba8_premultiplied", + "slint_image_from_shared_buffer_rgb8", + "slint_image_from_shared_buffer_rgba8", "SharedPixelBuffer", "SharedImageBuffer", "StaticTextures", diff --git a/api/cpp/include/private/slint_image.h b/api/cpp/include/private/slint_image.h index fd444a72f79..0a958888d30 100644 --- a/api/cpp/include/private/slint_image.h +++ b/api/cpp/include/private/slint_image.h @@ -172,27 +172,27 @@ struct Image } /// Construct an image from a SharedPixelBuffer of RGB pixels. - Image(SharedPixelBuffer buffer) - : data(Data::ImageInner_EmbeddedImage( - cbindgen_private::types::ImageCacheKey::Invalid(), - cbindgen_private::types::SharedImageBuffer::RGB8( - cbindgen_private::types::SharedPixelBuffer { - .width = buffer.width(), - .height = buffer.height(), - .data = buffer.m_data }))) + Image(SharedPixelBuffer buffer) : data(Data::ImageInner_None()) { + cbindgen_private::types::SharedPixelBuffer buffer_private { + .width = buffer.width(), + .height = buffer.height(), + .data = buffer.m_data, + }; + + cbindgen_private::types::slint_image_from_shared_buffer_rgb8(&buffer_private, &data); } /// Construct an image from a SharedPixelBuffer of RGBA pixels. - Image(SharedPixelBuffer buffer) - : data(Data::ImageInner_EmbeddedImage( - cbindgen_private::types::ImageCacheKey::Invalid(), - cbindgen_private::types::SharedImageBuffer::RGBA8( - cbindgen_private::types::SharedPixelBuffer { - .width = buffer.width(), - .height = buffer.height(), - .data = buffer.m_data }))) + Image(SharedPixelBuffer buffer) : data(Data::ImageInner_None()) { + cbindgen_private::types::SharedPixelBuffer buffer_private { + .width = buffer.width(), + .height = buffer.height(), + .data = buffer.m_data, + }; + + cbindgen_private::types::slint_image_from_shared_buffer_rgba8(&buffer_private, &data); } /// Returns the size of the Image in pixels. @@ -275,6 +275,7 @@ struct Image private: using Tag = cbindgen_private::types::ImageInner::Tag; using Data = cbindgen_private::types::Image; + Data data; }; diff --git a/helper_crates/vtable/src/vrc.rs b/helper_crates/vtable/src/vrc.rs index 927732439c4..391ca46bdf5 100644 --- a/helper_crates/vtable/src/vrc.rs +++ b/helper_crates/vtable/src/vrc.rs @@ -135,6 +135,12 @@ impl core::fmt::Debug for VRc> From for VRc { + fn from(value: X) -> Self { + Self::new(value) + } +} + impl> VRc { /// Create a new VRc from an instance of a type that can be associated with a VTable. /// diff --git a/internal/backends/linuxkms/fullscreenwindowadapter.rs b/internal/backends/linuxkms/fullscreenwindowadapter.rs index a1acbb7ae47..810b9dbd5aa 100644 --- a/internal/backends/linuxkms/fullscreenwindowadapter.rs +++ b/internal/backends/linuxkms/fullscreenwindowadapter.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use i_slint_core::Property; use i_slint_core::api::{LogicalPosition, PhysicalSize as PhysicalWindowSize}; -use i_slint_core::graphics::{Image, euclid}; +use i_slint_core::graphics::{EmbeddedImage, Image, euclid}; use i_slint_core::item_rendering::ItemRenderer; use i_slint_core::lengths::LogicalRect; use i_slint_core::platform::WindowEvent; @@ -149,10 +149,9 @@ fn mouse_cursor_image() -> Image { i_slint_core::ImageInner::Svg(svg) => { let pixels = svg.render(None).unwrap(); let cache_key = svg.cache_key(); - let mouse_pointer_pixel_image = i_slint_core::graphics::ImageInner::EmbeddedImage { - cache_key: cache_key.clone(), - buffer: pixels, - }; + let mouse_pointer_pixel_image = i_slint_core::graphics::ImageInner::EmbeddedImage( + EmbeddedImage { cache_key: cache_key.clone(), buffer: pixels }.into(), + ); i_slint_core::graphics::cache::replace_cached_image( cache_key, mouse_pointer_pixel_image.clone(), diff --git a/internal/core/graphics/image.rs b/internal/core/graphics/image.rs index 3b07e19984f..d4f9f24e731 100644 --- a/internal/core/graphics/image.rs +++ b/internal/core/graphics/image.rs @@ -333,7 +333,7 @@ impl ImageCacheKey { pub fn new(resource: &ImageInner) -> Option { let key = match resource { ImageInner::None => return None, - ImageInner::EmbeddedImage { cache_key, .. } => cache_key.clone(), + ImageInner::EmbeddedImage(embedded) => embedded.cache_key.clone(), ImageInner::StaticTextures(textures) => { Self::from_embedded_image_data(textures.data.as_slice()) } @@ -409,6 +409,16 @@ impl OpaqueImage for WGPUTexture { } } +/// An embedded image, storing a cache key and the inline image data. +#[derive(Clone, Debug, PartialEq)] +#[repr(C)] +pub struct EmbeddedImage { + /// The cache key uniquely identifying this image. + pub cache_key: ImageCacheKey, + /// The image data. + pub buffer: SharedImageBuffer, +} + /// A resource is a reference to binary data, for example images. They can be accessible on the file /// system or embedded in the resulting binary. Or they might be URLs to a web server and a downloaded /// is necessary before they can be used. @@ -420,10 +430,7 @@ pub enum ImageInner { /// A resource that does not represent any data. #[default] None = 0, - EmbeddedImage { - cache_key: ImageCacheKey, - buffer: SharedImageBuffer, - } = 1, + EmbeddedImage(vtable::VRc) = 1, #[cfg(feature = "svg")] Svg(vtable::VRc) = 2, StaticTextures(&'static StaticTextures) = 3, @@ -433,10 +440,55 @@ pub enum ImageInner { #[cfg(not(target_arch = "wasm32"))] BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6, NineSlice(vtable::VRc) = 7, + // Wrapped in `VRc`, as otherwise `WGPUTexture` bloats the type significantly. #[cfg(any(feature = "unstable-wgpu-27", feature = "unstable-wgpu-28"))] - WGPUTexture(WGPUTexture) = 8, + WGPUTexture(vtable::VRc) = 8, } +#[expect(unused)] +mod drop_shim_vtable { + #[cfg(any(feature = "unstable-wgpu-27", feature = "unstable-wgpu-28"))] + impl DropShim for super::WGPUTexture {} + impl DropShim for super::EmbeddedImage {} + + /// Stub vtable to allow a `VRc` that only maintains lifetimes without granting access to + /// any functionality on the type. + #[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)] + #[vtable::vtable] + #[repr(C)] + pub(super) struct DropShimVTable { + /// in-place destructor (for VRc) + pub drop_in_place: unsafe extern "C" fn(vtable::VRefMut) -> vtable::Layout, + + /// dealloc function (for VRc) + pub dealloc: unsafe extern "C" fn(&DropShimVTable, ptr: *mut u8, layout: vtable::Layout), + } + + #[cfg(any(feature = "unstable-wgpu-27", feature = "unstable-wgpu-28"))] + DropShimVTable_static!(static WGPU_TEXTURE_DROP_VTABLE for super::WGPUTexture); + DropShimVTable_static!(static EMBEDDED_TEXTURE_DROP_VTABLE for super::EmbeddedImage); +} + +#[cfg(all(debug_assertions, feature = "ffi"))] +#[expect(unused)] +const _: () = { + #[repr(u8)] + enum ImageInnerAllFeaturesDisabled { + None = 0, + EmbeddedImage(vtable::VRc) = 1, + StaticTextures(&'static StaticTextures) = 3, + #[cfg(target_arch = "wasm32")] + HTMLImage(vtable::VRc) = 4, + BackendStorage(vtable::VRc) = 5, + NineSlice(vtable::VRc) = 7, + #[cfg(not(target_arch = "wasm32"))] + BorrowedOpenGLTexture(BorrowedOpenGLTexture) = 6, + } + + static_assertions::assert_eq_align!(ImageInner, ImageInnerAllFeaturesDisabled); + static_assertions::assert_eq_size!(ImageInner, ImageInnerAllFeaturesDisabled); +}; + impl ImageInner { /// Return or render the image into a buffer /// @@ -449,7 +501,7 @@ impl ImageInner { _target_size_for_scalable_source: Option>, ) -> Option { match self { - ImageInner::EmbeddedImage { buffer, .. } => Some(buffer.clone()), + ImageInner::EmbeddedImage(embedded) => Some(embedded.buffer.clone()), #[cfg(feature = "svg")] ImageInner::Svg(svg) => match svg.render(_target_size_for_scalable_source) { Ok(b) => Some(b), @@ -542,7 +594,7 @@ impl ImageInner { pub fn size(&self) -> IntSize { match self { ImageInner::None => Default::default(), - ImageInner::EmbeddedImage { buffer, .. } => buffer.size(), + ImageInner::EmbeddedImage(embedded) => embedded.buffer.size(), ImageInner::StaticTextures(StaticTextures { original_size, .. }) => *original_size, #[cfg(feature = "svg")] ImageInner::Svg(svg) => svg.size(), @@ -561,10 +613,9 @@ impl ImageInner { impl PartialEq for ImageInner { fn eq(&self, other: &Self) -> bool { match (self, other) { - ( - Self::EmbeddedImage { cache_key: l_cache_key, buffer: l_buffer }, - Self::EmbeddedImage { cache_key: r_cache_key, buffer: r_buffer }, - ) => l_cache_key == r_cache_key && l_buffer == r_buffer, + (Self::EmbeddedImage(embedded_l), Self::EmbeddedImage(embedded_r)) => { + **embedded_l == **embedded_r + } #[cfg(feature = "svg")] (Self::Svg(l0), Self::Svg(r0)) => vtable::VRc::ptr_eq(l0, r0), (Self::StaticTextures(l0), Self::StaticTextures(r0)) => l0 == r0, @@ -716,19 +767,19 @@ impl Image { /// Creates a new Image from the specified shared pixel buffer, where each pixel has three color /// channels (red, green and blue) encoded as u8. pub fn from_rgb8(buffer: SharedPixelBuffer) -> Self { - Image(ImageInner::EmbeddedImage { + Image(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key: ImageCacheKey::Invalid, buffer: SharedImageBuffer::RGB8(buffer), - }) + }))) } /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color /// channels (red, green, blue and alpha) encoded as u8. pub fn from_rgba8(buffer: SharedPixelBuffer) -> Self { - Image(ImageInner::EmbeddedImage { + Image(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key: ImageCacheKey::Invalid, buffer: SharedImageBuffer::RGBA8(buffer), - }) + }))) } /// Creates a new Image from the specified shared pixel buffer, where each pixel has four color @@ -737,10 +788,10 @@ impl Image { /// /// Only construct an Image with this function if you know that your pixels are encoded this way. pub fn from_rgba8_premultiplied(buffer: SharedPixelBuffer) -> Self { - Image(ImageInner::EmbeddedImage { + Image(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key: ImageCacheKey::Invalid, buffer: SharedImageBuffer::RGBA8Premultiplied(buffer), - }) + }))) } /// Returns the pixel buffer for the Image if available in RGB format without alpha. @@ -827,8 +878,13 @@ impl Image { /// as new major WGPU releases become available. #[cfg(feature = "unstable-wgpu-27")] pub fn to_wgpu_27_texture(&self) -> Option { + #[cfg_attr(not(feature = "unstable-wgpu-28"), expect(irrefutable_let_patterns))] match &self.0 { - ImageInner::WGPUTexture(WGPUTexture::WGPU27Texture(texture)) => Some(texture.clone()), + ImageInner::WGPUTexture(texture_rc) + if WGPUTexture::WGPU27Texture(texture) = &**texture_rc => + { + Some(texture.clone()) + } _ => None, } } @@ -840,8 +896,13 @@ impl Image { /// as new major WGPU releases become available. #[cfg(feature = "unstable-wgpu-28")] pub fn to_wgpu_28_texture(&self) -> Option { + #[cfg_attr(not(feature = "unstable-wgpu-27"), expect(irrefutable_let_patterns))] match &self.0 { - ImageInner::WGPUTexture(WGPUTexture::WGPU28Texture(texture)) => Some(texture.clone()), + ImageInner::WGPUTexture(texture_rc) + if let WGPUTexture::WGPU28Texture(texture) = &**texture_rc => + { + Some(texture.clone()) + } _ => None, } } @@ -910,6 +971,28 @@ impl Image { self.0.size() } + #[cfg(any(feature = "ffi", feature = "std"))] + fn path_str(&self) -> Option<&SharedString> { + match &self.0 { + #[cfg(feature = "std")] + ImageInner::EmbeddedImage(embedded) + if let ImageCacheKey::Path(CachedPath { path, .. }) = &embedded.cache_key => + { + Some(path) + } + #[cfg(feature = "std")] + ImageInner::NineSlice(nine) => match &nine.0 { + ImageInner::EmbeddedImage(embedded) + if let ImageCacheKey::Path(CachedPath { path, .. }) = &embedded.cache_key => + { + Some(path) + } + _ => None, + }, + _ => None, + } + } + #[cfg(feature = "std")] /// Returns the path of the image on disk, if it was constructed via [`Self::load_from_path`]. /// @@ -923,20 +1006,7 @@ impl Image { /// assert_eq!(image.path(), Some(path_buf.as_path())); /// ``` pub fn path(&self) -> Option<&std::path::Path> { - match &self.0 { - ImageInner::EmbeddedImage { - cache_key: ImageCacheKey::Path(CachedPath { path, .. }), - .. - } => Some(std::path::Path::new(path.as_str())), - ImageInner::NineSlice(nine) => match &nine.0 { - ImageInner::EmbeddedImage { - cache_key: ImageCacheKey::Path(CachedPath { path, .. }), - .. - } => Some(std::path::Path::new(path.as_str())), - _ => None, - }, - _ => None, - } + self.path_str().map(|str| std::path::Path::new(str.as_str())) } } @@ -1035,10 +1105,10 @@ pub fn decode_image_data(data: &[u8], format: &str) -> Option { image::load_from_memory(data) }; match decoded { - Ok(image) => Some(Image(ImageInner::EmbeddedImage { + Ok(image) => Some(Image(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key: ImageCacheKey::Invalid, buffer: cache::dynamic_image_to_shared_image_buffer(image), - })), + })))), Err(err) => { crate::debug_log!("Error decoding image data: {}", err); None @@ -1370,6 +1440,42 @@ pub(crate) mod ffi { } } + #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_image_from_shared_buffer_rgb8( + buffer: &SharedPixelBuffer, + out: *mut Image, + ) { + let image = Image(ImageInner::EmbeddedImage( + EmbeddedImage { + cache_key: ImageCacheKey::Invalid, + buffer: SharedImageBuffer::RGB8(buffer.clone()), + } + .into(), + )); + + unsafe { + core::ptr::write(out, image); + } + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_image_from_shared_buffer_rgba8( + buffer: &SharedPixelBuffer, + out: *mut Image, + ) { + let image = Image(ImageInner::EmbeddedImage( + EmbeddedImage { + cache_key: ImageCacheKey::Invalid, + buffer: SharedImageBuffer::RGBA8(buffer.clone()), + } + .into(), + )); + + unsafe { + core::ptr::write(out, image); + } + } + #[cfg(feature = "std")] #[unsafe(no_mangle)] pub unsafe extern "C" fn slint_image_load_from_embedded_data( @@ -1387,22 +1493,7 @@ pub(crate) mod ffi { #[unsafe(no_mangle)] pub extern "C" fn slint_image_path(image: &Image) -> Option<&SharedString> { - match &image.0 { - #[cfg(feature = "std")] - ImageInner::EmbeddedImage { - cache_key: ImageCacheKey::Path(CachedPath { path, .. }), - .. - } => Some(path), - ImageInner::NineSlice(nine) => match &nine.0 { - #[cfg(feature = "std")] - ImageInner::EmbeddedImage { - cache_key: ImageCacheKey::Path(CachedPath { path, .. }), - .. - } => Some(path), - _ => None, - }, - _ => None, - } + image.path_str() } #[unsafe(no_mangle)] diff --git a/internal/core/graphics/image/cache.rs b/internal/core/graphics/image/cache.rs index f8bff9b7a54..98019385b38 100644 --- a/internal/core/graphics/image/cache.rs +++ b/internal/core/graphics/image/cache.rs @@ -6,7 +6,7 @@ This module contains image and caching related types for the run-time library. */ use super::{CachedPath, Image, ImageCacheKey, ImageInner, SharedImageBuffer, SharedPixelBuffer}; -use crate::{SharedString, slice::Slice}; +use crate::{SharedString, graphics::image::EmbeddedImage, slice::Slice}; struct ImageWeightInBytes; @@ -14,7 +14,7 @@ impl clru::WeightScale for ImageWeightInBytes { fn weight(&self, _key: &ImageCacheKey, value: &ImageInner) -> usize { match value { ImageInner::None => 0, - ImageInner::EmbeddedImage { buffer, .. } => match buffer { + ImageInner::EmbeddedImage(embedded) => match &embedded.buffer { SharedImageBuffer::RGB8(pixels) => pixels.as_bytes().len(), SharedImageBuffer::RGBA8(pixels) => pixels.as_bytes().len(), SharedImageBuffer::RGBA8Premultiplied(pixels) => pixels.as_bytes().len(), @@ -103,10 +103,12 @@ impl ImageCache { None }, |image| { - Some(ImageInner::EmbeddedImage { + use crate::graphics::image::EmbeddedImage; + + Some(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key, buffer: dynamic_image_to_shared_image_buffer(image), - }) + }))) }, ) }); @@ -142,10 +144,10 @@ impl ImageCache { }; match maybe_image { - Ok(image) => Some(ImageInner::EmbeddedImage { + Ok(image) => Some(ImageInner::EmbeddedImage(vtable::VRc::new(EmbeddedImage { cache_key, buffer: dynamic_image_to_shared_image_buffer(image), - }), + }))), Err(decode_err) => { crate::debug_log!("Error decoding embedded image: {}", decode_err); None diff --git a/internal/core/graphics/wgpu_27.rs b/internal/core/graphics/wgpu_27.rs index d7c0e5c4b6d..eb0d3bcd095 100644 --- a/internal/core/graphics/wgpu_27.rs +++ b/internal/core/graphics/wgpu_27.rs @@ -130,7 +130,7 @@ pub mod api { return Err(Self::Error::InvalidUsage); } Ok(Self(super::super::ImageInner::WGPUTexture( - super::super::WGPUTexture::WGPU27Texture(texture), + super::super::WGPUTexture::WGPU27Texture(texture).into(), ))) } } diff --git a/internal/core/graphics/wgpu_28.rs b/internal/core/graphics/wgpu_28.rs index 63677312fbb..2487dc0a8b0 100644 --- a/internal/core/graphics/wgpu_28.rs +++ b/internal/core/graphics/wgpu_28.rs @@ -129,9 +129,9 @@ pub mod api { { return Err(Self::Error::InvalidUsage); } - Ok(Self(super::super::ImageInner::WGPUTexture( + Ok(Self(super::super::ImageInner::WGPUTexture(vtable::VRc::new( super::super::WGPUTexture::WGPU28Texture(texture), - ))) + )))) } } diff --git a/internal/renderers/femtovg/images.rs b/internal/renderers/femtovg/images.rs index 6a90316fd81..714a5176bef 100644 --- a/internal/renderers/femtovg/images.rs +++ b/internal/renderers/femtovg/images.rs @@ -188,9 +188,11 @@ impl Texture { .unwrap() } #[cfg(all(not(target_arch = "wasm32"), feature = "unstable-wgpu-28"))] - ImageInner::WGPUTexture(i_slint_core::graphics::WGPUTexture::WGPU28Texture( - texture, - )) => { + #[cfg_attr(not(feature = "unstable-wgpu-27"), expect(irrefutable_let_patterns))] + ImageInner::WGPUTexture(wgpu_texture) + if let i_slint_core::graphics::WGPUTexture::WGPU28Texture(texture) = + &**wgpu_texture => + { let texture = texture.clone(); let size = texture.size(); diff --git a/internal/renderers/skia/cached_image.rs b/internal/renderers/skia/cached_image.rs index 918e59f3755..c7c78e88719 100644 --- a/internal/renderers/skia/cached_image.rs +++ b/internal/renderers/skia/cached_image.rs @@ -39,13 +39,16 @@ pub(crate) fn as_skia_image( let image_inner: &ImageInner = (&image).into(); match image_inner { ImageInner::None => None, - ImageInner::EmbeddedImage { buffer, cache_key } => { - let result = image_buffer_to_skia_image(buffer); + ImageInner::EmbeddedImage(embedded) => { + let result = image_buffer_to_skia_image(&embedded.buffer); if let Some(img) = result.as_ref() { core_cache::replace_cached_image( - cache_key.clone(), + embedded.cache_key.clone(), ImageInner::BackendStorage(vtable::VRc::into_dyn(vtable::VRc::new( - SkiaCachedImage { image: img.clone(), cache_key: cache_key.clone() }, + SkiaCachedImage { + image: img.clone(), + cache_key: embedded.cache_key.clone(), + }, ))), ) } diff --git a/tests/cases/issues/issue_2608_canon_img_path.slint b/tests/cases/issues/issue_2608_canon_img_path.slint index 933b022f585..11fa2bb6ab6 100644 --- a/tests/cases/issues/issue_2608_canon_img_path.slint +++ b/tests/cases/issues/issue_2608_canon_img_path.slint @@ -20,10 +20,16 @@ let img1_inner: &ImageInner = (&img1).into(); let img2_inner: &ImageInner = (&img2).into(); match (img1_inner, img2_inner) { - (ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(img1_path), .. }, ImageInner::EmbeddedImage { cache_key: ImageCacheKey::Path(img2_path), .. }) => { + (ImageInner::EmbeddedImage(img1), ImageInner::EmbeddedImage(img2)) + if let ImageCacheKey::Path(img1_path) = &img1.cache_key + && let ImageCacheKey::Path(img2_path) = &img2.cache_key => + { assert_eq!(img1_path, img2_path) }, - (ImageInner::EmbeddedImage { cache_key: ImageCacheKey::EmbeddedData(img1_addr), .. }, ImageInner::EmbeddedImage { cache_key: ImageCacheKey::EmbeddedData(img2_addr), .. }) => { + (ImageInner::EmbeddedImage(img1), ImageInner::EmbeddedImage(img2)) + if let ImageCacheKey::EmbeddedData(img1_addr) = &img1.cache_key + && let ImageCacheKey::EmbeddedData(img2_addr) = &img2.cache_key => + { assert_eq!(img1_addr, img2_addr) }, _ => todo!("adjust this test to new image comparison"),