Skip to main content

gpui_rhai/
asset.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt::Write as _;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{Receiver, Sender, channel};
9use std::sync::{Arc, LazyLock};
10
11use gpui::{AppContext as _, Image, ImageCacheError, ImageFormat, ImageSource, RenderImage};
12use rhai::{CustomType, TypeBuilder};
13use thiserror::Error;
14
15use crate::{
16    AsyncDelivery, AsyncScope, ComponentInstancePath, OpaqueHandle, Rgba8, ScriptCallback,
17    ScriptGeneration, UiValue,
18};
19
20const MAX_SVG_RASTER_DIMENSION: u32 = 16_384;
21const MAX_SVG_RASTER_DIMENSION_F32: f32 = 16_384.0;
22const MAX_SVG_RASTER_PIXELS: u64 = 16_777_216;
23const SVG_VARIANT_CACHE_MAX_ENTRIES: usize = 256;
24const SVG_VARIANT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
25
26#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct AssetId(String);
28
29impl AssetId {
30    /// Parse a namespaced logical asset ID such as `core/check`.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`AssetError::InvalidId`] for URLs, absolute/traversal paths, or
35    /// unsupported characters.
36    pub fn parse(value: impl Into<String>) -> Result<Self, AssetError> {
37        let value = value.into();
38        let segments = value.split('/').collect::<Vec<_>>();
39        let valid = segments.len() >= 2
40            && segments.iter().all(|segment| {
41                !segment.is_empty()
42                    && *segment != "."
43                    && *segment != ".."
44                    && segment.chars().all(|character| {
45                        character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
46                    })
47            });
48        if valid && !value.contains(':') && !value.contains('\\') {
49            Ok(Self(value))
50        } else {
51            Err(AssetError::InvalidId(value))
52        }
53    }
54
55    #[must_use]
56    pub fn as_str(&self) -> &str {
57        &self.0
58    }
59
60    fn split(&self) -> (&str, &str) {
61        self.0.split_once('/').expect("validated asset ID")
62    }
63}
64
65impl CustomType for AssetId {
66    fn build(mut builder: TypeBuilder<Self>) {
67        builder
68            .with_name("AssetId")
69            .with_fn("to_string", |asset: &mut Self| asset.0.clone());
70    }
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct AssetData {
75    pub mime_type: String,
76    pub bytes: Vec<u8>,
77}
78
79pub trait AssetProvider {
80    /// Load a provider-relative logical asset name.
81    ///
82    /// # Errors
83    ///
84    /// Returns a provider error without exposing arbitrary filesystem access to
85    /// Rhai.
86    fn load(&self, name: &str) -> Result<AssetData, String>;
87}
88
89#[derive(Clone, Debug, Default)]
90pub struct InMemoryAssetProvider {
91    assets: BTreeMap<String, AssetData>,
92}
93
94impl InMemoryAssetProvider {
95    #[must_use]
96    pub fn new(assets: BTreeMap<String, AssetData>) -> Self {
97        Self { assets }
98    }
99}
100
101impl AssetProvider for InMemoryAssetProvider {
102    fn load(&self, name: &str) -> Result<AssetData, String> {
103        self.assets
104            .get(name)
105            .cloned()
106            .ok_or_else(|| format!("asset `{name}` does not exist"))
107    }
108}
109
110#[derive(Clone, Debug)]
111pub struct DirectoryAssetProvider {
112    root: PathBuf,
113}
114
115impl DirectoryAssetProvider {
116    /// Restrict a provider to an existing canonical directory.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`AssetError::Io`] when the root cannot be canonicalized.
121    pub fn new(root: impl AsRef<Path>) -> Result<Self, AssetError> {
122        let path = root.as_ref();
123        Ok(Self {
124            root: path.canonicalize().map_err(|source| AssetError::Io {
125                path: path.to_path_buf(),
126                source,
127            })?,
128        })
129    }
130}
131
132impl AssetProvider for DirectoryAssetProvider {
133    fn load(&self, name: &str) -> Result<AssetData, String> {
134        let direct = self.root.join(name);
135        let path = if direct.exists() {
136            direct
137        } else {
138            [
139                "svg", "png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff",
140            ]
141            .into_iter()
142            .map(|extension| self.root.join(name).with_extension(extension))
143            .find(|candidate| candidate.exists())
144            .ok_or_else(|| format!("asset `{name}` does not exist"))?
145        };
146        let canonical = path.canonicalize().map_err(|error| error.to_string())?;
147        if !canonical.starts_with(&self.root) {
148            return Err("asset escaped provider root".to_owned());
149        }
150        let mime_type = mime_for_path(&canonical)
151            .ok_or_else(|| "asset has an unsupported image extension".to_owned())?;
152        let bytes = fs::read(&canonical).map_err(|error| error.to_string())?;
153        Ok(AssetData {
154            mime_type: mime_type.to_owned(),
155            bytes,
156        })
157    }
158}
159
160fn mime_for_path(path: &Path) -> Option<&'static str> {
161    match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
162        "png" => Some("image/png"),
163        "jpg" | "jpeg" => Some("image/jpeg"),
164        "webp" => Some("image/webp"),
165        "gif" => Some("image/gif"),
166        "svg" => Some("image/svg+xml"),
167        "bmp" => Some("image/bmp"),
168        "tif" | "tiff" => Some("image/tiff"),
169        _ => None,
170    }
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct ImageHandle {
175    opaque: OpaqueHandle,
176    asset: AssetId,
177}
178
179impl ImageHandle {
180    #[must_use]
181    pub fn opaque(&self) -> &OpaqueHandle {
182        &self.opaque
183    }
184
185    #[must_use]
186    pub fn asset(&self) -> &AssetId {
187        &self.asset
188    }
189}
190
191#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct ImageDecodeHandle(u64);
193
194impl CustomType for ImageDecodeHandle {
195    fn build(mut builder: TypeBuilder<Self>) {
196        builder
197            .with_name("ImageDecodeHandle")
198            .with_fn("to_string", |handle: &mut Self| {
199                format!("image-decode#{}", handle.0)
200            });
201    }
202}
203
204#[derive(Clone, Debug)]
205struct PendingImageDecode {
206    asset: AssetId,
207    scope: AsyncScope,
208    generation: ScriptGeneration,
209    success: ScriptCallback,
210    error: ScriptCallback,
211    canceled: Arc<AtomicBool>,
212}
213
214struct ImageDecodeMessage {
215    id: u64,
216    result: Result<PreparedImageData, String>,
217}
218
219struct PreparedImageData {
220    data: StoredAssetData,
221    image: Arc<Image>,
222}
223
224#[derive(Clone, Debug)]
225struct StoredAssetData {
226    mime_type: String,
227    content: StoredAssetContent,
228}
229
230#[derive(Clone, Debug)]
231enum StoredAssetContent {
232    Svg(Arc<str>),
233    Raster,
234}
235
236impl StoredAssetData {
237    fn from_validated(data: AssetData, format: ImageFormat) -> Result<Self, AssetError> {
238        let content = if format == ImageFormat::Svg {
239            StoredAssetContent::Svg(Arc::from(
240                String::from_utf8(data.bytes).map_err(|_| AssetError::InvalidSvg)?,
241            ))
242        } else {
243            StoredAssetContent::Raster
244        };
245        Ok(Self {
246            mime_type: data.mime_type,
247            content,
248        })
249    }
250
251    fn svg_source(&self) -> Option<Arc<str>> {
252        match &self.content {
253            StoredAssetContent::Svg(source) => Some(Arc::clone(source)),
254            StoredAssetContent::Raster => None,
255        }
256    }
257}
258
259#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
260struct SvgRasterSource {
261    source: Arc<str>,
262    color: Option<u32>,
263}
264
265impl SvgRasterSource {
266    fn new(source: Arc<str>, color: Option<Rgba8>) -> Self {
267        Self {
268            source,
269            color: color.map(Rgba8::as_rgba_hex),
270        }
271    }
272
273    fn source_bytes(&self) -> usize {
274        self.source.len()
275    }
276
277    fn color(&self) -> Option<Rgba8> {
278        self.color.map(Rgba8::from_rgba_hex)
279    }
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
283pub struct SvgCacheStats {
284    pub entries: usize,
285    pub bytes: usize,
286    pub max_entries: usize,
287    pub max_bytes: usize,
288    pub hits: u64,
289    pub misses: u64,
290    pub evictions: u64,
291}
292
293#[derive(Clone, Debug)]
294struct SvgVariantCacheEntry {
295    bytes: usize,
296    last_access: u64,
297    state: SvgVariantCacheState,
298}
299
300#[derive(Clone, Debug)]
301enum SvgVariantCacheState {
302    Pending {
303        token: u64,
304        canceled: Arc<AtomicBool>,
305    },
306    Ready(Result<Arc<RenderImage>, ImageCacheError>),
307}
308
309struct SvgVariantMessage {
310    source: SvgRasterSource,
311    token: u64,
312    result: Result<Arc<RenderImage>, ImageCacheError>,
313}
314
315struct SvgVariantCache {
316    entries: BTreeMap<SvgRasterSource, SvgVariantCacheEntry>,
317    bytes: usize,
318    max_entries: usize,
319    max_bytes: usize,
320    access_clock: u64,
321    next_token: u64,
322    hits: u64,
323    misses: u64,
324    evictions: u64,
325    sender: Sender<SvgVariantMessage>,
326    receiver: Receiver<SvgVariantMessage>,
327}
328
329impl Default for SvgVariantCache {
330    fn default() -> Self {
331        let (sender, receiver) = channel();
332        Self {
333            entries: BTreeMap::new(),
334            bytes: 0,
335            max_entries: SVG_VARIANT_CACHE_MAX_ENTRIES,
336            max_bytes: SVG_VARIANT_CACHE_MAX_BYTES,
337            access_clock: 0,
338            next_token: 1,
339            hits: 0,
340            misses: 0,
341            evictions: 0,
342            sender,
343            receiver,
344        }
345    }
346}
347
348impl SvgVariantCache {
349    #[cfg(test)]
350    fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
351        let mut cache = Self::default();
352        cache.max_entries = max_entries;
353        cache.max_bytes = max_bytes;
354        cache
355    }
356
357    fn drain(&mut self) {
358        while let Ok(message) = self.receiver.try_recv() {
359            let Some(entry) = self.entries.get_mut(&message.source) else {
360                continue;
361            };
362            let SvgVariantCacheState::Pending { token, canceled } = &entry.state else {
363                continue;
364            };
365            if *token != message.token || canceled.load(Ordering::Acquire) {
366                continue;
367            }
368            let rendered_bytes = message
369                .result
370                .as_ref()
371                .ok()
372                .and_then(|image| image.as_bytes(0))
373                .map_or(0, <[u8]>::len);
374            let new_bytes = message.source.source_bytes().saturating_add(rendered_bytes);
375            self.bytes = self.bytes.saturating_sub(entry.bytes);
376            entry.bytes = new_bytes;
377            entry.state = SvgVariantCacheState::Ready(message.result);
378            self.bytes = self.bytes.saturating_add(new_bytes);
379        }
380    }
381
382    fn clear(&mut self) {
383        for entry in self.entries.values() {
384            if let SvgVariantCacheState::Pending { canceled, .. } = &entry.state {
385                canceled.store(true, Ordering::Release);
386            }
387        }
388        self.evictions = self
389            .evictions
390            .saturating_add(u64::try_from(self.entries.len()).unwrap_or(u64::MAX));
391        self.entries.clear();
392        self.bytes = 0;
393        while self.receiver.try_recv().is_ok() {}
394    }
395
396    fn access(&mut self, source: &SvgRasterSource) -> SvgVariantAccess {
397        self.drain();
398        self.access_clock = self.access_clock.saturating_add(1);
399        if let Some(entry) = self.entries.get_mut(source) {
400            entry.last_access = self.access_clock;
401            self.hits = self.hits.saturating_add(1);
402            let access = match &entry.state {
403                SvgVariantCacheState::Pending { .. } => SvgVariantAccess::Pending,
404                SvgVariantCacheState::Ready(result) => SvgVariantAccess::Ready(result.clone()),
405            };
406            self.enforce_limits(Some(source));
407            return access;
408        }
409        let bytes = source.source_bytes();
410        let token = self.next_token;
411        self.next_token = self.next_token.saturating_add(1);
412        let canceled = Arc::new(AtomicBool::new(false));
413        self.entries.insert(
414            source.clone(),
415            SvgVariantCacheEntry {
416                bytes,
417                last_access: self.access_clock,
418                state: SvgVariantCacheState::Pending {
419                    token,
420                    canceled: Arc::clone(&canceled),
421                },
422            },
423        );
424        self.bytes = self.bytes.saturating_add(bytes);
425        self.misses = self.misses.saturating_add(1);
426        self.enforce_limits(Some(source));
427        SvgVariantAccess::Start {
428            token,
429            canceled,
430            sender: self.sender.clone(),
431        }
432    }
433
434    fn enforce_limits(&mut self, protected: Option<&SvgRasterSource>) {
435        while self.entries.len() > self.max_entries || self.bytes > self.max_bytes {
436            let candidate = self
437                .entries
438                .iter()
439                .filter(|(source, _)| protected != Some(*source))
440                .min_by_key(|(_, entry)| entry.last_access)
441                .map(|(source, _)| source.clone())
442                .or_else(|| protected.cloned());
443            let Some(candidate) = candidate else {
444                break;
445            };
446            let Some(entry) = self.entries.remove(&candidate) else {
447                break;
448            };
449            if let SvgVariantCacheState::Pending { canceled, .. } = entry.state {
450                canceled.store(true, Ordering::Release);
451            }
452            self.bytes = self.bytes.saturating_sub(entry.bytes);
453            self.evictions = self.evictions.saturating_add(1);
454        }
455    }
456
457    fn stats(&mut self) -> SvgCacheStats {
458        self.drain();
459        self.enforce_limits(None);
460        SvgCacheStats {
461            entries: self.entries.len(),
462            bytes: self.bytes,
463            max_entries: self.max_entries,
464            max_bytes: self.max_bytes,
465            hits: self.hits,
466            misses: self.misses,
467            evictions: self.evictions,
468        }
469    }
470}
471
472impl Drop for SvgVariantCache {
473    fn drop(&mut self) {
474        self.clear();
475    }
476}
477
478enum SvgVariantAccess {
479    Start {
480        token: u64,
481        canceled: Arc<AtomicBool>,
482        sender: Sender<SvgVariantMessage>,
483    },
484    Pending,
485    Ready(Result<Arc<RenderImage>, ImageCacheError>),
486}
487
488#[derive(Clone, Debug)]
489pub(crate) struct ImageDecodeSnapshot {
490    pending: BTreeMap<u64, PendingImageDecode>,
491    deferred: BTreeMap<u64, PendingImageDecode>,
492    transaction_depth: usize,
493}
494
495fn defer_or_cancel_decode(inner: &mut AssetRegistryInner, id: u64, pending: PendingImageDecode) {
496    if inner.transaction_depth > 0 {
497        inner.deferred_decode_cancellations.insert(id, pending);
498    } else {
499        pending.canceled.store(true, Ordering::Release);
500    }
501}
502
503fn cancel_decode_ids(inner: &mut AssetRegistryInner, ids: impl IntoIterator<Item = u64>) {
504    for id in ids {
505        if let Some(pending) = inner.pending_decodes.remove(&id) {
506            defer_or_cancel_decode(inner, id, pending);
507        }
508    }
509}
510
511struct AssetRegistryInner {
512    providers: BTreeMap<String, Box<dyn AssetProvider>>,
513    by_asset: BTreeMap<AssetId, ImageHandle>,
514    images: BTreeMap<u64, Arc<Image>>,
515    image_data: BTreeMap<u64, StoredAssetData>,
516    svg_variants: SvgVariantCache,
517    next_id: u64,
518    next_decode_id: u64,
519    pending_decodes: BTreeMap<u64, PendingImageDecode>,
520    deferred_decode_cancellations: BTreeMap<u64, PendingImageDecode>,
521    transaction_depth: usize,
522    decode_sender: Sender<ImageDecodeMessage>,
523    decode_receiver: Receiver<ImageDecodeMessage>,
524}
525
526impl Default for AssetRegistryInner {
527    fn default() -> Self {
528        let (decode_sender, decode_receiver) = channel();
529        Self {
530            providers: BTreeMap::new(),
531            by_asset: BTreeMap::new(),
532            images: BTreeMap::new(),
533            image_data: BTreeMap::new(),
534            svg_variants: SvgVariantCache::default(),
535            next_id: 0,
536            next_decode_id: 1,
537            pending_decodes: BTreeMap::new(),
538            deferred_decode_cancellations: BTreeMap::new(),
539            transaction_depth: 0,
540            decode_sender,
541            decode_receiver,
542        }
543    }
544}
545
546#[derive(Clone, Default)]
547pub struct AssetRegistry {
548    inner: Rc<RefCell<AssetRegistryInner>>,
549}
550
551impl std::fmt::Debug for AssetRegistry {
552    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553        match self.inner.try_borrow() {
554            Ok(inner) => formatter
555                .debug_struct("AssetRegistry")
556                .field("providers", &inner.providers.keys().collect::<Vec<_>>())
557                .field("cached", &inner.by_asset.keys().collect::<Vec<_>>())
558                .finish_non_exhaustive(),
559            Err(_) => formatter.write_str("AssetRegistry(<borrowed>)"),
560        }
561    }
562}
563
564impl AssetRegistry {
565    pub(crate) fn begin_transaction(&self) -> Result<ImageDecodeSnapshot, AssetError> {
566        let snapshot = self.decode_snapshot()?;
567        let mut inner = self
568            .inner
569            .try_borrow_mut()
570            .map_err(|_| AssetError::Borrowed)?;
571        inner.transaction_depth = inner.transaction_depth.saturating_add(1);
572        Ok(snapshot)
573    }
574
575    pub(crate) fn commit_transaction(&self) -> Result<(), AssetError> {
576        let mut inner = self
577            .inner
578            .try_borrow_mut()
579            .map_err(|_| AssetError::Borrowed)?;
580        inner.transaction_depth = inner.transaction_depth.saturating_sub(1);
581        if inner.transaction_depth == 0 {
582            for pending in std::mem::take(&mut inner.deferred_decode_cancellations).into_values() {
583                pending.canceled.store(true, Ordering::Release);
584            }
585        }
586        Ok(())
587    }
588
589    #[must_use]
590    pub fn new() -> Self {
591        Self::default()
592    }
593
594    /// Register one logical asset namespace.
595    ///
596    /// # Errors
597    ///
598    /// Returns [`AssetError`] for invalid or duplicate namespaces.
599    pub fn register(
600        &self,
601        namespace: impl Into<String>,
602        provider: impl AssetProvider + 'static,
603    ) -> Result<(), AssetError> {
604        let namespace = namespace.into();
605        if !valid_segment(&namespace) {
606            return Err(AssetError::InvalidNamespace(namespace));
607        }
608        let mut inner = self
609            .inner
610            .try_borrow_mut()
611            .map_err(|_| AssetError::Borrowed)?;
612        if inner.providers.contains_key(&namespace) {
613            return Err(AssetError::DuplicateNamespace(namespace));
614        }
615        inner.providers.insert(namespace, Box::new(provider));
616        Ok(())
617    }
618
619    /// Reload every cached image in a provider namespace while preserving
620    /// existing opaque handle identities.
621    ///
622    /// # Errors
623    ///
624    /// Returns provider, MIME, empty-data, or borrow errors. Validation is
625    /// transactional: existing cached bytes remain active on failure.
626    pub fn refresh_namespace(&self, namespace: &str) -> Result<usize, AssetError> {
627        let refreshed = {
628            let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
629            let provider = inner
630                .providers
631                .get(namespace)
632                .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?;
633            inner
634                .by_asset
635                .iter()
636                .filter(|(asset, _)| asset.split().0 == namespace)
637                .map(|(asset, handle)| {
638                    let (_, name) = asset.split();
639                    let data = provider
640                        .load(name)
641                        .map_err(|message| AssetError::Provider {
642                            asset: asset.clone(),
643                            message,
644                        })?;
645                    let format = ImageFormat::from_mime_type(&data.mime_type)
646                        .ok_or_else(|| AssetError::UnsupportedMime(data.mime_type.clone()))?;
647                    if data.bytes.is_empty() {
648                        return Err(AssetError::Empty(asset.clone()));
649                    }
650                    let image = prepare_image(format, &data.bytes, None)?;
651                    let stored = StoredAssetData::from_validated(data, format)?;
652                    Ok((handle.opaque.id(), stored, image))
653                })
654                .collect::<Result<Vec<_>, AssetError>>()?
655        };
656        let mut inner = self
657            .inner
658            .try_borrow_mut()
659            .map_err(|_| AssetError::Borrowed)?;
660        for (id, data, image) in &refreshed {
661            inner.images.insert(*id, Arc::clone(image));
662            inner.image_data.insert(*id, data.clone());
663        }
664        if !refreshed.is_empty() {
665            inner.svg_variants.clear();
666        }
667        Ok(refreshed.len())
668    }
669
670    /// Load, validate, decode, and cache an image by logical ID.
671    ///
672    /// # Errors
673    ///
674    /// Returns provider, MIME, decode-boundary, or borrow errors.
675    pub fn load_image(&self, id: &AssetId) -> Result<ImageHandle, AssetError> {
676        if let Some(handle) = self
677            .inner
678            .try_borrow()
679            .map_err(|_| AssetError::Borrowed)?
680            .by_asset
681            .get(id)
682            .cloned()
683        {
684            return Ok(handle);
685        }
686        let (namespace, name) = id.split();
687        let data = {
688            let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
689            inner
690                .providers
691                .get(namespace)
692                .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?
693                .load(name)
694                .map_err(|message| AssetError::Provider {
695                    asset: id.clone(),
696                    message,
697                })?
698        };
699        let format = ImageFormat::from_mime_type(&data.mime_type)
700            .ok_or_else(|| AssetError::UnsupportedMime(data.mime_type.clone()))?;
701        if data.bytes.is_empty() {
702            return Err(AssetError::Empty(id.clone()));
703        }
704        let image = prepare_image(format, &data.bytes, None)?;
705        let data = StoredAssetData::from_validated(data, format)?;
706        let mut inner = self
707            .inner
708            .try_borrow_mut()
709            .map_err(|_| AssetError::Borrowed)?;
710        inner.next_id = inner.next_id.saturating_add(1);
711        let image_id = inner.next_id;
712        let handle = ImageHandle {
713            opaque: OpaqueHandle::new("image", image_id),
714            asset: id.clone(),
715        };
716        inner.images.insert(image_id, image);
717        inner.image_data.insert(image_id, data);
718        inner.by_asset.insert(id.clone(), handle.clone());
719        Ok(handle)
720    }
721
722    /// Preload a deterministic set of declared image assets.
723    ///
724    /// # Errors
725    ///
726    /// Returns the first provider/MIME/data error without hiding which asset
727    /// failed. Successfully loaded earlier entries remain cached and reusable.
728    pub fn preload_images(
729        &self,
730        ids: impl IntoIterator<Item = AssetId>,
731    ) -> Result<usize, AssetError> {
732        let mut loaded = 0;
733        for id in ids {
734            self.load_image(&id)?;
735            loaded += 1;
736        }
737        Ok(loaded)
738    }
739
740    /// Resolve a declarative asset only if preparation already cached it.
741    ///
742    /// # Errors
743    ///
744    /// Returns [`AssetError::NotPreloaded`] instead of performing provider I/O.
745    pub fn cached_image(&self, id: &AssetId) -> Result<ImageHandle, AssetError> {
746        self.inner
747            .try_borrow()
748            .map_err(|_| AssetError::Borrowed)?
749            .by_asset
750            .get(id)
751            .cloned()
752            .ok_or_else(|| AssetError::NotPreloaded(id.clone()))
753    }
754
755    /// Start background validation/decoding for a logical image while keeping
756    /// callbacks and runtime ownership on the foreground thread.
757    ///
758    /// # Errors
759    ///
760    /// Returns provider, borrow, or worker-spawn errors.
761    pub fn start_image_decode(
762        &self,
763        id: &AssetId,
764        scope: AsyncScope,
765        generation: ScriptGeneration,
766        success: ScriptCallback,
767        error: ScriptCallback,
768    ) -> Result<ImageDecodeHandle, AssetError> {
769        let (namespace, name) = id.split();
770        let data = {
771            let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
772            inner
773                .providers
774                .get(namespace)
775                .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?
776                .load(name)
777                .map_err(|message| AssetError::Provider {
778                    asset: id.clone(),
779                    message,
780                })?
781        };
782        let (decode_id, sender, canceled) = {
783            let mut inner = self
784                .inner
785                .try_borrow_mut()
786                .map_err(|_| AssetError::Borrowed)?;
787            let decode_id = inner.next_decode_id;
788            inner.next_decode_id = inner.next_decode_id.saturating_add(1);
789            let canceled = Arc::new(AtomicBool::new(false));
790            inner.pending_decodes.insert(
791                decode_id,
792                PendingImageDecode {
793                    asset: id.clone(),
794                    scope,
795                    generation,
796                    success,
797                    error,
798                    canceled: canceled.clone(),
799                },
800            );
801            (decode_id, inner.decode_sender.clone(), canceled)
802        };
803        let worker_cancel = canceled.clone();
804        if let Err(source) = std::thread::Builder::new()
805            .name("gpui-rhai-image-decode".to_owned())
806            .spawn(move || {
807                if worker_cancel.load(Ordering::Acquire) {
808                    return;
809                }
810                let result = decode_asset_data(data);
811                if !worker_cancel.load(Ordering::Acquire) {
812                    let _ = sender.send(ImageDecodeMessage {
813                        id: decode_id,
814                        result,
815                    });
816                }
817            })
818        {
819            canceled.store(true, Ordering::Release);
820            self.inner
821                .try_borrow_mut()
822                .map_err(|_| AssetError::Borrowed)?
823                .pending_decodes
824                .remove(&decode_id);
825            return Err(AssetError::DecodeSpawn(source));
826        }
827        Ok(ImageDecodeHandle(decode_id))
828    }
829
830    /// Cancel one pending decode. Completed or unknown handles return `false`.
831    ///
832    /// # Errors
833    ///
834    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
835    pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, AssetError> {
836        let mut inner = self
837            .inner
838            .try_borrow_mut()
839            .map_err(|_| AssetError::Borrowed)?;
840        let Some(pending) = inner.pending_decodes.remove(&handle.0) else {
841            return Ok(false);
842        };
843        defer_or_cancel_decode(&mut inner, handle.0, pending);
844        Ok(true)
845    }
846
847    /// Cancel pending image work owned by a closing window and its components.
848    ///
849    /// # Errors
850    ///
851    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
852    pub fn cancel_window_scope(
853        &self,
854        window: &str,
855        component: &ComponentInstancePath,
856    ) -> Result<(), AssetError> {
857        let mut inner = self
858            .inner
859            .try_borrow_mut()
860            .map_err(|_| AssetError::Borrowed)?;
861        let remove = inner
862            .pending_decodes
863            .iter()
864            .filter_map(|(id, pending)| {
865                let remove = match &pending.scope {
866                    AsyncScope::Window(id) => id == window,
867                    AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
868                        pending.scope.is_within_component(component)
869                    }
870                    AsyncScope::App => false,
871                };
872                remove.then_some(*id)
873            })
874            .collect::<Vec<_>>();
875        cancel_decode_ids(&mut inner, remove);
876        Ok(())
877    }
878
879    /// Cancel pending image work owned by a removed component subtree.
880    ///
881    /// # Errors
882    ///
883    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
884    pub fn cancel_component_scope(
885        &self,
886        component: &ComponentInstancePath,
887    ) -> Result<(), AssetError> {
888        let mut inner = self
889            .inner
890            .try_borrow_mut()
891            .map_err(|_| AssetError::Borrowed)?;
892        let remove = inner
893            .pending_decodes
894            .iter()
895            .filter_map(|(id, pending)| pending.scope.is_within_component(component).then_some(*id))
896            .collect::<Vec<_>>();
897        cancel_decode_ids(&mut inner, remove);
898        Ok(())
899    }
900
901    /// Cancel pending image work owned by one exact asynchronous scope.
902    ///
903    /// # Errors
904    ///
905    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
906    pub fn cancel_scope(&self, scope: &AsyncScope) -> Result<(), AssetError> {
907        let mut inner = self
908            .inner
909            .try_borrow_mut()
910            .map_err(|_| AssetError::Borrowed)?;
911        let remove = inner
912            .pending_decodes
913            .iter()
914            .filter_map(|(id, pending)| (&pending.scope == scope).then_some(*id))
915            .collect::<Vec<_>>();
916        cancel_decode_ids(&mut inner, remove);
917        Ok(())
918    }
919
920    /// Cancel pending decode work from obsolete script generations.
921    ///
922    /// # Errors
923    ///
924    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
925    pub fn retain_decode_generation(&self, generation: ScriptGeneration) -> Result<(), AssetError> {
926        let mut inner = self
927            .inner
928            .try_borrow_mut()
929            .map_err(|_| AssetError::Borrowed)?;
930        inner.pending_decodes.retain(|_, pending| {
931            let retain = pending.generation == generation;
932            if !retain {
933                pending.canceled.store(true, Ordering::Release);
934            }
935            retain
936        });
937        Ok(())
938    }
939
940    /// Install completed images and return generation-safe callback deliveries.
941    ///
942    /// # Errors
943    ///
944    /// Returns a borrow error if called during another asset registry access.
945    pub fn drain_image_decodes(
946        &self,
947        generation: ScriptGeneration,
948    ) -> Result<Vec<AsyncDelivery>, AssetError> {
949        let mut inner = self
950            .inner
951            .try_borrow_mut()
952            .map_err(|_| AssetError::Borrowed)?;
953        let mut messages = Vec::new();
954        while let Ok(message) = inner.decode_receiver.try_recv() {
955            messages.push(message);
956        }
957        let mut deliveries = Vec::new();
958        for message in messages {
959            let Some(pending) = inner.pending_decodes.remove(&message.id) else {
960                continue;
961            };
962            if pending.canceled.load(Ordering::Acquire) || pending.generation != generation {
963                continue;
964            }
965            let (callback, payload) = match message.result {
966                Ok(prepared) => {
967                    let handle = install_decoded_image(&mut inner, pending.asset, prepared);
968                    (pending.success, UiValue::Handle(handle.opaque().clone()))
969                }
970                Err(message) => (pending.error, UiValue::String(message)),
971            };
972            deliveries.push(AsyncDelivery {
973                callback,
974                payload,
975                scope: pending.scope,
976            });
977        }
978        Ok(deliveries)
979    }
980
981    #[must_use]
982    pub fn pending_decode_count(&self) -> usize {
983        self.inner
984            .try_borrow()
985            .map_or(0, |inner| inner.pending_decodes.len())
986    }
987
988    pub(crate) fn decode_snapshot(&self) -> Result<ImageDecodeSnapshot, AssetError> {
989        let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
990        Ok(ImageDecodeSnapshot {
991            pending: inner.pending_decodes.clone(),
992            deferred: inner.deferred_decode_cancellations.clone(),
993            transaction_depth: inner.transaction_depth,
994        })
995    }
996
997    pub(crate) fn restore_decode_snapshot(
998        &self,
999        snapshot: ImageDecodeSnapshot,
1000    ) -> Result<(), AssetError> {
1001        let mut inner = self
1002            .inner
1003            .try_borrow_mut()
1004            .map_err(|_| AssetError::Borrowed)?;
1005        for (id, pending) in inner
1006            .pending_decodes
1007            .iter()
1008            .chain(inner.deferred_decode_cancellations.iter())
1009        {
1010            if !snapshot.pending.contains_key(id) && !snapshot.deferred.contains_key(id) {
1011                pending.canceled.store(true, Ordering::Release);
1012            }
1013        }
1014        inner.pending_decodes = snapshot.pending;
1015        inner.deferred_decode_cancellations = snapshot.deferred;
1016        inner.transaction_depth = snapshot.transaction_depth;
1017        Ok(())
1018    }
1019
1020    /// Resolve an opaque image handle into a GPUI image source.
1021    ///
1022    /// # Errors
1023    ///
1024    /// Returns [`AssetError`] for wrong-kind, unknown, or borrow errors.
1025    pub fn image_source(&self, handle: &OpaqueHandle) -> Result<ImageSource, AssetError> {
1026        self.image_source_tinted(handle, None)
1027    }
1028
1029    /// Resolve an image and apply semantic `currentColor` inheritance to SVG
1030    /// bytes. Raster images reuse their original cache entry.
1031    ///
1032    /// # Errors
1033    ///
1034    /// Returns wrong-kind, unknown, malformed-SVG, or borrow errors.
1035    pub fn image_source_tinted(
1036        &self,
1037        handle: &OpaqueHandle,
1038        color: Option<Rgba8>,
1039    ) -> Result<ImageSource, AssetError> {
1040        if handle.kind() != "image" {
1041            return Err(AssetError::WrongHandleKind(handle.kind().to_owned()));
1042        }
1043        let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
1044        let Some(data) = inner.image_data.get(&handle.id()) else {
1045            return Err(AssetError::UnknownHandle(handle.id()));
1046        };
1047        if data.mime_type == "image/svg+xml"
1048            && let Some(color) = color
1049            && let Some(source) = data.svg_source()
1050            && svg_uses_external_current_color(&source)
1051        {
1052            drop(inner);
1053            return Ok(self.svg_variant_image_source(SvgRasterSource::new(source, Some(color))));
1054        }
1055        let image = inner
1056            .images
1057            .get(&handle.id())
1058            .cloned()
1059            .ok_or(AssetError::UnknownHandle(handle.id()))?;
1060        Ok(ImageSource::Image(image))
1061    }
1062
1063    pub(crate) fn inline_svg_source(&self, source: Arc<str>, color: Option<Rgba8>) -> ImageSource {
1064        self.svg_variant_image_source(SvgRasterSource::new(source, color))
1065    }
1066
1067    fn svg_variant_image_source(&self, source: SvgRasterSource) -> ImageSource {
1068        let registry = self.clone();
1069        ImageSource::from(move |window: &mut gpui::Window, cx: &mut gpui::App| {
1070            let access = {
1071                let Ok(mut inner) = registry.inner.try_borrow_mut() else {
1072                    return Some(Err(ImageCacheError::Asset(
1073                        AssetError::Borrowed.to_string().into(),
1074                    )));
1075                };
1076                inner.svg_variants.access(&source)
1077            };
1078            match access {
1079                SvgVariantAccess::Ready(result) => Some(result),
1080                SvgVariantAccess::Pending => {
1081                    window.request_animation_frame();
1082                    None
1083                }
1084                SvgVariantAccess::Start {
1085                    token,
1086                    canceled,
1087                    sender,
1088                } => {
1089                    let source = source.clone();
1090                    cx.background_spawn(async move {
1091                        if canceled.load(Ordering::Acquire) {
1092                            return;
1093                        }
1094                        let result = svg_render_image(source.source.as_bytes(), source.color())
1095                            .map_err(|error| ImageCacheError::Asset(error.to_string().into()));
1096                        if !canceled.load(Ordering::Acquire) {
1097                            let _ = sender.send(SvgVariantMessage {
1098                                source,
1099                                token,
1100                                result,
1101                            });
1102                        }
1103                    })
1104                    .detach();
1105                    window.request_animation_frame();
1106                    None
1107                }
1108            }
1109        })
1110    }
1111
1112    /// Return bounded SVG variant-cache usage and lifetime counters.
1113    ///
1114    /// The byte count includes retained SVG source text and ready BGRA pixels.
1115    /// Pending or canceled background work is never counted as a ready image.
1116    ///
1117    /// # Errors
1118    ///
1119    /// Returns [`AssetError::Borrowed`] during conflicting registry access.
1120    pub fn svg_cache_stats(&self) -> Result<SvgCacheStats, AssetError> {
1121        self.inner
1122            .try_borrow_mut()
1123            .map(|mut inner| inner.svg_variants.stats())
1124            .map_err(|_| AssetError::Borrowed)
1125    }
1126}
1127
1128fn install_decoded_image(
1129    inner: &mut AssetRegistryInner,
1130    asset: AssetId,
1131    prepared: PreparedImageData,
1132) -> ImageHandle {
1133    if let Some(handle) = inner.by_asset.get(&asset) {
1134        return handle.clone();
1135    }
1136    inner.next_id = inner.next_id.saturating_add(1);
1137    let image_id = inner.next_id;
1138    let handle = ImageHandle {
1139        opaque: OpaqueHandle::new("image", image_id),
1140        asset: asset.clone(),
1141    };
1142    inner.images.insert(image_id, prepared.image);
1143    inner.image_data.insert(image_id, prepared.data);
1144    inner.by_asset.insert(asset, handle.clone());
1145    handle
1146}
1147
1148fn prepare_image(
1149    format: ImageFormat,
1150    bytes: &[u8],
1151    color: Option<Rgba8>,
1152) -> Result<Arc<Image>, AssetError> {
1153    if format == ImageFormat::Svg {
1154        svg_image(bytes, color)
1155    } else {
1156        Ok(Arc::new(Image::from_bytes(format, bytes.to_vec())))
1157    }
1158}
1159
1160fn decode_asset_data(data: AssetData) -> Result<PreparedImageData, String> {
1161    if data.bytes.is_empty() {
1162        return Err("image data is empty".to_owned());
1163    }
1164    let format = ImageFormat::from_mime_type(&data.mime_type)
1165        .ok_or_else(|| format!("unsupported image MIME type `{}`", data.mime_type))?;
1166    if format == ImageFormat::Svg {
1167        let source = std::str::from_utf8(&data.bytes)
1168            .map_err(|_| "SVG image is not valid UTF-8".to_owned())?;
1169        if !source.contains("<svg") {
1170            return Err("SVG image has no root element".to_owned());
1171        }
1172    } else {
1173        let raster_format = raster_image_format(format)
1174            .ok_or_else(|| "unsupported raster image format".to_owned())?;
1175        image::load_from_memory_with_format(&data.bytes, raster_format)
1176            .map_err(|error| format!("raster image decode failed: {error}"))?;
1177    }
1178    let image = prepare_image(format, &data.bytes, None).map_err(|error| error.to_string())?;
1179    let data = StoredAssetData::from_validated(data, format).map_err(|error| error.to_string())?;
1180    Ok(PreparedImageData { data, image })
1181}
1182
1183fn raster_image_format(format: ImageFormat) -> Option<image::ImageFormat> {
1184    match format {
1185        ImageFormat::Png => Some(image::ImageFormat::Png),
1186        ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
1187        ImageFormat::Webp => Some(image::ImageFormat::WebP),
1188        ImageFormat::Gif => Some(image::ImageFormat::Gif),
1189        ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
1190        ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
1191        ImageFormat::Svg | ImageFormat::Ico | ImageFormat::Pnm => None,
1192    }
1193}
1194
1195pub(crate) fn svg_image(bytes: &[u8], color: Option<Rgba8>) -> Result<Arc<Image>, AssetError> {
1196    // The gpui-pre 0.3.7 family has a correct premultiplied-RGBA to BGRA SVG renderer.
1197    // gpui-rhai intentionally keeps this complete-document adapter because it
1198    // also owns external currentColor substitution, bounded variant caching,
1199    // system-font fallback and off-foreground preparation. Encoding the
1200    // resulting RGBA pixmap as PNG lets GPUI perform exactly one RGBA-to-BGRA
1201    // conversion; do not add the old channel-swap workaround back here.
1202    let pixmap = svg_pixmap(bytes, color)?;
1203    let png = pixmap.encode_png().map_err(|_| AssetError::InvalidSvg)?;
1204    Ok(Arc::new(Image::from_bytes(ImageFormat::Png, png)))
1205}
1206
1207fn svg_render_image(bytes: &[u8], color: Option<Rgba8>) -> Result<Arc<RenderImage>, AssetError> {
1208    let pixmap = svg_pixmap(bytes, color)?;
1209    let png = pixmap.encode_png().map_err(|_| AssetError::InvalidSvg)?;
1210    let mut bgra = image::load_from_memory_with_format(&png, image::ImageFormat::Png)
1211        .map_err(|_| AssetError::InvalidSvg)?
1212        .into_rgba8();
1213    for pixel in bgra.chunks_exact_mut(4) {
1214        pixel.swap(0, 2);
1215    }
1216    Ok(Arc::new(RenderImage::new(vec![image::Frame::new(bgra)])))
1217}
1218
1219fn svg_pixmap(bytes: &[u8], color: Option<Rgba8>) -> Result<resvg::tiny_skia::Pixmap, AssetError> {
1220    let source = std::str::from_utf8(bytes).map_err(|_| AssetError::InvalidSvg)?;
1221    let source = inherited_svg_color(source, color)?;
1222    let tree = usvg::Tree::from_str(&source, &svg_options()).map_err(|_| AssetError::InvalidSvg)?;
1223    let width = svg_raster_dimension(tree.size().width()).ok_or(AssetError::InvalidSvg)?;
1224    let height = svg_raster_dimension(tree.size().height()).ok_or(AssetError::InvalidSvg)?;
1225    if u64::from(width).saturating_mul(u64::from(height)) > MAX_SVG_RASTER_PIXELS {
1226        return Err(AssetError::InvalidSvg);
1227    }
1228    let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).ok_or(AssetError::InvalidSvg)?;
1229    resvg::render(
1230        &tree,
1231        resvg::tiny_skia::Transform::identity(),
1232        &mut pixmap.as_mut(),
1233    );
1234    Ok(pixmap)
1235}
1236
1237pub(crate) fn svg_options() -> usvg::Options<'static> {
1238    static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
1239        let mut database = usvg::fontdb::Database::new();
1240        database.load_system_fonts();
1241        let first_family = database
1242            .faces()
1243            .find_map(|face| face.families.first().map(|family| family.0.clone()));
1244        if let Some(family) = available_font_family(
1245            &database,
1246            &[
1247                "Arial",
1248                "Helvetica",
1249                "DejaVu Sans",
1250                "Liberation Sans",
1251                "Noto Sans",
1252            ],
1253        )
1254        .or_else(|| first_family.clone())
1255        {
1256            database.set_sans_serif_family(family.clone());
1257            database.set_cursive_family(family.clone());
1258            database.set_fantasy_family(family);
1259        }
1260        if let Some(family) = available_font_family(
1261            &database,
1262            &[
1263                "Times New Roman",
1264                "Times",
1265                "DejaVu Serif",
1266                "Liberation Serif",
1267                "Noto Serif",
1268            ],
1269        )
1270        .or_else(|| first_family.clone())
1271        {
1272            database.set_serif_family(family);
1273        }
1274        if let Some(family) = available_font_family(
1275            &database,
1276            &[
1277                "Courier New",
1278                "Menlo",
1279                "DejaVu Sans Mono",
1280                "Liberation Mono",
1281                "Noto Sans Mono",
1282            ],
1283        )
1284        .or(first_family)
1285        {
1286            database.set_monospace_family(family);
1287        }
1288        Arc::new(database)
1289    });
1290    let default_font_resolver = usvg::FontResolver::default_font_selector();
1291    let font_resolver = Box::new(
1292        move |font: &usvg::Font, database: &mut Arc<usvg::fontdb::Database>| {
1293            if database.is_empty() {
1294                *database = Arc::clone(&FONT_DB);
1295            }
1296            default_font_resolver(font, database)
1297        },
1298    );
1299    usvg::Options {
1300        font_resolver: usvg::FontResolver {
1301            select_font: font_resolver,
1302            select_fallback: usvg::FontResolver::default_fallback_selector(),
1303        },
1304        ..Default::default()
1305    }
1306}
1307
1308fn available_font_family(database: &usvg::fontdb::Database, candidates: &[&str]) -> Option<String> {
1309    candidates.iter().find_map(|candidate| {
1310        database.faces().find_map(|face| {
1311            face.families
1312                .iter()
1313                .find(|family| family.0.eq_ignore_ascii_case(candidate))
1314                .map(|family| family.0.clone())
1315        })
1316    })
1317}
1318
1319fn svg_raster_dimension(value: f32) -> Option<u32> {
1320    let value = value.ceil();
1321    if !value.is_finite() || !(1.0..=MAX_SVG_RASTER_DIMENSION_F32).contains(&value) {
1322        return None;
1323    }
1324    let parsed = value.to_string().parse::<u32>().ok()?;
1325    (parsed <= MAX_SVG_RASTER_DIMENSION).then_some(parsed)
1326}
1327
1328fn inherited_svg_color(source: &str, color: Option<Rgba8>) -> Result<String, AssetError> {
1329    let document = roxmltree::Document::parse(source).map_err(|_| AssetError::InvalidSvg)?;
1330    let root = document.root_element();
1331    if root.tag_name().name() != "svg" {
1332        return Err(AssetError::InvalidSvg);
1333    }
1334    let Some(color) = color else {
1335        return Ok(source.to_owned());
1336    };
1337    if let Some(attribute) = root
1338        .attributes()
1339        .find(|attribute| attribute.name() == "color")
1340    {
1341        if is_explicit_svg_color_value(attribute.value()) {
1342            return Ok(source.to_owned());
1343        }
1344        let range = attribute.range();
1345        let mut inherited = String::with_capacity(source.len().saturating_add(8));
1346        inherited.push_str(&source[..range.start]);
1347        let _ = write!(inherited, "color=\"#{:08x}\"", color.as_rgba_hex());
1348        inherited.push_str(&source[range.end..]);
1349        return Ok(inherited);
1350    }
1351    let root_start = root.range().start;
1352    let bytes = source.as_bytes();
1353    let mut name_end = root_start.saturating_add(1);
1354    while bytes
1355        .get(name_end)
1356        .is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'/' | b'>'))
1357    {
1358        name_end = name_end.saturating_add(1);
1359    }
1360    if name_end <= root_start.saturating_add(1) || name_end > source.len() {
1361        return Err(AssetError::InvalidSvg);
1362    }
1363    let mut inherited = String::with_capacity(source.len().saturating_add(20));
1364    inherited.push_str(&source[..name_end]);
1365    let _ = write!(inherited, " color=\"#{:08x}\"", color.as_rgba_hex());
1366    inherited.push_str(&source[name_end..]);
1367    Ok(inherited)
1368}
1369
1370fn svg_uses_external_current_color(source: &str) -> bool {
1371    if !source
1372        .as_bytes()
1373        .windows(b"currentColor".len())
1374        .any(|candidate| candidate.eq_ignore_ascii_case(b"currentColor"))
1375    {
1376        return false;
1377    }
1378    let Ok(document) = roxmltree::Document::parse(source) else {
1379        return true;
1380    };
1381    let mut found_attribute_use = false;
1382    for node in document.descendants().filter(roxmltree::Node::is_element) {
1383        let uses_current_color = node.attributes().any(|attribute| {
1384            attribute
1385                .value()
1386                .as_bytes()
1387                .windows(b"currentColor".len())
1388                .any(|candidate| candidate.eq_ignore_ascii_case(b"currentColor"))
1389        });
1390        if !uses_current_color {
1391            continue;
1392        }
1393        found_attribute_use = true;
1394        let mut ancestor = Some(node);
1395        let mut locally_resolved = false;
1396        while let Some(element) = ancestor {
1397            if element_has_explicit_color(element) {
1398                locally_resolved = true;
1399                break;
1400            }
1401            ancestor = element.parent_element();
1402        }
1403        if !locally_resolved {
1404            return true;
1405        }
1406    }
1407    !found_attribute_use
1408}
1409
1410fn element_has_explicit_color(node: roxmltree::Node<'_, '_>) -> bool {
1411    if node
1412        .attribute("color")
1413        .is_some_and(is_explicit_svg_color_value)
1414    {
1415        return true;
1416    }
1417    node.attribute("style").is_some_and(|style| {
1418        style.split(';').any(|declaration| {
1419            let Some((name, value)) = declaration.split_once(':') else {
1420                return false;
1421            };
1422            name.trim().eq_ignore_ascii_case("color") && is_explicit_svg_color_value(value)
1423        })
1424    })
1425}
1426
1427fn is_explicit_svg_color_value(value: &str) -> bool {
1428    let value = value.trim();
1429    !value.eq_ignore_ascii_case("inherit")
1430        && !value.eq_ignore_ascii_case("currentColor")
1431        && !value.eq_ignore_ascii_case("unset")
1432        && !value.eq_ignore_ascii_case("revert")
1433}
1434
1435fn valid_segment(value: &str) -> bool {
1436    !value.is_empty()
1437        && value
1438            .chars()
1439            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
1440}
1441
1442pub(crate) fn asset_id_from_script(value: &str) -> Result<AssetId, Box<rhai::EvalAltResult>> {
1443    AssetId::parse(value).map_err(|error| error.to_string().into())
1444}
1445
1446#[derive(Debug, Error)]
1447pub enum AssetError {
1448    #[error("asset ID `{0}` must be a namespaced logical path")]
1449    InvalidId(String),
1450    #[error("asset namespace `{0}` is invalid")]
1451    InvalidNamespace(String),
1452    #[error("asset namespace `{0}` is already registered")]
1453    DuplicateNamespace(String),
1454    #[error("asset namespace `{0}` is not registered")]
1455    UnknownNamespace(String),
1456    #[error("asset registry is already borrowed")]
1457    Borrowed,
1458    #[error("asset provider failed for `{asset:?}`: {message}")]
1459    Provider { asset: AssetId, message: String },
1460    #[error("image MIME type `{0}` is unsupported")]
1461    UnsupportedMime(String),
1462    #[error("image asset `{0:?}` is empty")]
1463    Empty(AssetId),
1464    #[error("SVG asset is not valid UTF-8 SVG data")]
1465    InvalidSvg,
1466    #[error("failed to spawn image decode worker: {0}")]
1467    DecodeSpawn(std::io::Error),
1468    #[error("opaque handle kind `{0}` is not an image")]
1469    WrongHandleKind(String),
1470    #[error("image handle `{0}` is unknown")]
1471    UnknownHandle(u64),
1472    #[error("declarative image asset `{0:?}` was not preloaded during preparation")]
1473    NotPreloaded(AssetId),
1474    #[error("asset I/O failed for `{path}`: {source}")]
1475    Io {
1476        path: PathBuf,
1477        #[source]
1478        source: std::io::Error,
1479    },
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484    use super::*;
1485    use std::io::Cursor;
1486    use std::time::Duration;
1487
1488    use rhai::FnPtr;
1489
1490    #[derive(Clone)]
1491    struct MutableProvider(Rc<RefCell<AssetData>>);
1492
1493    impl AssetProvider for MutableProvider {
1494        fn load(&self, _: &str) -> Result<AssetData, String> {
1495            Ok(self.0.borrow().clone())
1496        }
1497    }
1498
1499    fn one_pixel_png() -> Vec<u8> {
1500        let mut output = Cursor::new(Vec::new());
1501        image::DynamicImage::new_rgba8(1, 1)
1502            .write_to(&mut output, image::ImageFormat::Png)
1503            .unwrap();
1504        output.into_inner()
1505    }
1506
1507    #[test]
1508    fn logical_ids_reject_urls_and_traversal() {
1509        for invalid in ["https://example.com/a.png", "/tmp/a.png", "core/../secret"] {
1510            assert!(AssetId::parse(invalid).is_err());
1511        }
1512        assert_eq!(AssetId::parse("core/check").unwrap().as_str(), "core/check");
1513    }
1514
1515    #[test]
1516    fn provider_images_are_cached_behind_opaque_handles() {
1517        let registry = AssetRegistry::new();
1518        registry
1519            .register(
1520                "core",
1521                InMemoryAssetProvider::new(BTreeMap::from([(
1522                    "check".to_owned(),
1523                    AssetData {
1524                        mime_type: "image/svg+xml".to_owned(),
1525                        bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>"#
1526                            .to_vec(),
1527                    },
1528                )])),
1529            )
1530            .unwrap();
1531        let id = AssetId::parse("core/check").unwrap();
1532        let first = registry.load_image(&id).unwrap();
1533        let second = registry.load_image(&id).unwrap();
1534        assert_eq!(first, second);
1535        registry.image_source(first.opaque()).unwrap();
1536        assert!(matches!(
1537            registry.image_source(&OpaqueHandle::new("task", 1)),
1538            Err(AssetError::WrongHandleKind(_))
1539        ));
1540    }
1541
1542    #[test]
1543    fn rejected_duplicate_provider_keeps_uncached_assets_from_the_original() {
1544        let registry = AssetRegistry::new();
1545        let data = AssetData {
1546            mime_type: "image/svg+xml".to_owned(),
1547            bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>"#.to_vec(),
1548        };
1549        registry
1550            .register(
1551                "app",
1552                InMemoryAssetProvider::new(BTreeMap::from([
1553                    ("first".to_owned(), data.clone()),
1554                    ("second".to_owned(), data),
1555                ])),
1556            )
1557            .unwrap();
1558        registry
1559            .load_image(&AssetId::parse("app/first").unwrap())
1560            .unwrap();
1561        assert!(matches!(
1562            registry.register("app", InMemoryAssetProvider::default()),
1563            Err(AssetError::DuplicateNamespace(_))
1564        ));
1565        registry
1566            .load_image(&AssetId::parse("app/second").unwrap())
1567            .unwrap();
1568    }
1569
1570    #[test]
1571    fn namespace_refresh_preserves_handle_and_replaces_cached_bytes() {
1572        let data = Rc::new(RefCell::new(AssetData {
1573            mime_type: "image/svg+xml".to_owned(),
1574            bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="one"/>"#.to_vec(),
1575        }));
1576        let registry = AssetRegistry::new();
1577        registry
1578            .register("app", MutableProvider(Rc::clone(&data)))
1579            .unwrap();
1580        let id = AssetId::parse("app/icon").unwrap();
1581        let handle = registry.load_image(&id).unwrap();
1582        data.borrow_mut().bytes =
1583            br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="two"/>"#
1584                .to_vec();
1585        assert_eq!(registry.refresh_namespace("app").unwrap(), 1);
1586        assert_eq!(registry.load_image(&id).unwrap(), handle);
1587        assert_eq!(
1588            registry.inner.borrow().image_data[&handle.opaque.id()]
1589                .svg_source()
1590                .unwrap()
1591                .as_bytes(),
1592            br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="two"/>"#
1593        );
1594    }
1595
1596    #[test]
1597    fn svg_current_color_uses_external_default_without_overriding_local_color() {
1598        let inherited = br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="currentColor"/></svg>"#;
1599        assert!(svg_uses_external_current_color(
1600            std::str::from_utf8(inherited).unwrap()
1601        ));
1602        let image = svg_image(inherited, Some(Rgba8::from_rgba_hex(0x1234_ab80))).unwrap();
1603        assert_eq!(
1604            image::load_from_memory(&image.bytes)
1605                .unwrap()
1606                .into_rgba8()
1607                .into_raw(),
1608            [0x12, 0x34, 0xab, 0x80]
1609        );
1610
1611        let local = br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" color="#ff0000" fill="currentColor"/></svg>"##;
1612        assert!(!svg_uses_external_current_color(
1613            std::str::from_utf8(local).unwrap()
1614        ));
1615        let image = svg_image(local, Some(Rgba8::from_rgba_hex(0x00ff_00ff))).unwrap();
1616        assert_eq!(
1617            image::load_from_memory(&image.bytes)
1618                .unwrap()
1619                .into_rgba8()
1620                .into_raw(),
1621            [0xff, 0x00, 0x00, 0xff]
1622        );
1623
1624        let root_inherit = br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" color="inherit"><rect width="1" height="1" fill="currentColor"/></svg>"#;
1625        let image = svg_image(root_inherit, Some(Rgba8::from_rgba_hex(0x1234_abff))).unwrap();
1626        assert_eq!(
1627            image::load_from_memory(&image.bytes)
1628                .unwrap()
1629                .into_rgba8()
1630                .into_raw(),
1631            [0x12, 0x34, 0xab, 0xff]
1632        );
1633    }
1634
1635    #[test]
1636    fn svg_raster_adapter_preserves_fixed_color_gradient_and_semantic_alpha() {
1637        let mixed = br##"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="1"><rect width="1" height="1" fill="currentColor"/><rect x="1" width="1" height="1" fill="#ff0000"/></svg>"##;
1638        let image = svg_image(mixed, Some(Rgba8::from_rgba_hex(0x1234_ab80))).unwrap();
1639        let pixels = image::load_from_memory(&image.bytes)
1640            .unwrap()
1641            .into_rgba8()
1642            .into_raw();
1643        assert_eq!(&pixels[0..4], &[0x12, 0x34, 0xab, 0x80]);
1644        assert_eq!(&pixels[4..8], &[0xff, 0x00, 0x00, 0xff]);
1645
1646        let gradient = br##"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="1"><defs><linearGradient id="g"><stop offset="0" stop-color="#ff0000"/><stop offset="1" stop-color="#0000ff"/></linearGradient></defs><rect width="4" height="1" fill="url(#g)"/></svg>"##;
1647        let image = svg_image(gradient, None).unwrap();
1648        let pixels = image::load_from_memory(&image.bytes)
1649            .unwrap()
1650            .into_rgba8()
1651            .into_raw();
1652        assert!(pixels[0] > pixels[2], "gradient must begin red: {pixels:?}");
1653        let last = &pixels[pixels.len() - 4..];
1654        assert!(last[2] > last[0], "gradient must end blue: {pixels:?}");
1655    }
1656
1657    #[test]
1658    fn svg_variant_cache_is_lru_bounded_and_observable() {
1659        let mut cache = SvgVariantCache::with_limits(2, 120);
1660        let mut first_canceled = None;
1661        for index in 0..3 {
1662            let source = SvgRasterSource::new(
1663                Arc::from(format!(
1664                    "<svg width='1' height='1' data-index='{index}'>{}</svg>",
1665                    "x".repeat(20)
1666                )),
1667                None,
1668            );
1669            let SvgVariantAccess::Start { canceled, .. } = cache.access(&source) else {
1670                panic!("new SVG source must start one worker");
1671            };
1672            if index == 0 {
1673                first_canceled = Some(canceled);
1674            }
1675        }
1676        let stats = cache.stats();
1677        assert!(stats.entries <= 2);
1678        assert_eq!(stats.misses, 3);
1679        assert!(stats.evictions >= 1);
1680        assert!(first_canceled.unwrap().load(Ordering::Acquire));
1681        assert!(stats.bytes <= stats.max_bytes);
1682    }
1683
1684    #[test]
1685    fn raster_decode_delivers_handle_on_foreground_drain() {
1686        let registry = AssetRegistry::new();
1687        registry
1688            .register(
1689                "app",
1690                InMemoryAssetProvider::new(BTreeMap::from([(
1691                    "pixel".to_owned(),
1692                    AssetData {
1693                        mime_type: "image/png".to_owned(),
1694                        bytes: one_pixel_png(),
1695                    },
1696                )])),
1697            )
1698            .unwrap();
1699        let generation = ScriptGeneration::initial();
1700        registry
1701            .start_image_decode(
1702                &AssetId::parse("app/pixel").unwrap(),
1703                AsyncScope::Component(crate::ComponentInstancePath::root("App", "root")),
1704                generation,
1705                ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1706                ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1707            )
1708            .unwrap();
1709        let mut deliveries = Vec::new();
1710        for _ in 0..1_000 {
1711            deliveries = registry.drain_image_decodes(generation).unwrap();
1712            if !deliveries.is_empty() {
1713                break;
1714            }
1715            std::thread::sleep(Duration::from_millis(1));
1716        }
1717        assert_eq!(deliveries.len(), 1);
1718        assert_eq!(deliveries[0].callback.name(), "loaded");
1719        let UiValue::Handle(handle) = &deliveries[0].payload else {
1720            panic!("successful decode must deliver an image handle");
1721        };
1722        registry.image_source(handle).unwrap();
1723        assert_eq!(registry.pending_decode_count(), 0);
1724    }
1725
1726    #[test]
1727    fn canceled_and_stale_image_decodes_do_not_deliver() {
1728        let registry = AssetRegistry::new();
1729        registry
1730            .register(
1731                "app",
1732                InMemoryAssetProvider::new(BTreeMap::from([(
1733                    "pixel".to_owned(),
1734                    AssetData {
1735                        mime_type: "image/png".to_owned(),
1736                        bytes: one_pixel_png(),
1737                    },
1738                )])),
1739            )
1740            .unwrap();
1741        let generation = ScriptGeneration::initial();
1742        let handle = registry
1743            .start_image_decode(
1744                &AssetId::parse("app/pixel").unwrap(),
1745                AsyncScope::App,
1746                generation,
1747                ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1748                ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1749            )
1750            .unwrap();
1751        assert!(registry.cancel_image_decode(handle).unwrap());
1752        let effect_scope = AsyncScope::Effect {
1753            component: crate::ComponentInstancePath::root("App", "root"),
1754            key: "image".to_owned(),
1755            activation: 1,
1756        };
1757        registry
1758            .start_image_decode(
1759                &AssetId::parse("app/pixel").unwrap(),
1760                effect_scope.clone(),
1761                generation,
1762                ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1763                ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1764            )
1765            .unwrap();
1766        registry.cancel_scope(&effect_scope).unwrap();
1767        registry
1768            .retain_decode_generation(generation.next())
1769            .unwrap();
1770        std::thread::sleep(Duration::from_millis(10));
1771        assert!(
1772            registry
1773                .drain_image_decodes(generation.next())
1774                .unwrap()
1775                .is_empty()
1776        );
1777        assert_eq!(registry.pending_decode_count(), 0);
1778    }
1779
1780    #[test]
1781    fn transaction_rollback_restores_a_cancelled_decode_registration() {
1782        let registry = AssetRegistry::new();
1783        registry
1784            .register(
1785                "app",
1786                InMemoryAssetProvider::new(BTreeMap::from([(
1787                    "pixel".to_owned(),
1788                    AssetData {
1789                        mime_type: "image/png".to_owned(),
1790                        bytes: one_pixel_png(),
1791                    },
1792                )])),
1793            )
1794            .unwrap();
1795        let generation = ScriptGeneration::initial();
1796        let handle = registry
1797            .start_image_decode(
1798                &AssetId::parse("app/pixel").unwrap(),
1799                AsyncScope::App,
1800                generation,
1801                ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1802                ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1803            )
1804            .unwrap();
1805        let snapshot = registry.begin_transaction().unwrap();
1806        assert!(registry.cancel_image_decode(handle).unwrap());
1807        assert_eq!(registry.pending_decode_count(), 0);
1808        registry.restore_decode_snapshot(snapshot).unwrap();
1809        assert_eq!(registry.pending_decode_count(), 1);
1810    }
1811}