zng_ext_image/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Image loading, rendering and cache.
5//!
6//! # Services
7//!
8//! Services this extension provides.
9//!
10//! * [`IMAGES`]
11//!
12//! # Crate
13//!
14#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
15#![warn(unused_extern_crates)]
16#![warn(missing_docs)]
17
18use std::{
19    any::Any,
20    mem,
21    path::{Path, PathBuf},
22    pin::Pin,
23};
24
25use parking_lot::Mutex;
26use zng_app::{
27    static_id,
28    update::UPDATES,
29    view_process::{
30        VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
31        raw_events::{
32            RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
33            RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
34        },
35    },
36    widget::{
37        WIDGET,
38        node::{IntoUiNode, UiNode, UiNodeOp, match_node},
39    },
40    window::{WINDOW, WindowId},
41};
42use zng_app_context::app_local;
43use zng_clone_move::clmv;
44use zng_layout::unit::{ByteLength, ByteUnits};
45use zng_state_map::StateId;
46use zng_task::channel::IpcBytes;
47use zng_txt::ToTxt;
48use zng_unique_id::{IdEntry, IdMap};
49use zng_var::{IntoVar, Var, VarHandle, var};
50use zng_view_api::{
51    image::{ImageDecoded, ImageRequest},
52    window::RenderMode,
53};
54
55mod types;
56pub use types::*;
57
58app_local! {
59    static IMAGES_SV: ImagesService = ImagesService::new();
60}
61
62struct ImagesService {
63    load_in_headless: Var<bool>,
64    limits: Var<ImageLimits>,
65
66    extensions: Vec<Box<dyn ImagesExtension>>,
67    render_windows: Option<Box<dyn ImageRenderWindowsService>>,
68
69    cache: IdMap<ImageHash, ImageVar>,
70}
71impl ImagesService {
72    pub fn new() -> Self {
73        Self {
74            load_in_headless: var(false),
75            limits: var(ImageLimits::default()),
76
77            extensions: vec![],
78            render_windows: None,
79
80            cache: IdMap::new(),
81        }
82    }
83
84    pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
85        self.render_windows
86            .as_ref()
87            .expect("WINDOWS service not integrated with IMAGES service")
88            .clone_boxed()
89    }
90}
91
92/// Image loading, cache and render service.
93///
94/// If the app is running without a [`VIEW_PROCESS`] all images are dummy, see [`load_in_headless`] for
95/// details.
96///
97/// [`load_in_headless`]: IMAGES::load_in_headless
98/// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
99pub struct IMAGES;
100impl IMAGES {
101    /// If should still download/read image bytes in headless/renderless mode.
102    ///
103    /// When an app is in headless mode without renderer no [`VIEW_PROCESS`] is available, so
104    /// images cannot be decoded, in this case all images are dummy loading and no attempt
105    /// to download/read the image files is made. You can enable loading in headless tests to detect
106    /// IO errors, in this case if there is an error acquiring the image file the image will be a
107    /// dummy with error.
108    ///
109    /// [`VIEW_PROCESS`]: zng_app::view_process::VIEW_PROCESS
110    pub fn load_in_headless(&self) -> Var<bool> {
111        IMAGES_SV.read().load_in_headless.clone()
112    }
113
114    /// Default loading and decoding limits for each image.
115    pub fn limits(&self) -> Var<ImageLimits> {
116        IMAGES_SV.read().limits.clone()
117    }
118
119    /// Request an image, reads from a `path` and caches it.
120    ///
121    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Read`] and [`ImageOptions::cache`].
122    ///
123    /// [`IMAGES.image`]: IMAGES::image
124    pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
125        self.image_impl(path.into().into(), ImageOptions::cache(), None)
126    }
127
128    /// Request an image, downloads from an `uri` and caches it.
129    ///
130    /// Optionally define the HTTP ACCEPT header, if not set all image formats supported by the view-process
131    /// backend are accepted.
132    ///
133    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Download`] and [`ImageOptions::cache`].
134    ///
135    /// [`IMAGES.image`]: IMAGES::image
136    #[cfg(feature = "http")]
137    pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
138    where
139        U: TryInto<zng_task::http::Uri>,
140        <U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
141    {
142        match uri.try_into() {
143            Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
144            Err(e) => {
145                let e = e.to_txt();
146                tracing::debug!("cannot convert into download URI, {e}");
147                zng_var::const_var(ImageEntry::new_error(e))
148            }
149        }
150    }
151
152    /// Request an image from `&'static [u8]` data.
153    ///
154    /// The data can be any of the formats described in [`ImageDataFormat`].
155    ///
156    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Data`] and [`ImageOptions::cache`].
157    ///
158    /// # Examples
159    ///
160    /// Get an image from a PNG file embedded in the app executable using [`include_bytes!`].
161    ///
162    /// ```
163    /// # use zng_ext_image::*;
164    /// # macro_rules! include_bytes { ($tt:tt) => { &[] } }
165    /// # fn demo() {
166    /// let image_var = IMAGES.from_static(include_bytes!("ico.png"), "png");
167    /// # }
168    /// ```
169    ///
170    /// [`IMAGES.image`]: IMAGES::image
171    pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
172        self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
173    }
174
175    /// Get a cached image from shared data.
176    ///
177    /// The data can be any of the formats described in [`ImageDataFormat`].
178    ///
179    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::Data`] and [`ImageOptions::cache`].
180    ///
181    /// [`IMAGES.image`]: IMAGES::image
182    pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
183        self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
184    }
185
186    /// Request an image, with full load and cache configuration.
187    ///
188    /// If `limits` is `None` the [`IMAGES.limits`] is used.
189    ///
190    /// Always returns a *loading* image due to the deferred nature of services. If the image is already in cache
191    /// it will be set and bound to it once the current update finishes.
192    ///
193    /// [`IMAGES.limits`]: IMAGES::limits
194    pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
195        self.image_impl(source.into(), options, limits)
196    }
197    fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
198        tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
199        let r = var(ImageEntry::new_loading());
200        let ri = r.read_only();
201        UPDATES.once_update("IMAGES.image", move || {
202            image(source, options, limits, r);
203        });
204        ri
205    }
206
207    /// Await for an image source, then get or load the image.
208    ///
209    /// If `limits` is `None` the [`IMAGES.limits`] is used.
210    ///
211    /// This method returns immediately with a loading [`ImageVar`], when `source` is ready it
212    /// is used to get the actual [`ImageVar`] and binds it to the returned image.
213    ///
214    /// Note that the [`cache_mode`] always applies to the inner image, and only to the return image if `cache_key` is set.
215    ///
216    /// [`IMAGES.limits`]: IMAGES::limits
217    /// [`cache_mode`]: ImageOptions::cache_mode
218    pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
219    where
220        F: Future<Output = ImageSource> + Send + 'static,
221    {
222        self.image_task_impl(Box::pin(source.into_future()), options, limits)
223    }
224    fn image_task_impl(
225        &self,
226        source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
227        options: ImageOptions,
228        limits: Option<ImageLimits>,
229    ) -> ImageVar {
230        let r = var(ImageEntry::new_loading());
231        let ri = r.read_only();
232        zng_task::spawn(async move {
233            let source = source.await;
234            image(source, options, limits, r);
235        });
236        ri
237    }
238
239    /// Associate the `image` produced by direct interaction with the view-process with the `key` in the cache.
240    ///
241    /// Returns an image var that tracks the image, note that if the `key` is already known does not use the `image` data.
242    ///
243    /// Note that you can register entries in [`ImageEntry::insert_entry`], this method is only for tracking a new entry.
244    ///
245    /// Note that the image will not automatically restore on respawn if the view-process fails while decoding.
246    pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
247        let r = var(ImageEntry::new_loading());
248        let rr = r.read_only();
249        UPDATES.once_update("IMAGES.register", move || {
250            image_view(key, image.0, image.1, None, r);
251        });
252        rr
253    }
254
255    /// Remove the image from the cache, if it is only held by the cache.
256    ///
257    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
258    /// for files or downloads.
259    pub fn clean(&self, key: ImageHash) {
260        UPDATES.once_update("IMAGES.clean", move || {
261            if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
262                && e.get().strong_count() == 1
263            {
264                e.remove();
265            }
266        });
267    }
268
269    /// Remove the image from the cache, even if it is still referenced outside of the cache.
270    ///
271    /// You can use [`ImageSource::hash128_read`] and [`ImageSource::hash128_download`] to get the `key`
272    /// for files or downloads.
273    pub fn purge(&self, key: ImageHash) {
274        UPDATES.once_update("IMAGES.purge", move || {
275            IMAGES_SV.write().cache.remove(&key);
276        });
277    }
278
279    /// Gets the cache key of an image.
280    pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
281        let key = image.cache_key?;
282        if IMAGES_SV.read().cache.contains_key(&key) {
283            Some(key)
284        } else {
285            None
286        }
287    }
288
289    /// If the image is cached.
290    pub fn is_cached(&self, image: &ImageEntry) -> bool {
291        match &image.cache_key {
292            Some(k) => IMAGES_SV.read().cache.contains_key(k),
293            None => false,
294        }
295    }
296
297    /// Clear cached images that are not referenced outside of the cache.
298    pub fn clean_all(&self) {
299        UPDATES.once_update("IMAGES.clean_all", || {
300            IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
301        });
302    }
303
304    /// Clear all cached images, including images that are still referenced outside of the cache.
305    ///
306    /// Image memory only drops when all strong references are removed, so if an image is referenced
307    /// outside of the cache it will merely be disconnected from the cache by this method.
308    pub fn purge_all(&self) {
309        UPDATES.once_update("IMAGES.purge_all", || {
310            IMAGES_SV.write().cache.clear();
311        });
312    }
313
314    /// Add an images service extension.
315    ///
316    /// See [`ImagesExtension`] for extension capabilities.
317    pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
318        UPDATES.once_update("IMAGES.extend", move || {
319            IMAGES_SV.write().extensions.push(extension);
320        });
321    }
322
323    /// Image formats implemented by the current view-process and extensions.
324    pub fn available_formats(&self) -> Vec<ImageFormat> {
325        let mut formats = VIEW_PROCESS.info().image.clone();
326
327        let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
328        for ext in exts.iter_mut() {
329            ext.available_formats(&mut formats);
330        }
331        let mut s = IMAGES_SV.write();
332        exts.append(&mut s.extensions);
333        s.extensions = exts;
334
335        formats
336    }
337
338    #[cfg(feature = "http")]
339    fn http_accept(&self) -> zng_txt::Txt {
340        let mut s = String::new();
341        let mut sep = "";
342        for f in self.available_formats() {
343            for f in f.media_type_suffixes_iter() {
344                s.push_str(sep);
345                s.push_str("image/");
346                s.push_str(f);
347                sep = ",";
348            }
349        }
350        s.into()
351    }
352}
353
354fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
355    let mut s = IMAGES_SV.write();
356
357    let limits = limits.unwrap_or_else(|| s.limits.get());
358
359    // apply extensions
360    let mut exts = mem::take(&mut s.extensions);
361    drop(s); // drop because extensions may use the service
362    if !exts.is_empty() {
363        tracing::trace!("process image with {} extensions", exts.len());
364    }
365    for ext in &mut exts {
366        ext.image(&limits, &mut source, &mut options);
367    }
368    let mut s = IMAGES_SV.write();
369    exts.append(&mut s.extensions);
370    s.extensions = exts;
371
372    if let ImageSource::Image(var) = source {
373        // Image is passthrough, cache config is ignored
374        var.set_bind(&r).perm();
375        r.hold(var).perm();
376        return;
377    } else if let ImageSource::Entries { primary, entries } = source {
378        let entries: Vec<_> = entries
379            .into_iter()
380            .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
381            .collect();
382        let r_weak = r.downgrade();
383        let binding = move |mut primary: ImageEntry| -> bool {
384            let primary_id = primary.handle.image_id();
385            for (i, (kind, entry)) in entries.iter().enumerate() {
386                let kind = kind.clone();
387                primary.insert_entry(entry.map(move |e| {
388                    let mut e = e.clone();
389                    e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
390                    e
391                }));
392            }
393            if let Some(r) = r_weak.upgrade() {
394                r.set(primary);
395                true
396            } else {
397                false
398            }
399        };
400        let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
401        binding(primary.get());
402        primary.hook(move |a| binding(a.value().clone())).perm();
403        r.hold(primary).perm();
404        return;
405    }
406
407    if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
408        tracing::debug!("ignoring image request due headless mode");
409        return;
410    }
411
412    let key = source.hash128(&options).unwrap();
413
414    // setup cache and drop service lock
415    match options.cache_mode {
416        ImageCacheMode::Ignore => (),
417        ImageCacheMode::Cache => {
418            match s.cache.entry(key) {
419                IdEntry::Occupied(e) => {
420                    // already cached
421                    let var = e.get();
422                    var.set_bind(&r).perm();
423                    r.hold(var.clone()).perm();
424                    return;
425                }
426                IdEntry::Vacant(e) => {
427                    // cache
428                    e.insert(r.clone());
429                }
430            }
431        }
432        ImageCacheMode::Retry => {
433            match s.cache.entry(key) {
434                IdEntry::Occupied(mut e) => {
435                    let var = e.get();
436                    if var.with(ImageEntry::is_error) {
437                        // already cached with error
438
439                        // bind old entry to new, in case there are listeners to it,
440                        // can't use `strong_count` to optimize here because it might have weak refs out there
441                        r.set_bind(var).perm();
442                        var.hold(r.clone()).perm();
443
444                        // new var `r` becomes the entry
445                        e.insert(r.clone());
446                    } else {
447                        // already cached ok
448                        var.set_bind(&r).perm();
449                        r.hold(var.clone()).perm();
450                        return;
451                    }
452                }
453                IdEntry::Vacant(e) => {
454                    // cache
455                    e.insert(r.clone());
456                }
457            }
458        }
459        ImageCacheMode::Reload => {
460            match s.cache.entry(key) {
461                IdEntry::Occupied(mut e) => {
462                    let var = e.get();
463                    r.set_bind(var).perm();
464                    var.hold(r.clone()).perm();
465
466                    e.insert(r.clone());
467                }
468                IdEntry::Vacant(e) => {
469                    // cache
470                    e.insert(r.clone());
471                }
472            }
473        }
474    }
475    drop(s);
476
477    match source {
478        ImageSource::Read(path) => {
479            fn read(path: &Path, limit: ByteLength) -> std::io::Result<IpcBytes> {
480                let file = std::fs::File::open(path)?;
481                if file.metadata()?.len() > limit.bytes() as u64 {
482                    return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
483                }
484                IpcBytes::from_file_blocking(file)
485            }
486            let limit = limits.max_encoded_len;
487            let data_format = match path.extension() {
488                Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
489                None => ImageDataFormat::Unknown,
490            };
491            zng_task::spawn_wait(move || match read(&path, limit) {
492                Ok(data) => {
493                    tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", data.len().bytes());
494                    image_data(false, Some(key), data_format, data, options, limits, r)
495                }
496                Err(e) => {
497                    tracing::debug!("cannot read {path:?}, {e}");
498                    r.set(ImageEntry::new_error(e.to_txt()));
499                }
500            });
501        }
502        #[cfg(feature = "http")]
503        ImageSource::Download(uri, accept) => {
504            let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
505
506            use zng_task::http::*;
507            async fn download(uri: Uri, accept: zng_txt::Txt, limit: ByteLength) -> Result<(ImageDataFormat, IpcBytes), Error> {
508                let request = Request::get(uri)?.max_length(limit).header(header::ACCEPT, accept.as_str())?;
509                let mut response = send(request).await?;
510                let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
511                    Some(m) => ImageDataFormat::MimeType(m.to_txt()),
512                    None => ImageDataFormat::Unknown,
513                };
514                let data = response.body().await?;
515
516                Ok((data_format, data))
517            }
518
519            let limit = limits.max_encoded_len;
520            zng_task::spawn(async move {
521                match download(uri.clone(), accept, limit).await {
522                    Ok((fmt, data)) => {
523                        tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", data.len().bytes());
524                        image_data(false, Some(key), fmt, data, options, limits, r);
525                    }
526                    Err(e) => {
527                        tracing::debug!("cannot download {uri:?}, {e}");
528                        r.set(ImageEntry::new_error(e.to_txt()));
529                    }
530                }
531            });
532        }
533        ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data, options, limits, r),
534        ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
535        _ => unreachable!(),
536    }
537}
538
539// source data acquired, setup view-process handle
540fn image_data(
541    is_respawn: bool,
542    cache_key: Option<ImageHash>,
543    format: ImageDataFormat,
544    data: IpcBytes,
545    options: ImageOptions,
546    limits: ImageLimits,
547    r: Var<ImageEntry>,
548) {
549    if !is_respawn && let Some(key) = cache_key {
550        let mut replaced = false;
551        let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
552        if !exts.is_empty() {
553            tracing::trace!("process image_data with {} extensions", exts.len());
554        }
555        for ext in &mut exts {
556            if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
557                replacement.set_bind(&r).perm();
558                r.hold(replacement).perm();
559
560                replaced = true;
561                break;
562            }
563        }
564        {
565            let mut s = IMAGES_SV.write();
566            exts.append(&mut s.extensions);
567            s.extensions = exts;
568
569            if replaced {
570                tracing::trace!("extension replaced image_data");
571                return;
572            }
573        }
574    }
575
576    if !VIEW_PROCESS.is_available() {
577        tracing::debug!("ignoring image view request after test load due to headless mode");
578        return;
579    }
580
581    let mut request = ImageRequest::new(
582        format.clone(),
583        data.clone(),
584        limits.max_decoded_len.bytes() as u64,
585        options.downscale.clone(),
586        options.mask,
587    );
588    request.entries = options.entries;
589
590    if is_respawn {
591        request.parent = r.with(|r| r.data.meta.parent.clone());
592    }
593
594    if VIEW_PROCESS.is_connected()
595        && let Ok(view_img) = VIEW_PROCESS.add_image(request)
596    {
597        // explicitly checking for connected to avoid logging an error
598        image_view(
599            cache_key,
600            view_img,
601            ImageDecoded::default(),
602            Some((format, data, options, limits)),
603            r,
604        );
605    } else {
606        tracing::debug!("image view request failed, will retry on respawn");
607        let mut once = Some((format, data, options, limits, r));
608        VIEW_PROCESS_INITED_EVENT
609            .hook(move |_| {
610                let (format, data, options, limits, r) = once.take().unwrap();
611                image_data(true, cache_key, format, data, options, limits, r);
612                false
613            })
614            .perm();
615    }
616}
617// monitor view-process handle until it is loaded
618fn image_view(
619    cache_key: Option<ImageHash>,
620    handle: ViewImageHandle,
621    decoded: ImageDecoded,
622    respawn_data: Option<(ImageDataFormat, IpcBytes, ImageOptions, ImageLimits)>,
623    r: Var<ImageEntry>,
624) {
625    // reuse value to keep entry vars alive in case of respawn
626    let mut img = r.get();
627    img.cache_key = cache_key;
628    img.handle = handle;
629    img.data = decoded;
630
631    let is_loaded = img.is_loaded();
632    let is_dummy = img.view_handle().is_dummy();
633    r.set(img);
634
635    if is_loaded {
636        image_decoded(r);
637        return;
638    }
639
640    if is_dummy {
641        tracing::error!("tried to register dummy handle");
642        return;
643    }
644
645    // handle respawn during image decode
646    let decoding_respawn_handle = if respawn_data.is_some() {
647        let r_weak = r.downgrade();
648        let mut respawn_data = respawn_data;
649        VIEW_PROCESS_INITED_EVENT.hook(move |_| {
650            if let Some(r) = r_weak.upgrade() {
651                let (format, data, options, limits) = respawn_data.take().unwrap();
652                image_data(true, cache_key, format, data, options, limits, r);
653            }
654            false
655        })
656    } else {
657        // image registered (without source info), respawn is the responsibility of the caller
658        VarHandle::dummy()
659    };
660
661    // handle decode error
662    let r_weak = r.downgrade();
663    let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
664        Some(r) => {
665            if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
666                tracing::debug!("image view error, {}", args.error);
667                r.set(ImageEntry::new_error(args.error.clone()));
668                false
669            } else {
670                r.with(ImageEntry::is_loading)
671            }
672        }
673        None => false,
674    });
675
676    // handle metadata decoded
677    let r_weak = r.downgrade();
678    let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
679        Some(r) => {
680            if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
681                let meta = args.meta.clone();
682                tracing::trace!("image view metadata decoded for request");
683                r.modify(move |i| i.data.meta = meta);
684            } else if let Some(p) = &args.meta.parent
685                && p.parent == r.with(|img| img.view_handle().image_id())
686            {
687                // discovered an entry for this image, start tracking it
688                tracing::trace!("image view metadata decoded for entry of request");
689                let mut entry_d = ImageDecoded::default();
690                entry_d.meta = args.meta.clone();
691                let entry = var(ImageEntry::new(None, args.handle.upgrade().unwrap(), entry_d.clone()));
692                r.modify(clmv!(entry, |i| i.insert_entry(entry)));
693                image_view(None, args.handle.upgrade().unwrap(), entry_d, None, entry);
694            }
695            r.with(ImageEntry::is_loading)
696        }
697        None => false,
698    });
699
700    // handle pixels decoded
701    let r_weak = r.downgrade();
702    RAW_IMAGE_DECODED_EVENT
703        .hook(move |args| {
704            let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
705            match r_weak.upgrade() {
706                Some(r) => {
707                    if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
708                        let data = args.image.upgrade().unwrap();
709                        let is_loading = data.partial.is_some();
710                        tracing::trace!("image view decoded, partial={:?}", is_loading);
711                        r.modify(move |i| i.data = (*data.0).clone());
712                        if !is_loading {
713                            image_decoded(r);
714                        }
715                        is_loading
716                    } else {
717                        r.with(ImageEntry::is_loading)
718                    }
719                }
720                None => false,
721            }
722        })
723        .perm();
724}
725// image decoded ok, setup respawn handle
726fn image_decoded(r: Var<ImageEntry>) {
727    let r_weak = r.downgrade();
728    VIEW_PROCESS_INITED_EVENT
729        .hook(move |_| {
730            if let Some(r) = r_weak.upgrade() {
731                let img = r.get();
732                if !img.is_loaded() {
733                    // image rebound, maybe due to cache refresh
734                    return false;
735                }
736
737                // respawn the image as decoded data
738                let size = img.size();
739                let mut options = ImageOptions::none();
740                let format = match img.is_mask() {
741                    true => {
742                        options.mask = Some(ImageMaskMode::A);
743                        ImageDataFormat::A8 { size }
744                    }
745                    false => ImageDataFormat::Bgra8 {
746                        size,
747                        density: img.density(),
748                        original_color_type: img.original_color_type(),
749                    },
750                };
751                image_data(
752                    true,
753                    img.cache_key,
754                    format,
755                    img.data.pixels.clone(),
756                    options,
757                    ImageLimits::none(),
758                    r,
759                );
760            }
761            false
762        })
763        .perm();
764}
765
766// image render request, respawn errors during rendering are handled by the WINDOWS service
767fn image_render(
768    cache_key: Option<ImageHash>,
769    render_fn: crate::RenderFn,
770    args: Option<ImageRenderArgs>,
771    options: ImageOptions,
772    r: Var<ImageEntry>,
773) {
774    let s = IMAGES_SV.read();
775    let windows = s.render_windows();
776    let windows_ctx = windows.clone_boxed();
777    let mask = options.mask;
778    windows.open_headless_window(Box::new(move || {
779        let ctx = ImageRenderCtx::new();
780        let retain = ctx.retain.clone();
781        WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
782        let w = render_fn(&args.unwrap_or_default());
783        windows_ctx.enable_frame_capture_in_window_context(mask);
784        image_render_open(cache_key, WINDOW.id(), retain, r);
785        w
786    }));
787}
788
789fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
790    // handle window open error
791    let r_weak = r.downgrade();
792    let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
793        if args.window_id == win_id {
794            if let Some(r) = r_weak.upgrade() {
795                r.set(ImageEntry::new_error(args.error.clone()));
796            }
797            false
798        } else {
799            true
800        }
801    });
802    // hold error handle until open ok
803    RAW_HEADLESS_OPEN_EVENT
804        .hook(move |args| {
805            let _hold = &error_handle;
806            args.window_id != win_id
807        })
808        .perm();
809
810    // handle frame(s)
811    let r_weak = r.downgrade();
812    RAW_FRAME_RENDERED_EVENT
813        .hook(move |args| {
814            if args.window_id == win_id {
815                if let Some(r) = r_weak.upgrade() {
816                    match args.frame_image.clone() {
817                        Some(h) => {
818                            let h = h.upgrade().unwrap();
819                            let handle = h.0.0.clone();
820                            let data = h.1.clone();
821                            let retain = retain.get();
822                            r.set(ImageEntry::new(cache_key, handle, data));
823                            if !retain {
824                                IMAGES_SV.read().render_windows().close_window(win_id);
825                                // image_decoded setup a normal respawn recovery for the image
826                                image_decoded(r);
827                            }
828                            // else if it is retained on respawn the window will render again
829                            retain
830                        }
831                        None => {
832                            r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
833                            false
834                        }
835                    }
836                } else {
837                    false
838                }
839            } else {
840                true
841            }
842        })
843        .perm();
844}
845
846impl IMAGES {
847    /// Render the *window* generated by `render` to an image.
848    ///
849    /// The *window* is created as a headless surface and rendered to the returned image. You can set the
850    /// [`IMAGE_RENDER.retain`] var inside `render` to create an image that updates with new frames. By default it will only render once.
851    ///
852    /// The closure runs in the [`WINDOW`] context of the headless window.
853    ///
854    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::render`] and [`ImageOptions::none`].
855    ///
856    /// [`IMAGE_RENDER.retain`]: IMAGE_RENDER::retain
857    /// [`WINDOW`]: zng_app::window::WINDOW
858    /// [`IMAGES.image`]: IMAGES::image
859    pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
860    where
861        N: FnOnce() -> R + Send + Sync + 'static,
862        R: ImageRenderWindowRoot,
863    {
864        let render = Mutex::new(Some(render));
865        let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
866        let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
867        self.image_impl(source, options, None)
868    }
869
870    /// Render an [`UiNode`] to an image.
871    ///
872    /// This method is a shortcut to [`render`] a node without needing to declare the headless window, note that
873    /// a headless window is still used, the node does not have the same context as the calling widget.
874    ///
875    /// This is shorthand for calling [`IMAGES.image`] with [`ImageSource::render_node`] and [`ImageOptions::none`].
876    ///
877    /// [`render`]: Self::render
878    /// [`UiNode`]: zng_app::widget::node::UiNode
879    /// [`IMAGES.image`]: IMAGES::image
880    pub fn render_node(
881        &self,
882        render_mode: RenderMode,
883        mask: Option<ImageMaskMode>,
884        render: impl FnOnce() -> UiNode + Send + Sync + 'static,
885    ) -> ImageVar {
886        let render = Mutex::new(Some(render));
887        let source = ImageSource::render_node(render_mode, move |_| {
888            render.lock().take().expect("IMAGES.render closure called more than once")()
889        });
890        let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
891        self.image_impl(source, options, None)
892    }
893}
894
895/// Images render window hook.
896#[expect(non_camel_case_types)]
897pub struct IMAGES_WINDOW;
898impl IMAGES_WINDOW {
899    /// Sets the windows service used to manage the headless windows used to render images.
900    ///
901    /// This must be called by the windows implementation only.
902    pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
903        let mut img = IMAGES_SV.write();
904        img.render_windows = Some(service);
905    }
906}
907
908/// Reference to a windows manager service that [`IMAGES`] can use to render images.
909///
910/// This service must be implemented by the window implementer, the `WINDOWS` service implements it.
911pub trait ImageRenderWindowsService: Send + Sync + 'static {
912    /// Clone the service reference.
913    fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
914
915    /// Create a window root that presents the node.
916    ///
917    /// This is to produce a window wrapper for [`ImageSource::render_node`].
918    fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
919
920    /// Set parent window for the headless render window.
921    ///
922    /// Called inside the [`WINDOW`] context for the new window.
923    fn set_parent_in_window_context(&self, parent_id: WindowId);
924
925    /// Enable frame capture for the window.
926    ///
927    /// If `mask` is set captures only the given channel, if not set will capture the full BGRA image.
928    ///
929    /// Called inside the [`WINDOW`] context for the new window.
930    fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
931
932    /// Open the window.
933    ///
934    /// The `new_window_root` must be called inside the [`WINDOW`] context for the new window.
935    fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
936
937    /// Close the window, does nothing if the window is not found.
938    fn close_window(&self, window_id: WindowId);
939}
940
941/// Implemented for the root window type.
942///
943/// This is implemented for the `WindowRoot` type.
944pub trait ImageRenderWindowRoot: Send + Any + 'static {}
945
946/// Controls properties of the render window used by [`IMAGES.render`].
947///
948/// [`IMAGES.render`]: IMAGES::render
949#[expect(non_camel_case_types)]
950pub struct IMAGE_RENDER;
951impl IMAGE_RENDER {
952    /// If the current context is an [`IMAGES.render`] closure, window or widget.
953    ///
954    /// [`IMAGES.render`]: IMAGES::render
955    pub fn is_in_render(&self) -> bool {
956        WINDOW.contains_state(*IMAGE_RENDER_ID)
957    }
958
959    /// If the render task is kept alive after a frame is produced, this is `false` by default
960    /// meaning the image only renders once, if set to `true` the image will automatically update
961    /// when the render widget requests a new frame.
962    pub fn retain(&self) -> Var<bool> {
963        WINDOW.req_state(*IMAGE_RENDER_ID).retain
964    }
965}
966
967/// If the render task is kept alive after a frame is produced, this is `false` by default
968/// meaning the image only renders once, if set to `true` the image will automatically update
969/// when the render widget requests a new frame.
970///
971/// This property sets and binds `retain` to [`IMAGE_RENDER.retain`].
972///
973/// [`IMAGE_RENDER.retain`]: IMAGE_RENDER::retain
974#[zng_app::widget::property(CONTEXT, default(false))]
975pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
976    let retain = retain.into_var();
977    match_node(child, move |_, op| {
978        if let UiNodeOp::Init = op {
979            if IMAGE_RENDER.is_in_render() {
980                let actual_retain = IMAGE_RENDER.retain();
981                actual_retain.set_from(&retain);
982                let handle = actual_retain.bind(&retain);
983                WIDGET.push_var_handle(handle);
984            } else {
985                tracing::error!("can only set `render_retain` in render widgets")
986            }
987        }
988    })
989}
990
991#[derive(Clone)]
992struct ImageRenderCtx {
993    retain: Var<bool>,
994}
995impl ImageRenderCtx {
996    fn new() -> Self {
997        Self { retain: var(false) }
998    }
999}
1000
1001static_id! {
1002    static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1003}