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#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
15#![warn(unused_extern_crates)]
16#![warn(missing_docs)]
17
18use std::{any::Any, path::PathBuf, pin::Pin};
19
20use parking_lot::Mutex;
21use zng_app::{
22 static_id,
23 update::UPDATES,
24 view_process::{
25 VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
26 raw_events::{
27 RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
28 RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
29 },
30 },
31 widget::{
32 WIDGET,
33 node::{IntoUiNode, UiNode, UiNodeOp, match_node},
34 },
35 window::{WINDOW, WindowId},
36};
37use zng_app_context::app_local;
38use zng_clone_move::clmv;
39use zng_layout::unit::{ByteLength, ByteUnits};
40use zng_state_map::StateId;
41use zng_task::channel::IpcBytes;
42use zng_txt::ToTxt;
43use zng_unique_id::{IdEntry, IdMap};
44use zng_var::{IntoVar, Var, VarHandle, var};
45use zng_view_api::{
46 image::{ImageDecoded, ImageRequest},
47 window::RenderMode,
48};
49
50mod types;
51pub use types::*;
52
53app_local! {
54 static IMAGES_SV: ImagesService = ImagesService::new();
55 static IMAGES_EXTENSIONS: Vec<Box<dyn ImagesExtension>> = vec![];
56}
57
58struct ImagesService {
59 load_in_headless: Var<bool>,
60 limits: Var<ImageLimits>,
61
62 render_windows: Option<Box<dyn ImageRenderWindowsService>>,
63
64 cache: IdMap<ImageHash, ImageVar>,
65}
66impl ImagesService {
67 pub fn new() -> Self {
68 Self {
69 load_in_headless: var(false),
70 limits: var(ImageLimits::default()),
71
72 render_windows: None,
73
74 cache: IdMap::new(),
75 }
76 }
77
78 pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
79 self.render_windows
80 .as_ref()
81 .expect("WINDOWS service not integrated with IMAGES service")
82 .clone_boxed()
83 }
84}
85
86pub struct IMAGES;
94impl IMAGES {
95 pub fn load_in_headless(&self) -> Var<bool> {
105 IMAGES_SV.read().load_in_headless.clone()
106 }
107
108 pub fn limits(&self) -> Var<ImageLimits> {
110 IMAGES_SV.read().limits.clone()
111 }
112
113 pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
119 self.image_impl(path.into().into(), ImageOptions::cache(), None)
120 }
121
122 #[cfg(feature = "http")]
131 pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
132 where
133 U: TryInto<zng_task::http::Uri>,
134 <U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
135 {
136 match uri.try_into() {
137 Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
138 Err(e) => {
139 let e = e.to_txt();
140 tracing::debug!("cannot convert into download URI, {e}");
141 zng_var::const_var(ImageEntry::new_error(e))
142 }
143 }
144 }
145
146 pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
166 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
167 }
168
169 pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
177 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
178 }
179
180 pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
189 self.image_impl(source.into(), options, limits)
190 }
191 fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
192 tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
193 let r = var(ImageEntry::new_loading());
194 let ri = r.read_only();
195 UPDATES.once_update("IMAGES.image", move || {
196 image(source, options, limits, r);
197 });
198 ri
199 }
200
201 pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
213 where
214 F: Future<Output = ImageSource> + Send + 'static,
215 {
216 self.image_task_impl(Box::pin(source.into_future()), options, limits)
217 }
218 fn image_task_impl(
219 &self,
220 source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
221 options: ImageOptions,
222 limits: Option<ImageLimits>,
223 ) -> ImageVar {
224 let r = var(ImageEntry::new_loading());
225 let ri = r.read_only();
226 zng_task::spawn(async move {
227 let source = source.await;
228 image(source, options, limits, r);
229 });
230 ri
231 }
232
233 pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
241 let r = var(ImageEntry::new_loading());
242 let rr = r.read_only();
243 UPDATES.once_update("IMAGES.register", move || {
244 image_view(key, image.0, image.1, None, r);
245 });
246 rr
247 }
248
249 pub fn clean(&self, key: ImageHash) {
254 UPDATES.once_update("IMAGES.clean", move || {
255 if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
256 && e.get().strong_count() == 1
257 {
258 e.remove();
259 }
260 });
261 }
262
263 pub fn purge(&self, key: ImageHash) {
268 UPDATES.once_update("IMAGES.purge", move || {
269 IMAGES_SV.write().cache.remove(&key);
270 });
271 }
272
273 pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
275 let key = image.cache_key?;
276 if IMAGES_SV.read().cache.contains_key(&key) {
277 Some(key)
278 } else {
279 None
280 }
281 }
282
283 pub fn is_cached(&self, image: &ImageEntry) -> bool {
285 match &image.cache_key {
286 Some(k) => IMAGES_SV.read().cache.contains_key(k),
287 None => false,
288 }
289 }
290
291 pub fn clean_all(&self) {
293 UPDATES.once_update("IMAGES.clean_all", || {
294 IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
295 });
296 }
297
298 pub fn purge_all(&self) {
303 UPDATES.once_update("IMAGES.purge_all", || {
304 IMAGES_SV.write().cache.clear();
305 });
306 }
307
308 pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
312 UPDATES.once_update("IMAGES.extend", move || {
313 IMAGES_EXTENSIONS.write().push(extension);
314 });
315 }
316
317 pub fn available_formats(&self) -> Vec<ImageFormat> {
319 let mut formats = VIEW_PROCESS.info().image.clone();
320
321 for ext in IMAGES_EXTENSIONS.read().iter() {
322 ext.available_formats(&mut formats);
323 }
324
325 formats
326 }
327
328 #[cfg(feature = "http")]
329 fn http_accept(&self) -> zng_txt::Txt {
330 let mut s = String::new();
331 let mut sep = "";
332 for f in self.available_formats() {
333 for f in f.media_type_suffixes_iter() {
334 s.push_str(sep);
335 s.push_str("image/");
336 s.push_str(f);
337 sep = ",";
338 }
339 }
340 s.into()
341 }
342}
343
344fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
345 let limits = limits.unwrap_or_else(|| IMAGES_SV.read().limits.get());
346
347 {
349 let mut exts = IMAGES_EXTENSIONS.write();
350 if !exts.is_empty() {
351 tracing::trace!("process image with {} extensions", exts.len());
352 }
353 for ext in exts.iter_mut() {
354 ext.image(&limits, &mut source, &mut options);
355 }
356 }
357
358 let mut s = IMAGES_SV.write();
360
361 if let ImageSource::Image(var) = source {
362 var.set_bind(&r).perm();
364 r.hold(var).perm();
365 return;
366 } else if let ImageSource::Entries { primary, entries } = source {
367 let entries: Vec<_> = entries
368 .into_iter()
369 .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
370 .collect();
371 let r_weak = r.downgrade();
372 let binding = move |mut primary: ImageEntry| -> bool {
373 let primary_id = primary.handle.image_id();
374 for (i, (kind, entry)) in entries.iter().enumerate() {
375 let kind = kind.clone();
376 primary.insert_entry(entry.map(move |e| {
377 let mut e = e.clone();
378 e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
379 e
380 }));
381 }
382 if let Some(r) = r_weak.upgrade() {
383 r.set(primary);
384 true
385 } else {
386 false
387 }
388 };
389 let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
390 binding(primary.get());
391 primary.hook(move |a| binding(a.value().clone())).perm();
392 r.hold(primary).perm();
393 return;
394 }
395
396 if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
397 tracing::debug!("ignoring image request due headless mode");
398 return;
399 }
400
401 let key = source.hash128(&options).unwrap();
402
403 match options.cache_mode {
405 ImageCacheMode::Ignore => (),
406 ImageCacheMode::Cache => {
407 match s.cache.entry(key) {
408 IdEntry::Occupied(e) => {
409 let var = e.get();
411 var.set_bind(&r).perm();
412 r.hold(var.clone()).perm();
413 return;
414 }
415 IdEntry::Vacant(e) => {
416 e.insert(r.clone());
418 }
419 }
420 }
421 ImageCacheMode::Retry => {
422 match s.cache.entry(key) {
423 IdEntry::Occupied(mut e) => {
424 let var = e.get();
425 if var.with(ImageEntry::is_error) {
426 r.set_bind(var).perm();
431 var.hold(r.clone()).perm();
432
433 e.insert(r.clone());
435 } else {
436 var.set_bind(&r).perm();
438 r.hold(var.clone()).perm();
439 return;
440 }
441 }
442 IdEntry::Vacant(e) => {
443 e.insert(r.clone());
445 }
446 }
447 }
448 ImageCacheMode::Reload => {
449 match s.cache.entry(key) {
450 IdEntry::Occupied(mut e) => {
451 let var = e.get();
452 r.set_bind(var).perm();
453 var.hold(r.clone()).perm();
454
455 e.insert(r.clone());
456 }
457 IdEntry::Vacant(e) => {
458 e.insert(r.clone());
460 }
461 }
462 }
463 }
464 drop(s);
465
466 match source {
467 ImageSource::Read(path) => {
468 fn read(path: &PathBuf, limit: (&ImageSourceFilter<PathBuf>, ByteLength)) -> std::io::Result<IpcBytes> {
469 if !limit.0.allows(path) {
470 return Err(std::io::Error::new(
471 std::io::ErrorKind::PermissionDenied,
472 "file path no allowed by limit",
473 ));
474 }
475 let file = std::fs::File::open(path)?;
476 if file.metadata()?.len() > limit.1.bytes() as u64 {
477 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
478 }
479 IpcBytes::from_file_blocking(file)
480 }
481 let data_format = match path.extension() {
482 Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
483 None => ImageDataFormat::Unknown,
484 };
485 zng_task::spawn_wait(move || match read(&path, (&limits.allow_path, limits.max_encoded_len)) {
486 Ok(data) => {
487 tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", data.len().bytes());
488 image_data(false, Some(key), data_format, data, options, limits, r)
489 }
490 Err(e) => {
491 tracing::debug!("cannot read {path:?}, {e}");
492 r.set(ImageEntry::new_error(e.to_txt()));
493 }
494 });
495 }
496 #[cfg(feature = "http")]
497 ImageSource::Download(uri, accept) => {
498 let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
499
500 use zng_task::http::*;
501 async fn download(
502 uri: Uri,
503 accept: zng_txt::Txt,
504 limit: (ImageSourceFilter<Uri>, ByteLength),
505 ) -> Result<(ImageDataFormat, IpcBytes), Error> {
506 if !limit.0.allows(&uri) {
507 return Err(Box::new(std::io::Error::new(
508 std::io::ErrorKind::PermissionDenied,
509 "uri no allowed by limit",
510 )));
511 }
512
513 let request = Request::get(uri)?.max_length(limit.1).header(header::ACCEPT, accept.as_str())?;
514 let mut response = send(request).await?;
515 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
516 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
517 None => ImageDataFormat::Unknown,
518 };
519 let data = response.body().await?;
520
521 Ok((data_format, data))
522 }
523
524 zng_task::spawn(async move {
525 match download(uri.clone(), accept, (limits.allow_uri.clone(), limits.max_encoded_len)).await {
526 Ok((fmt, data)) => {
527 tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", data.len().bytes());
528 image_data(false, Some(key), fmt, data, options, limits, r);
529 }
530 Err(e) => {
531 tracing::debug!("cannot download {uri:?}, {e}");
532 r.set(ImageEntry::new_error(e.to_txt()));
533 }
534 }
535 });
536 }
537 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data, options, limits, r),
538 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
539 _ => unreachable!(),
540 }
541}
542
543fn image_data(
545 is_respawn: bool,
546 cache_key: Option<ImageHash>,
547 format: ImageDataFormat,
548 data: IpcBytes,
549 options: ImageOptions,
550 limits: ImageLimits,
551 r: Var<ImageEntry>,
552) {
553 if !is_respawn && let Some(key) = cache_key {
554 let mut exts = IMAGES_EXTENSIONS.write();
555 if !exts.is_empty() {
556 tracing::trace!("process image_data with {} extensions", exts.len());
557 }
558 for ext in exts.iter_mut() {
559 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
560 replacement.set_bind(&r).perm();
561 r.hold(replacement).perm();
562
563 tracing::trace!("extension replaced image_data");
564 return;
565 }
566 }
567 }
568
569 if !VIEW_PROCESS.is_available() {
570 tracing::debug!("ignoring image view request after test load due to headless mode");
571 return;
572 }
573
574 let mut request = ImageRequest::new(
575 format.clone(),
576 data.clone(),
577 limits.max_decoded_len.bytes() as u64,
578 options.downscale.clone(),
579 options.mask,
580 );
581 request.entries = options.entries;
582
583 if is_respawn {
584 request.parent = r.with(|r| r.data.meta.parent.clone());
585 }
586
587 if VIEW_PROCESS.is_connected()
588 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
589 {
590 image_view(
592 cache_key,
593 view_img,
594 ImageDecoded::default(),
595 Some((format, data, options, limits)),
596 r,
597 );
598 } else {
599 tracing::debug!("image view request failed, will retry on respawn");
600 let mut once = Some((format, data, options, limits, r));
601 VIEW_PROCESS_INITED_EVENT
602 .hook(move |_| {
603 let (format, data, options, limits, r) = once.take().unwrap();
604 image_data(true, cache_key, format, data, options, limits, r);
605 false
606 })
607 .perm();
608 }
609}
610fn image_view(
612 cache_key: Option<ImageHash>,
613 handle: ViewImageHandle,
614 decoded: ImageDecoded,
615 respawn_data: Option<(ImageDataFormat, IpcBytes, ImageOptions, ImageLimits)>,
616 r: Var<ImageEntry>,
617) {
618 let mut img = r.get();
620 img.cache_key = cache_key;
621 img.handle = handle;
622 img.data = decoded;
623
624 let is_loaded = img.is_loaded();
625 let is_dummy = img.view_handle().is_dummy();
626 r.set(img);
627
628 if is_loaded {
629 image_decoded(r);
630 return;
631 }
632
633 if is_dummy {
634 tracing::error!("tried to register dummy handle");
635 return;
636 }
637
638 let decoding_respawn_handle = if respawn_data.is_some() {
640 let r_weak = r.downgrade();
641 let mut respawn_data = respawn_data;
642 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
643 if let Some(r) = r_weak.upgrade() {
644 let (format, data, options, limits) = respawn_data.take().unwrap();
645 image_data(true, cache_key, format, data, options, limits, r);
646 }
647 false
648 })
649 } else {
650 VarHandle::dummy()
652 };
653
654 let r_weak = r.downgrade();
656 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
657 Some(r) => {
658 if let Some(handle) = args.handle.upgrade()
659 && r.with(|img| img.view_handle() == &handle)
660 {
661 tracing::debug!("image view error, {}", args.error);
662 r.set(ImageEntry::new_error(args.error.clone()));
663 false
664 } else {
665 r.with(ImageEntry::is_loading)
666 }
667 }
668 None => false,
669 });
670
671 let r_weak = r.downgrade();
673 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
674 Some(r) => {
675 let handle = match args.handle.upgrade() {
676 Some(h) => h,
677 None => return r.with(ImageEntry::is_loading),
678 };
679 if r.with(|img| img.view_handle() == &handle) {
680 let meta = args.meta.clone();
681 tracing::trace!("image view metadata decoded for request");
682 r.modify(move |i| i.data.meta = meta);
683 } else if let Some(p) = &args.meta.parent
684 && p.parent == r.with(|img| img.view_handle().image_id())
685 {
686 tracing::trace!("image view metadata decoded for entry of request");
688 let mut entry_d = ImageDecoded::default();
689 entry_d.meta = args.meta.clone();
690 let entry = var(ImageEntry::new(None, handle.clone(), entry_d.clone()));
691 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
692 image_view(None, handle, entry_d, None, entry);
693 }
694 r.with(ImageEntry::is_loading)
695 }
696 None => false,
697 });
698
699 let r_weak = r.downgrade();
701 RAW_IMAGE_DECODED_EVENT
702 .hook(move |args| {
703 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
704 match r_weak.upgrade() {
705 Some(r) => {
706 if let Some(handle) = args.handle.upgrade()
707 && r.with(|img| img.view_handle() == &handle)
708 {
709 let data = args.image.upgrade().unwrap();
710 let is_loading = data.partial.is_some();
711 tracing::trace!("image view decoded, partial={:?}", is_loading);
712 r.modify(move |i| i.data = (*data.0).clone());
713 if !is_loading {
714 image_decoded(r);
715 }
716 is_loading
717 } else {
718 r.with(ImageEntry::is_loading)
719 }
720 }
721 None => false,
722 }
723 })
724 .perm();
725}
726fn image_decoded(r: Var<ImageEntry>) {
728 let r_weak = r.downgrade();
729 VIEW_PROCESS_INITED_EVENT
730 .hook(move |_| {
731 if let Some(r) = r_weak.upgrade() {
732 let img = r.get();
733 if !img.is_loaded() {
734 return false;
736 }
737
738 let size = img.size();
740 let mut options = ImageOptions::none();
741 let format = match img.is_mask() {
742 true => {
743 options.mask = Some(ImageMaskMode::A);
744 ImageDataFormat::A8 { size }
745 }
746 false => ImageDataFormat::Bgra8 {
747 size,
748 density: img.density(),
749 original_color_type: img.original_color_type(),
750 },
751 };
752 image_data(
753 true,
754 img.cache_key,
755 format,
756 img.data.pixels.clone(),
757 options,
758 ImageLimits::none(),
759 r,
760 );
761 }
762 false
763 })
764 .perm();
765}
766
767fn image_render(
769 cache_key: Option<ImageHash>,
770 render_fn: crate::RenderFn,
771 args: Option<ImageRenderArgs>,
772 options: ImageOptions,
773 r: Var<ImageEntry>,
774) {
775 let s = IMAGES_SV.read();
776 let windows = s.render_windows();
777 let windows_ctx = windows.clone_boxed();
778 let mask = options.mask;
779 windows.open_headless_window(Box::new(move || {
780 let ctx = ImageRenderCtx::new();
781 let retain = ctx.retain.clone();
782 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
783 let w = render_fn(&args.unwrap_or_default());
784 windows_ctx.enable_frame_capture_in_window_context(mask);
785 image_render_open(cache_key, WINDOW.id(), retain, r);
786 w
787 }));
788}
789
790fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
791 let r_weak = r.downgrade();
793 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
794 if args.window_id == win_id {
795 if let Some(r) = r_weak.upgrade() {
796 r.set(ImageEntry::new_error(args.error.clone()));
797 }
798 false
799 } else {
800 true
801 }
802 });
803 RAW_HEADLESS_OPEN_EVENT
805 .hook(move |args| {
806 let _hold = &error_handle;
807 args.window_id != win_id
808 })
809 .perm();
810
811 let r_weak = r.downgrade();
813 RAW_FRAME_RENDERED_EVENT
814 .hook(move |args| {
815 if args.window_id == win_id {
816 if let Some(r) = r_weak.upgrade() {
817 match args.frame_image.clone() {
818 Some(h) => {
819 let h = h.upgrade().unwrap();
820 let handle = h.0.0.clone();
821 let data = h.1.clone();
822 let retain = retain.get();
823 r.set(ImageEntry::new(cache_key, handle, data));
824 if !retain {
825 IMAGES_SV.read().render_windows().close_window(win_id);
826 image_decoded(r);
828 }
829 retain
831 }
832 None => {
833 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
834 false
835 }
836 }
837 } else {
838 false
839 }
840 } else {
841 true
842 }
843 })
844 .perm();
845}
846
847impl IMAGES {
848 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
861 where
862 N: FnOnce() -> R + Send + Sync + 'static,
863 R: ImageRenderWindowRoot,
864 {
865 let render = Mutex::new(Some(render));
866 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
867 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
868 self.image_impl(source, options, None)
869 }
870
871 pub fn render_node(
882 &self,
883 render_mode: RenderMode,
884 mask: Option<ImageMaskMode>,
885 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
886 ) -> ImageVar {
887 let render = Mutex::new(Some(render));
888 let source = ImageSource::render_node(render_mode, move |_| {
889 render.lock().take().expect("IMAGES.render closure called more than once")()
890 });
891 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
892 self.image_impl(source, options, None)
893 }
894}
895
896#[expect(non_camel_case_types)]
898pub struct IMAGES_WINDOW;
899impl IMAGES_WINDOW {
900 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
904 let mut img = IMAGES_SV.write();
905 img.render_windows = Some(service);
906 }
907}
908
909pub trait ImageRenderWindowsService: Send + Sync + 'static {
913 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
915
916 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
920
921 fn set_parent_in_window_context(&self, parent_id: WindowId);
925
926 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
932
933 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
937
938 fn close_window(&self, window_id: WindowId);
940}
941
942pub trait ImageRenderWindowRoot: Send + Any + 'static {}
946
947#[expect(non_camel_case_types)]
951pub struct IMAGE_RENDER;
952impl IMAGE_RENDER {
953 pub fn is_in_render(&self) -> bool {
957 WINDOW.contains_state(*IMAGE_RENDER_ID)
958 }
959
960 pub fn retain(&self) -> Var<bool> {
964 WINDOW.req_state(*IMAGE_RENDER_ID).retain
965 }
966}
967
968#[zng_app::widget::property(CONTEXT, default(false))]
976pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
977 let retain = retain.into_var();
978 match_node(child, move |_, op| {
979 if let UiNodeOp::Init = op {
980 if IMAGE_RENDER.is_in_render() {
981 let actual_retain = IMAGE_RENDER.retain();
982 actual_retain.set_from(&retain);
983 let handle = actual_retain.bind(&retain);
984 WIDGET.push_var_handle(handle);
985 } else {
986 tracing::error!("can only set `render_retain` in render widgets")
987 }
988 }
989 })
990}
991
992#[derive(Clone)]
993struct ImageRenderCtx {
994 retain: Var<bool>,
995}
996impl ImageRenderCtx {
997 fn new() -> Self {
998 Self { retain: var(false) }
999 }
1000}
1001
1002static_id! {
1003 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1004}