use crate::compat::String;
use crate::widget::display_widgets::icon::{IconData, IconName};
#[cfg(all(feature = "image", not(alloc_frugal)))]
use alloc::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IconSpec {
Builtin(IconName),
Resource {
id: alloc::string::String,
color_mode: IconColorMode,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconColorMode {
Monochrome,
Original,
}
pub trait IconResourceResolver {
fn resolve_icon(
&self,
resource_id: &str,
color_mode: IconColorMode,
) -> Result<IconSource, String>;
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedIconPixels {
pixels: Arc<Vec<u8>>,
width: u32,
height: u32,
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
impl SharedIconPixels {
pub fn from_rgba8(pixels: Vec<u8>, width: u32, height: u32) -> Result<Self, String> {
Self::from_shared_rgba8(Arc::new(pixels), width, height)
}
pub fn from_shared_rgba8(
pixels: Arc<Vec<u8>>,
width: u32,
height: u32,
) -> Result<Self, String> {
if width == 0 || height == 0 {
return Err(format!("shared icon dimensions must be non-zero, got {width}x{height}"));
}
let expected = (width as usize)
.checked_mul(height as usize)
.and_then(|pixel_count| pixel_count.checked_mul(4))
.ok_or_else(|| {
format!("shared icon RGBA8 size overflows usize for {width}x{height}")
})?;
if pixels.len() != expected {
return Err(format!(
"shared icon has {} RGBA8 bytes but {width}x{height} requires {expected}",
pixels.len()
));
}
Ok(Self { pixels, width, height })
}
pub fn from_image(image: crate::image::Image) -> Result<Self, String> {
let width = image.width();
let height = image.height();
let pixels = match image.inner.data {
crate::image::ImageData::Rgba8(pixels) => pixels,
data => match data.to_rgba8(width, height) {
crate::image::ImageData::Rgba8(pixels) => pixels,
_ => return Err("image conversion did not produce RGBA8 pixels".to_string()),
},
};
Self::from_rgba8(pixels, width, height)
}
pub fn from_cached_bytes(data: &[u8]) -> Option<Self> {
Self::from_cached(crate::image::cache::pixels_rgba8_sized_or_none(data)?)
}
pub fn from_cached_file(path: impl AsRef<std::path::Path>) -> Option<Self> {
Self::from_cached(crate::image::cache::file_rgba8_sized_or_none(path)?)
}
fn from_cached((pixels, width, height): (Arc<Vec<u8>>, u32, u32)) -> Option<Self> {
match Self::from_shared_rgba8(pixels, width, height) {
Ok(handle) => Some(handle),
Err(error) => {
log::error!("decode cache produced an invalid shared icon ({error})");
None
}
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn pixels(&self) -> &[u8] {
self.pixels.as_slice()
}
pub fn fit_into(&self, slot: crate::core::Rect) -> crate::core::Rect {
fit_image_into(self.width, self.height, slot)
}
pub fn shares_allocation_with(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.pixels, &other.pixels)
}
}
pub fn fit_image_into(
source_width: u32,
source_height: u32,
slot: crate::core::Rect,
) -> crate::core::Rect {
if slot.width == 0 || slot.height == 0 || source_width == 0 || source_height == 0 {
return slot;
}
let scale =
(slot.width as f32 / source_width as f32).min(slot.height as f32 / source_height as f32);
let width = ((source_width as f32 * scale).round() as u32).clamp(1, slot.width);
let height = ((source_height as f32 * scale).round() as u32).clamp(1, slot.height);
crate::core::Rect::new(
slot.x + (slot.width - width) as i32 / 2,
slot.y + (slot.height - height) as i32 / 2,
width,
height,
)
}
#[cfg(feature = "image")]
pub(crate) fn normalize_rgba8_image(image: crate::image::Image) -> crate::image::Image {
let mut image = image;
if !matches!(image.inner.data, crate::image::ImageData::Rgba8(_)) {
let width = image.width();
let height = image.height();
image.inner.data = image.inner.data.to_rgba8(width, height);
}
image
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconSource {
Builtin(IconName),
StaticGeometry(&'static IconData),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IconItemError {
IndexOutOfBounds {
index: usize,
len: usize,
},
ItemNotFound {
id: u64,
},
AmbiguousItemId {
id: u64,
},
UnknownItemId {
id: crate::compat::String,
},
DuplicateItemId {
id: crate::compat::String,
},
}
impl IconSource {
pub const fn builtin(name: IconName) -> Self {
Self::Builtin(name)
}
pub const fn static_geometry(data: &'static IconData) -> Self {
Self::StaticGeometry(data)
}
}
impl IconSpec {
pub fn builtin_source(&self) -> Option<IconSource> {
match self {
Self::Builtin(name) => Some(IconSource::Builtin(*name)),
Self::Resource { .. } => None,
}
}
}
#[derive(Debug, Default)]
pub struct IconResourceBinding {
in_flight: Option<(u64, u64)>,
next_request: u64,
ready: Option<(u64, IconSource)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IconLoadOutcome {
Applied(IconSource),
Failed {
kept: Option<IconSource>,
diagnostic: crate::compat::String,
},
Stale,
}
impl IconResourceBinding {
pub const fn new() -> Self {
Self { in_flight: None, next_request: 0, ready: None }
}
pub fn begin(&mut self, revision: u64) -> u64 {
self.next_request += 1;
self.in_flight = Some((self.next_request, revision));
self.next_request
}
pub fn complete(
&mut self,
request_id: u64,
revision: u64,
result: Result<IconSource, crate::compat::String>,
) -> IconLoadOutcome {
if self.in_flight != Some((request_id, revision)) {
return IconLoadOutcome::Stale;
}
self.in_flight = None;
match result {
Ok(source) => {
if let Some((ready_revision, _)) = self.ready {
if revision < ready_revision {
return IconLoadOutcome::Stale;
}
}
self.ready = Some((revision, source));
IconLoadOutcome::Applied(source)
}
Err(diagnostic) => {
let kept = self.ready.as_ref().map(|(_, source)| *source);
IconLoadOutcome::Failed { kept, diagnostic }
}
}
}
pub fn current(&self) -> Option<IconSource> {
self.ready.as_ref().map(|(_, source)| *source)
}
pub fn revision(&self) -> Option<u64> {
self.ready.as_ref().map(|(revision, _)| *revision)
}
pub fn is_loading(&self) -> bool {
self.in_flight.is_some()
}
pub fn clear(&mut self) {
self.in_flight = None;
self.ready = None;
}
}
#[cfg(all(test, feature = "image", not(alloc_frugal)))]
mod shared_pixels_tests {
use super::SharedIconPixels;
use crate::image::Image;
#[test]
fn owned_rgba_image_transfers_its_pixel_allocation() {
let bytes = vec![7; 2 * 3 * 4];
let original_ptr = bytes.as_ptr();
let image = Image::from_rgba(bytes, 2, 3);
let shared = SharedIconPixels::from_image(image).expect("valid RGBA image");
assert_eq!(shared.width(), 2);
assert_eq!(shared.height(), 3);
assert_eq!(shared.pixels().as_ptr(), original_ptr, "RGBA bytes must be moved, not copied");
let clone = shared.clone();
assert!(shared.shares_allocation_with(&clone));
}
#[test]
fn shared_rgba_constructor_rejects_invalid_dimensions_and_payloads() {
assert!(SharedIconPixels::from_rgba8(Vec::new(), 0, 1).is_err());
assert!(SharedIconPixels::from_rgba8(vec![0; 3], 1, 1).is_err());
assert!(SharedIconPixels::from_rgba8(Vec::new(), u32::MAX, u32::MAX).is_err());
}
fn non_square_png() -> Vec<u8> {
use crate::image::format::{DecodedImage, ImageData, ImageFormat};
let decoded =
DecodedImage::new(ImageFormat::Rgba8, ImageData::Rgba8(vec![9; 4 * 2 * 4]), 4, 2);
crate::image::encode(&decoded, ImageFormat::Png).expect("the encoder produces a PNG")
}
#[test]
fn cached_pixels_become_a_sized_handle_without_copying() {
let png = non_square_png();
let first = SharedIconPixels::from_cached_bytes(&png).expect("the PNG decodes");
assert_eq!((first.width(), first.height()), (4, 2), "the decoded extent is kept");
let second = SharedIconPixels::from_cached_bytes(&png).expect("the PNG is cached");
assert!(first.shares_allocation_with(&second));
assert_eq!(first.pixels().len(), 4 * 2 * 4);
assert!(
SharedIconPixels::from_cached_bytes(b"not an image at all").is_none(),
"undecodable bytes produce no handle"
);
}
#[test]
fn fit_into_preserves_the_aspect_ratio_and_centres_the_image() {
use crate::core::Rect;
let wide = SharedIconPixels::from_rgba8(vec![0; 8 * 4 * 4], 8, 4).expect("valid RGBA");
let slot = Rect::new(10, 20, 20, 20);
let fitted = wide.fit_into(slot);
assert_eq!((fitted.width, fitted.height), (20, 10));
assert_eq!((fitted.x, fitted.y), (10, 25));
let square = SharedIconPixels::from_rgba8(vec![0; 4 * 4 * 4], 4, 4).expect("valid RGBA");
assert_eq!(square.fit_into(slot), slot);
assert_eq!(square.fit_into(Rect::new(0, 0, 0, 0)), Rect::new(0, 0, 0, 0));
}
}
#[cfg(test)]
mod binding_tests {
use super::{IconLoadOutcome, IconResourceBinding, IconSource};
use crate::widget::IconName;
fn source(name: IconName) -> IconSource {
IconSource::Builtin(name)
}
#[test]
fn a_superseded_result_is_not_applied_when_it_finishes_last() {
let mut binding = IconResourceBinding::new();
let request_a = binding.begin(1);
let request_b = binding.begin(2);
assert!(binding.is_loading());
assert_eq!(
binding.complete(request_b, 2, Ok(source(IconName::Save))),
IconLoadOutcome::Applied(source(IconName::Save))
);
assert_eq!(
binding.complete(request_a, 1, Ok(source(IconName::Search))),
IconLoadOutcome::Stale,
"a superseded request must be dropped, not applied and then corrected"
);
assert_eq!(binding.current(), Some(source(IconName::Save)));
assert_eq!(binding.revision(), Some(2));
assert!(!binding.is_loading(), "the completed request is no longer in flight");
}
#[test]
fn an_older_revision_does_not_replace_newer_content() {
let mut binding = IconResourceBinding::new();
let first = binding.begin(7);
assert_eq!(
binding.complete(first, 7, Ok(source(IconName::Save))),
IconLoadOutcome::Applied(source(IconName::Save))
);
let stale = binding.begin(6);
assert_eq!(
binding.complete(stale, 6, Ok(source(IconName::Search))),
IconLoadOutcome::Stale,
"the resource went backwards; the newer render is what the user already saw"
);
assert_eq!(binding.revision(), Some(7));
}
#[test]
fn a_failed_update_keeps_the_last_valid_content_until_an_explicit_clear() {
let mut binding = IconResourceBinding::new();
let first = binding.begin(1);
assert_eq!(
binding.complete(first, 1, Err(crate::compat::String::from("missing file"))),
IconLoadOutcome::Failed {
kept: None,
diagnostic: crate::compat::String::from("missing file"),
}
);
assert_eq!(binding.current(), None);
let good = binding.begin(2);
assert_eq!(
binding.complete(good, 2, Ok(source(IconName::Save))),
IconLoadOutcome::Applied(source(IconName::Save))
);
let failed_update = binding.begin(3);
assert_eq!(
binding.complete(failed_update, 3, Err(crate::compat::String::from("io error"))),
IconLoadOutcome::Failed {
kept: Some(source(IconName::Save)),
diagnostic: crate::compat::String::from("io error"),
}
);
assert_eq!(binding.current(), Some(source(IconName::Save)), "last-valid must survive");
let in_flight = binding.begin(4);
binding.clear();
assert_eq!(binding.current(), None, "an explicit clear must really remove the icon");
assert!(!binding.is_loading(), "clearing invalidates the in-flight request");
assert_eq!(
binding.complete(in_flight, 4, Ok(source(IconName::Search))),
IconLoadOutcome::Stale,
"a result that was already on its way must not undo the clear"
);
assert_eq!(binding.current(), None);
}
#[test]
fn a_fresh_binding_holds_nothing() {
let binding = IconResourceBinding::new();
assert_eq!(binding.current(), None);
assert_eq!(binding.revision(), None);
assert!(!binding.is_loading());
}
}