Skip to main content

ferroui_base/rendering/composition/
alpha_mask.rs

1//! The alpha mask of an element (phase 7, not from upstream): what
2//! `Shape.GetAlphaMask`, `TextBlock.GetAlphaMask` and `Image.GetAlphaMask` of
3//! WinUI return, ported from `dxaml/xcp/components/graphics/AlphaMask.cpp` of
4//! microsoft-ui-xaml.
5
6use super::{CompositionBrush, CompositionStretch, CompositionSurfaceBrush, Compositor, ICompositionSurface, LoadedImageSurface};
7use crate::media::imaging::{Bitmap, BitmapBlendingMode, RenderTargetBitmap};
8use crate::media::RenderOptions;
9use crate::platform::{AlphaFormat, PixelFormats};
10use crate::reactive::IDisposable;
11use crate::{LocatorExtensions, PixelSize, Rect, Vector, Visual};
12use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14
15/// The largest side of the image of a mask, in pixels. A mask that would be
16/// larger is drawn smaller, and its brush scaled back up.
17///
18/// Deviation (winui-composition.md): WinUI limits the mask to the largest
19/// texture of the device (16384 pixels with Direct3D 11); the port draws
20/// the mask into a bitmap of the render interface and keeps it smaller.
21pub const ALPHA_MASK_MAX_SIZE: u32 = 4096;
22
23/// The side of the black bitmap the colour of the mask is replaced with.
24const BLACK_SIDE: i32 = 4;
25
26/// The alpha mask of an element: a [`CompositionSurfaceBrush`] that shows the
27/// alpha of what the element draws, for a `DropShadow.Mask` and other
28/// composition effects.
29///
30/// The element embeds one and forwards to it: [`get`](Self::get) from its
31/// `get_alpha_mask`, [`update_if_available`](Self::update_if_available) at the
32/// end of its `render` (WinUI's `NotifyRenderContent`).
33///
34/// As in WinUI the brush is created on the first request and stays the same
35/// object for the life of the element; its surface is an image of the
36/// element's own drawing at the scale of its transform to the window and of
37/// the scaling of the window (WinUI's realization scale), with the colour
38/// dropped (black with the alpha, as an alpha-only texture is sampled). The
39/// image is drawn again each time the element draws its content again, so
40/// it follows the text, the geometry, the source and the size; while the
41/// element has no size, is not visible or is not in a tree, the brush has no
42/// surface and paints nothing. Its `Stretch` is `Fill`, so a visual of the
43/// size of the element shows the mask over itself.
44#[derive(Default)]
45pub struct AlphaMask {
46    brush: RefCell<Option<CompositionSurfaceBrush>>,
47    /// Whether the brush has a surface (shared with the subscriptions).
48    connected: Rc<Cell<bool>>,
49    /// Set while the element is drawn into the mask: the drawing calls the
50    /// `render` of the element, which must not update the mask again.
51    updating: Cell<bool>,
52    /// The subscriptions that hide the mask when the element leaves its tree
53    /// or stops being visible.
54    subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
55}
56
57impl AlphaMask {
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// `GetAlphaMask`: the brush of the mask of `element`, created on the
63    /// first call and drawn at once when the element can be drawn (WinUI's
64    /// `AlphaMask::Ensure`). `None` when no compositor is available for the
65    /// element.
66    pub fn get(&self, element: &Visual) -> Option<Rc<CompositionBrush>> {
67        if let Some(brush) = self.brush.borrow().as_ref() {
68            return Some((**brush).clone());
69        }
70        let compositor = element
71            .composition_visual()
72            .map(|visual| visual.compositor().clone())
73            .or_else(Compositor::try_get_default_compositor)?;
74        let brush = compositor.create_surface_brush();
75        brush.set_stretch(CompositionStretch::Fill);
76        let result: Rc<CompositionBrush> = (*brush).clone();
77        *self.brush.borrow_mut() = Some(brush);
78        self.subscribe(element);
79        self.update_if_available(element);
80        Some(result)
81    }
82
83    /// Whether the brush was created.
84    pub fn is_present(&self) -> bool {
85        self.brush.borrow().is_some()
86    }
87
88    /// Whether the brush shows an image of the element.
89    pub fn is_connected(&self) -> bool {
90        self.connected.get()
91    }
92
93    /// Draws the mask of `element` again when the brush was created (WinUI's
94    /// `AlphaMask::UpdateIfAvailable`, called by the render walk for an
95    /// element whose content is drawn again): an element with a size, in a
96    /// tree and visible gets a new image; otherwise the brush loses its
97    /// surface.
98    pub fn update_if_available(&self, element: &Visual) {
99        if self.updating.get() {
100            return;
101        }
102        let Some(brush) = self.brush.borrow().clone() else { return };
103
104        let bounds = element.bounds();
105        let (scale_x, scale_y) = realization_scale(element);
106        let width = (scale_x * bounds.width).floor();
107        let height = (scale_y * bounds.height).floor();
108        let drawable = width >= 1.0 && height >= 1.0
109            && element.is_attached_to_visual_tree()
110            && element.is_effectively_visible()
111            && crate::FerroLocator::current().get_service::<dyn crate::platform::IPlatformRenderInterface>().is_some();
112        if !drawable {
113            self.hide();
114            return;
115        }
116
117        // Larger than the largest image: drawn smaller, and the brush scaled
118        // back (`EnsureBackingSurface`, `RasterizeElement`).
119        let largest = width.max(height);
120        let adjusted = if largest > ALPHA_MASK_MAX_SIZE as f64 { ALPHA_MASK_MAX_SIZE as f64 / largest } else { 1.0 };
121        let pixel_size = PixelSize::new(
122            ((width * adjusted) as i32).clamp(1, ALPHA_MASK_MAX_SIZE as i32),
123            ((height * adjusted) as i32).clamp(1, ALPHA_MASK_MAX_SIZE as i32),
124        );
125        // The DPI that maps the size of the element onto the pixels.
126        let dpi = Vector::new(
127            96.0 * pixel_size.width as f64 / bounds.width,
128            96.0 * pixel_size.height as f64 / bounds.height,
129        );
130
131        self.updating.set(true);
132        struct Reset<'a>(&'a Cell<bool>);
133        impl Drop for Reset<'_> {
134            fn drop(&mut self) {
135                self.0.set(false);
136            }
137        }
138        let _reset = Reset(&self.updating);
139
140        let image = RenderTargetBitmap::with_dpi(pixel_size, dpi);
141        {
142            let mut context = image.create_drawing_context();
143            element.render(&mut context);
144            // The colour goes: black where the element has alpha, in its
145            // proportion.
146            let black = black_bitmap();
147            let options = context.push_render_options(RenderOptions {
148                bitmap_blending_mode: BitmapBlendingMode::SourceIn,
149                ..RenderOptions::default()
150            });
151            let side = BLACK_SIDE as f64;
152            context.draw_image_with_rects(
153                &black,
154                Rect::new(1.0, 1.0, side - 2.0, side - 2.0),
155                Rect::new(0.0, 0.0, bounds.width, bounds.height).inflate(1.0),
156            );
157            context.pop(options);
158            context.dispose();
159            black.dispose();
160        }
161
162        let compositor = brush.compositor().clone();
163        let surface: Rc<dyn ICompositionSurface> = LoadedImageSurface::from_bitmap(&compositor, &image);
164        image.dispose();
165        brush.set_surface(Some(surface));
166        let inverse = 1.0 / adjusted;
167        brush.set_scale(Vector::new(inverse, inverse));
168        self.connected.set(true);
169    }
170
171    /// Takes the surface from the brush (WinUI's `AlphaMask::Hide`): the
172    /// element is not drawn, so its mask is empty.
173    pub fn hide(&self) {
174        if !self.connected.get() {
175            return;
176        }
177        if let Some(brush) = self.brush.borrow().as_ref() {
178            brush.set_surface(None);
179        }
180        self.connected.set(false);
181    }
182
183    /// Hides the mask when the element leaves its tree or stops being
184    /// visible; it is drawn again when the element draws its content.
185    fn subscribe(&self, element: &Visual) {
186        let mut subscriptions = self.subscriptions.borrow_mut();
187        if !subscriptions.is_empty() {
188            return;
189        }
190        let weak = element.to_ref().downgrade();
191        let brush = self.brush.borrow().clone();
192        let connected = self.connected.clone();
193        let hide_brush = move || {
194            if !connected.replace(false) {
195                return;
196            }
197            if let Some(brush) = &brush {
198                brush.set_surface(None);
199            }
200        };
201        let hide = Rc::new(hide_brush);
202        let on_detached = {
203            let hide = hide.clone();
204            element.detached_from_visual_tree(move |_| hide())
205        };
206        let on_visibility = {
207            let hide = hide.clone();
208            element.is_effectively_visible_changed(move || {
209                if weak.upgrade().is_some_and(|element| !element.is_effectively_visible()) {
210                    hide();
211                }
212            })
213        };
214        subscriptions.push(on_detached);
215        subscriptions.push(on_visibility);
216    }
217}
218
219impl Drop for AlphaMask {
220    fn drop(&mut self) {
221        // The application may keep the brush: it loses the image of an
222        // element that is gone (WinUI disconnects the surface).
223        self.hide();
224        for subscription in self.subscriptions.get_mut().drain(..) {
225            subscription.dispose();
226        }
227    }
228}
229
230/// The scale an element is drawn at in its window: the scale of its
231/// transform to the root of its tree times the scaling of the window
232/// (WinUI's `ComputeRealizationTransform` and `GetScaleDimensions`).
233fn realization_scale(element: &Visual) -> (f64, f64) {
234    let scaling = element.presentation_source().map_or(1.0, |source| source.render_scaling());
235    let transform = element
236        .get_visual_root()
237        .and_then(|root| element.transform_to_visual(&root))
238        .unwrap_or(crate::Matrix::IDENTITY);
239    let scale_x = (transform.m11 * transform.m11 + transform.m12 * transform.m12).sqrt();
240    let scale_y = (transform.m21 * transform.m21 + transform.m22 * transform.m22).sqrt();
241    let positive = |scale: f64| if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
242    (positive(scale_x) * scaling, positive(scale_y) * scaling)
243}
244
245/// A few opaque black pixels.
246fn black_bitmap() -> Bitmap {
247    let pixels = [0u8, 0, 0, 255].repeat((BLACK_SIDE * BLACK_SIDE) as usize);
248    Bitmap::from_pixels(
249        PixelFormats::RGBA8888,
250        AlphaFormat::Premul,
251        &pixels,
252        PixelSize::new(BLACK_SIDE, BLACK_SIDE),
253        Vector::new(96.0, 96.0),
254        BLACK_SIDE * 4,
255    )
256}