1use std::rc::Rc;
57use std::sync::Arc;
58
59use teksilo_canvas::live_image::{
60 LiveImageDraw, LiveImageError, LiveImageSource, LiveImageWriter, LivePixelFormat, ScalingFilter,
61};
62use teksilo_canvas::{Canvas, RasterIcon, Rect, Size, SizeProposal};
63use teksilo_core::accessibility::AccessNodeBuilder;
64use teksilo_core::build_context::BuildContext;
65use teksilo_core::environment::LayoutDirection;
66use teksilo_core::widget::{LayoutContext, PaintContext, Widget};
67use teksilo_core::{LiveImageAttachment, LiveImageSignals, WidgetId};
68use teksilo_tokens::Alignment;
69
70use super::image_mask::{ImageMaskShape, apply_alpha_mask, center_crop_square};
71
72pub use teksilo_canvas::image_geometry::ImageFit;
75
76pub(crate) struct CommittedImage {
79 source: LiveImageSource,
80 _writer: LiveImageWriter,
81}
82
83impl CommittedImage {
84 pub(crate) fn commit(
88 pixels: Vec<u8>,
89 width: u32,
90 height: u32,
91 ) -> Result<Rc<Self>, LiveImageError> {
92 let source = LiveImageSource::builder(LivePixelFormat::Rgba8)
93 .label("image")
94 .build();
95 let writer = source.writer();
96 writer
97 .swap_frame(width, height, width as usize * 4, pixels)
98 .map_err(|rejected| rejected.error)?;
99 Ok(Rc::new(Self {
100 source,
101 _writer: writer,
102 }))
103 }
104
105 pub(crate) fn size(&self) -> (u32, u32) {
107 self.source.size().unwrap_or((0, 0))
108 }
109
110 #[cfg(test)]
111 pub(crate) fn source(&self) -> &LiveImageSource {
112 &self.source
113 }
114}
115
116pub(crate) fn masked(
120 pixels: Vec<u8>,
121 width: u32,
122 height: u32,
123 shape: ImageMaskShape,
124) -> (Vec<u8>, u32, u32) {
125 let enough = (width as usize)
126 .checked_mul(height as usize)
127 .and_then(|n| n.checked_mul(4))
128 .is_some_and(|needed| width > 0 && height > 0 && pixels.len() >= needed);
129 if matches!(shape, ImageMaskShape::None) || !enough {
130 return (pixels, width, height);
131 }
132 let tight = &pixels[..width as usize * height as usize * 4];
133 let (mut cropped, side) = center_crop_square(tight, width, height);
134 apply_alpha_mask(&mut cropped, side, side, shape);
135 (cropped, side, side)
136}
137
138enum Pixels {
140 Shared { name: String, upload: Arc<[u8]> },
143 Owned(Owned),
147}
148
149#[derive(Default)]
150struct Owned {
151 pending: Option<Vec<u8>>,
153 committed: Option<Rc<CommittedImage>>,
154 signals: Option<LiveImageSignals>,
155 attachment: Option<LiveImageAttachment>,
156}
157
158pub struct ImageWidget {
160 pixels: Pixels,
161 width: u32,
162 height: u32,
163 fit: ImageFit,
164 alignment: Alignment,
169 display_width: Option<f32>,
174 display_height: Option<f32>,
175 resizable: bool,
180 alt: Option<String>,
182 a11y_hidden: bool,
186}
187
188impl ImageWidget {
189 pub fn new(icon: &RasterIcon) -> Self {
206 Self {
207 pixels: Pixels::Shared {
208 name: format!("_img_{}", icon.texture_key()),
209 upload: Arc::clone(icon.shared_pixels()),
210 },
211 width: icon.width(),
212 height: icon.height(),
213 fit: ImageFit::Contain,
214 alignment: Alignment::CENTER,
215 display_width: None,
216 display_height: None,
217 resizable: true,
218 alt: None,
219 a11y_hidden: false,
220 }
221 }
222
223 pub fn from_raw(pixels: Vec<u8>, width: u32, height: u32) -> Self {
234 Self {
235 pixels: Pixels::Owned(Owned {
236 pending: Some(pixels),
237 ..Owned::default()
238 }),
239 width,
240 height,
241 fit: ImageFit::Contain,
242 alignment: Alignment::CENTER,
243 display_width: None,
244 display_height: None,
245 resizable: true,
246 alt: None,
247 a11y_hidden: false,
248 }
249 }
250
251 pub(crate) fn from_committed(image: Rc<CommittedImage>) -> Self {
255 let (width, height) = image.size();
256 Self {
257 pixels: Pixels::Owned(Owned {
258 committed: Some(image),
259 ..Owned::default()
260 }),
261 width,
262 height,
263 fit: ImageFit::Contain,
264 alignment: Alignment::CENTER,
265 display_width: None,
266 display_height: None,
267 resizable: true,
268 alt: None,
269 a11y_hidden: false,
270 }
271 }
272
273 pub fn mask(mut self, shape: ImageMaskShape) -> Self {
289 if matches!(shape, ImageMaskShape::None) {
290 return self;
291 }
292 let pixels = match std::mem::replace(&mut self.pixels, Pixels::Owned(Owned::default())) {
293 Pixels::Shared { upload, .. } => upload.to_vec(),
294 Pixels::Owned(Owned {
295 pending: Some(pixels),
296 ..
297 }) => pixels,
298 Pixels::Owned(owned) => {
300 self.pixels = Pixels::Owned(owned);
301 return self;
302 }
303 };
304 let (masked, width, height) = masked(pixels, self.width, self.height, shape);
305 self.pixels = Pixels::Owned(Owned {
306 pending: Some(masked),
307 ..Owned::default()
308 });
309 self.width = width;
310 self.height = height;
311 self
312 }
313
314 pub fn fit(mut self, fit: ImageFit) -> Self {
317 self.fit = fit;
318 self
319 }
320
321 pub fn alignment(mut self, alignment: Alignment) -> Self {
326 self.alignment = alignment;
327 self
328 }
329
330 pub fn width(mut self, w: f32) -> Self {
335 self.display_width = Some(w);
336 self
337 }
338
339 pub fn height(mut self, h: f32) -> Self {
343 self.display_height = Some(h);
344 self
345 }
346
347 pub fn size(mut self, w: f32, h: f32) -> Self {
352 self.display_width = Some(w);
353 self.display_height = Some(h);
354 self
355 }
356
357 pub fn resizable(mut self, resizable: bool) -> Self {
364 self.resizable = resizable;
365 self
366 }
367
368 pub fn alt(mut self, text: impl Into<String>) -> Self {
370 self.alt = Some(text.into());
371 self
372 }
373
374 pub fn a11y_hidden(mut self) -> Self {
379 self.a11y_hidden = true;
380 self
381 }
382
383 fn aspect_ratio(&self) -> f32 {
385 if self.height == 0 {
386 1.0
387 } else {
388 self.width as f32 / self.height as f32
389 }
390 }
391
392 fn fitted_rect(&self, bounds: Rect, rtl: bool) -> Rect {
396 self.fit.fitted_rect(
397 Size::new(self.width as f32, self.height as f32),
398 bounds,
399 self.alignment,
400 rtl,
401 )
402 }
403}
404
405impl std::fmt::Debug for ImageWidget {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 let path = match &self.pixels {
408 Pixels::Shared { .. } => "shared",
409 Pixels::Owned(_) => "owned",
410 };
411 f.debug_struct("ImageWidget")
412 .field("width", &self.width)
413 .field("height", &self.height)
414 .field("fit", &self.fit)
415 .field("pixels", &path)
416 .finish()
417 }
418}
419
420impl Widget for ImageWidget {
421 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
424 let Pixels::Owned(owned) = &mut self.pixels else {
425 return vec![];
426 };
427 if owned.committed.is_none()
428 && let Some(pixels) = owned.pending.take()
429 {
430 let bytes = pixels.len();
431 match CommittedImage::commit(pixels, self.width, self.height) {
432 Ok(image) => owned.committed = Some(image),
433 Err(error) => eprintln!(
434 "teksilo: an image of {}x{} with {bytes} bytes of pixels was refused \
435 ({error}): it draws nothing",
436 self.width, self.height
437 ),
438 }
439 }
440 if let Some(image) = &owned.committed {
441 let signals = owned
442 .signals
443 .get_or_insert_with(|| LiveImageSignals::new(&image.source));
444 owned.attachment = Some(ctx.attach_live_image(&image.source, signals));
445 }
446 vec![]
447 }
448
449 fn layout_response(
450 &self,
451 proposal: SizeProposal,
452 _ctx: &LayoutContext,
453 ) -> teksilo_core::widget::LayoutResponse {
454 let ar = self.aspect_ratio();
455
456 let size = match (self.display_width, self.display_height) {
457 (Some(w), Some(h)) => Size::new(w, h),
460 (Some(w), None) => Size::new(w, w / ar),
463 (None, Some(h)) => Size::new(h * ar, h),
464 (None, None) => {
467 let natural_w = self.width as f32;
468 let natural_h = self.height as f32;
469 if !self.resizable {
470 Size::new(natural_w, natural_h)
471 } else {
472 match (proposal.width, proposal.height) {
473 (Some(pw), Some(ph)) => {
475 let scale = (pw / natural_w).min(ph / natural_h);
476 Size::new(natural_w * scale, natural_h * scale)
477 }
478 (Some(pw), None) => Size::new(pw, pw / ar),
480 (None, Some(ph)) => Size::new(ph * ar, ph),
482 (None, None) => Size::new(natural_w, natural_h),
484 }
485 }
486 }
487 };
488 size.into()
489 }
490
491 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
492 let rtl = matches!(ctx.layout_direction, LayoutDirection::RightToLeft);
493 let rect = self.fitted_rect(bounds, rtl);
494 let (name, upload) = match &self.pixels {
495 Pixels::Shared { name, upload } => (name, upload),
496 Pixels::Owned(owned) => {
497 if let Some(attachment) = &owned.attachment {
502 canvas.draw_live_image(
503 attachment.consumer(),
504 &LiveImageDraw::new(rect, bounds).filter(ScalingFilter::Trilinear),
505 );
506 }
507 return;
508 }
509 };
510 if !canvas.has_pending_image(name) {
512 canvas.ensure_shared_image_registered(
513 name,
514 self.width,
515 self.height,
516 Arc::clone(upload),
517 );
518 }
519
520 const EPS: f32 = 0.01;
528 let overflows = rect.x < bounds.x - EPS
529 || rect.y < bounds.y - EPS
530 || rect.x + rect.width > bounds.x + bounds.width + EPS
531 || rect.y + rect.height > bounds.y + bounds.height + EPS;
532 if overflows {
533 canvas.set_clip(bounds);
534 canvas.draw_image(rect, name);
535 canvas.clear_clip();
536 } else {
537 canvas.draw_image(rect, name);
538 }
539 }
540
541 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
542 if self.a11y_hidden {
543 builder.set_hidden();
544 return;
545 }
546 debug_assert!(
547 self.alt.is_some(),
548 "ImageWidget has no alt text — call .alt(\"…\") for meaningful images or .a11y_hidden() for decorative ones"
549 );
550 builder.set_role(teksilo_core::accesskit::Role::Image);
551 if let Some(ref alt) = self.alt {
552 builder.set_name(alt);
553 }
554 }
555}
556
557#[cfg(test)]
558impl ImageWidget {
559 fn shared_name(&self) -> Option<&str> {
561 match &self.pixels {
562 Pixels::Shared { name, .. } => Some(name),
563 Pixels::Owned(_) => None,
564 }
565 }
566
567 fn pending_pixels(&self) -> Option<&[u8]> {
569 match &self.pixels {
570 Pixels::Owned(owned) => owned.pending.as_deref(),
571 Pixels::Shared { .. } => None,
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use teksilo_core::widget_tree::WidgetTree;
580
581 #[test]
582 fn natural_size() {
583 let icon = RasterIcon::from_raw(vec![255; 400], 10, 10);
584 let mut tree = WidgetTree::new();
585 let img = tree.add(ImageWidget::new(&icon));
586 tree.layout(SizeProposal::unspecified());
587 let b = tree.bounds(img);
588 assert!((b.width - 10.0).abs() < 0.01);
589 assert!((b.height - 10.0).abs() < 0.01);
590 }
591
592 #[test]
593 fn explicit_display_size() {
594 let icon = RasterIcon::from_raw(vec![255; 400], 10, 10);
595 let mut tree = WidgetTree::new();
596 let img = tree.add(ImageWidget::new(&icon).size(200.0, 100.0));
597 tree.layout(SizeProposal::unspecified());
598 let b = tree.bounds(img);
599 assert!((b.width - 200.0).abs() < 0.01);
600 assert!((b.height - 100.0).abs() < 0.01);
601 }
602
603 #[test]
604 fn size_that_fits_preserves_aspect_ratio() {
605 let icon = RasterIcon::from_raw(vec![255; 800], 20, 10); let widget = ImageWidget::new(&icon);
607 let theme = teksilo_core::presets::intui::light();
608 let ctx = LayoutContext::for_testing(&theme);
609 let size = widget
611 .layout_response(
612 SizeProposal {
613 width: Some(100.0),
614 height: None,
615 },
616 &ctx,
617 )
618 .size;
619 assert!((size.width - 100.0).abs() < 0.5, "width: {}", size.width);
620 assert!((size.height - 50.0).abs() < 0.5, "height: {}", size.height);
621 }
622
623 #[test]
624 fn paints_image_quad() {
625 let icon = RasterIcon::from_raw(vec![255; 400], 10, 10);
626 let mut tree = WidgetTree::new();
627 tree.add(ImageWidget::new(&icon));
628 tree.layout(SizeProposal::exact(10.0, 10.0));
629 let frame = tree.render();
630 assert!(!frame.images.is_empty(), "should render an image");
631 assert!(frame.images[0].tint.is_none(), "should be full-color");
632 }
633
634 #[test]
635 fn pending_image_registered() {
636 let icon = RasterIcon::from_raw(vec![255; 400], 10, 10);
637 let mut tree = WidgetTree::new();
638 tree.add(ImageWidget::new(&icon));
639 tree.layout(SizeProposal::exact(10.0, 10.0));
640 let frame = tree.render();
641 assert!(
642 !frame.pending_images.is_empty(),
643 "should register pending image"
644 );
645 }
646
647 #[test]
648 fn new_names_its_texture_after_the_icon_identity() {
649 let icon = RasterIcon::from_raw(vec![255; 16], 2, 2);
650 let a = ImageWidget::new(&icon);
651 let clone = icon.clone();
652 let b = ImageWidget::new(&clone);
653 let expected = format!("_img_{}", icon.texture_key());
654 assert_eq!(a.shared_name(), Some(expected.as_str()));
655 assert_eq!(
656 a.shared_name(),
657 b.shared_name(),
658 "a clone shares the original's texture"
659 );
660
661 let first_name = {
664 let first = Box::new(RasterIcon::from_raw(vec![255; 16], 2, 2));
665 ImageWidget::new(&first).shared_name().map(str::to_owned)
666 };
667 let second = Box::new(RasterIcon::from_raw(vec![0; 16], 2, 2));
668 assert_ne!(
669 ImageWidget::new(&second).shared_name().map(str::to_owned),
670 first_name
671 );
672 }
673
674 #[test]
675 fn from_raw_widgets_each_own_a_source() {
676 let mut tree = WidgetTree::new();
681 tree.add(
682 crate::primitives::HStack::new()
683 .child(ImageWidget::from_raw(vec![255; 16], 2, 2))
684 .child(ImageWidget::from_raw(vec![0; 16], 2, 2)),
685 );
686 tree.layout(SizeProposal::exact(100.0, 50.0));
687 let frame = tree.render();
688 assert_eq!(frame.live_images.len(), 2);
689 assert!(
690 !frame.live_images[0]
691 .consumer
692 .source()
693 .ptr_eq(frame.live_images[1].consumer.source())
694 );
695 assert!(frame.images.is_empty() && frame.pending_images.is_empty());
696 }
697
698 #[test]
699 fn fixed_size_is_rigid_against_constraining_proposal() {
700 let icon = RasterIcon::from_raw(vec![255; 512 * 512 * 4], 512, 512);
706 let widget = ImageWidget::new(&icon).size(32.0, 32.0);
707 let theme = teksilo_core::presets::intui::light();
708 let ctx = LayoutContext::for_testing(&theme);
709 let size = widget
712 .layout_response(
713 SizeProposal {
714 width: Some(600.0),
715 height: None,
716 },
717 &ctx,
718 )
719 .size;
720 assert!((size.width - 32.0).abs() < 0.01, "width: {}", size.width);
721 assert!((size.height - 32.0).abs() < 0.01, "height: {}", size.height);
722 }
723
724 #[test]
725 fn single_axis_pin_derives_other_from_aspect_ratio() {
726 let icon = RasterIcon::from_raw(vec![255; 40 * 10 * 4], 40, 10); let theme = teksilo_core::presets::intui::light();
728 let ctx = LayoutContext::for_testing(&theme);
729 let w_pinned = ImageWidget::new(&icon)
731 .width(200.0)
732 .layout_response(SizeProposal::exact(999.0, 999.0), &ctx)
733 .size;
734 assert!((w_pinned.width - 200.0).abs() < 0.01);
735 assert!(
736 (w_pinned.height - 50.0).abs() < 0.01,
737 "h: {}",
738 w_pinned.height
739 );
740 let h_pinned = ImageWidget::new(&icon)
742 .height(20.0)
743 .layout_response(SizeProposal::exact(999.0, 999.0), &ctx)
744 .size;
745 assert!(
746 (h_pinned.width - 80.0).abs() < 0.01,
747 "w: {}",
748 h_pinned.width
749 );
750 assert!((h_pinned.height - 20.0).abs() < 0.01);
751 }
752
753 #[test]
754 fn resizable_false_locks_natural_pixel_size() {
755 let icon = RasterIcon::from_raw(vec![255; 64 * 64 * 4], 64, 64);
756 let theme = teksilo_core::presets::intui::light();
757 let ctx = LayoutContext::for_testing(&theme);
758 let scaled = ImageWidget::new(&icon)
760 .layout_response(SizeProposal::exact(16.0, 16.0), &ctx)
761 .size;
762 assert!((scaled.width - 16.0).abs() < 0.01);
763 let locked = ImageWidget::new(&icon)
765 .resizable(false)
766 .layout_response(SizeProposal::exact(16.0, 16.0), &ctx)
767 .size;
768 assert!((locked.width - 64.0).abs() < 0.01, "w: {}", locked.width);
769 assert!((locked.height - 64.0).abs() < 0.01);
770 }
771
772 #[test]
773 fn contain_centers_inside_a_wider_box() {
774 let icon = RasterIcon::from_raw(vec![255; 10 * 10 * 4], 10, 10);
776 let widget = ImageWidget::new(&icon).fit(ImageFit::Contain);
777 let r = widget.fitted_rect(Rect::new(0.0, 0.0, 200.0, 100.0), false);
778 assert!((r.width - 100.0).abs() < 0.01);
779 assert!((r.height - 100.0).abs() < 0.01);
780 assert!((r.x - 50.0).abs() < 0.01, "x: {}", r.x); assert!((r.y - 0.0).abs() < 0.01);
782 }
783
784 #[test]
785 fn alignment_positions_content_within_box() {
786 use teksilo_tokens::{HAlignment, VAlignment};
787 let icon = RasterIcon::from_raw(vec![255; 10 * 10 * 4], 10, 10);
788 let widget = ImageWidget::new(&icon)
789 .fit(ImageFit::Contain)
790 .alignment(Alignment {
791 horizontal: HAlignment::Trailing,
792 vertical: VAlignment::Bottom,
793 });
794 let r = widget.fitted_rect(Rect::new(0.0, 0.0, 200.0, 100.0), false);
795 assert!((r.x - 100.0).abs() < 0.01, "x: {}", r.x);
797 assert!((r.y - 0.0).abs() < 0.01, "y: {}", r.y);
798 let r_rtl = widget.fitted_rect(Rect::new(0.0, 0.0, 200.0, 100.0), true);
800 assert!((r_rtl.x - 0.0).abs() < 0.01, "rtl x: {}", r_rtl.x);
801 }
802
803 #[test]
804 fn fit_none_draws_at_natural_pixel_size() {
805 let icon = RasterIcon::from_raw(vec![255; 20 * 20 * 4], 20, 20);
806 let widget = ImageWidget::new(&icon).fit(ImageFit::None);
807 let r = widget.fitted_rect(Rect::new(0.0, 0.0, 8.0, 8.0), false);
809 assert!((r.width - 20.0).abs() < 0.01);
810 assert!((r.height - 20.0).abs() < 0.01);
811 assert!((r.x - (-6.0)).abs() < 0.01, "x: {}", r.x); }
814
815 #[test]
816 fn cover_overflow_is_clipped_to_bounds() {
817 let icon = RasterIcon::from_raw(vec![255; 20 * 10 * 4], 20, 10);
820 let mut tree = WidgetTree::new();
821 tree.add(
822 ImageWidget::new(&icon)
823 .size(50.0, 50.0)
824 .fit(ImageFit::Cover),
825 );
826 tree.layout(SizeProposal::exact(50.0, 50.0));
827 let frame = tree.render();
828 let has_set_clip = frame
829 .draw_order
830 .iter()
831 .any(|c| matches!(c, teksilo_canvas::DrawCommand::SetClip(_)));
832 let has_clear_clip = frame
833 .draw_order
834 .iter()
835 .any(|c| matches!(c, teksilo_canvas::DrawCommand::ClearClip));
836 assert!(has_set_clip, "Cover overflow should emit SetClip");
837 assert!(has_clear_clip, "Cover overflow should emit ClearClip");
838 }
839
840 #[test]
841 fn contain_within_box_emits_no_clip() {
842 let icon = RasterIcon::from_raw(vec![255; 10 * 10 * 4], 10, 10);
844 let mut tree = WidgetTree::new();
845 tree.add(
846 ImageWidget::new(&icon)
847 .size(50.0, 50.0)
848 .fit(ImageFit::Contain),
849 );
850 tree.layout(SizeProposal::exact(50.0, 50.0));
851 let frame = tree.render();
852 let has_set_clip = frame
853 .draw_order
854 .iter()
855 .any(|c| matches!(c, teksilo_canvas::DrawCommand::SetClip(_)));
856 assert!(!has_set_clip, "Contain should not clip");
857 }
858
859 #[test]
860 fn mask_circle_alpha_zero_at_corners() {
861 let icon = RasterIcon::from_raw(vec![255; 32 * 32 * 4], 32, 32);
864 let widget = ImageWidget::from_raw(icon.pixels().to_vec(), icon.width(), icon.height())
865 .mask(ImageMaskShape::Circle);
866 let stride = (widget.width * 4) as usize;
869 let pixels = widget.pending_pixels().expect("masked pixels");
870 let top_left_alpha = pixels[3];
871 let top_right_alpha = pixels[stride - 1];
872 assert_eq!(top_left_alpha, 0);
873 assert_eq!(top_right_alpha, 0);
874 let center_idx = (((widget.height / 2) * widget.width + widget.width / 2) * 4 + 3) as usize;
876 assert_eq!(pixels[center_idx], 255);
877 }
878
879 #[test]
880 fn mask_none_is_passthrough() {
881 let original = vec![123, 45, 67, 200, 8, 9, 10, 200];
882 let widget = ImageWidget::from_raw(original.clone(), 2, 1).mask(ImageMaskShape::None);
883 assert_eq!(widget.pending_pixels(), Some(original.as_slice()));
884 let icon = RasterIcon::from_raw(original.clone(), 2, 1);
886 assert!(
887 ImageWidget::new(&icon)
888 .mask(ImageMaskShape::None)
889 .shared_name()
890 .is_some()
891 );
892 }
893
894 #[test]
895 fn mask_crops_non_square_to_square() {
896 let pixels = vec![255; 8 * 4 * 4];
898 let widget = ImageWidget::from_raw(pixels, 8, 4).mask(ImageMaskShape::Circle);
899 assert_eq!(widget.width, 4);
900 assert_eq!(widget.height, 4);
901 }
902
903 #[test]
904 fn a_masked_icon_owns_its_pixels_and_leaves_the_shared_texture() {
905 let icon = RasterIcon::from_raw(vec![255; 16 * 16 * 4], 16, 16);
908 let unmasked = ImageWidget::new(&icon);
909 let masked = ImageWidget::new(&icon).mask(ImageMaskShape::Circle);
910 assert!(unmasked.shared_name().is_some());
911 assert!(masked.shared_name().is_none());
912 assert!(masked.pending_pixels().is_some());
913 }
914}