Skip to main content

ferroui_base/rendering/composition/brushes/
server_composition_nine_grid_brush.rs

1//! The server-side counterparts of the WinUI nine-grid and mask brushes, and
2//! the rectangles of a nine-grid.
3
4use super::{paint_composition_brush, ServerCompositionSurfaceBrush};
5use crate::media::imaging::BitmapBlendingMode;
6use crate::media::{BoxShadows, RenderOptions};
7use crate::platform::IDrawingContextImpl;
8use crate::rendering::composition::generated::{
9    ServerCompositionBrushHooks, ServerCompositionBrushProps, ServerCompositionMaskBrushHooks,
10    ServerCompositionMaskBrushProps, ServerCompositionNineGridBrushHooks, ServerCompositionNineGridBrushProps,
11};
12use crate::rendering::composition::server::{
13    impl_server_render_resource, IAnimatedServerObject, IServerObject, IServerRenderResource, ServerCompositor,
14    ServerRenderResource,
15};
16use crate::rendering::composition::transport::BatchStreamReader;
17use crate::{Rect, RoundedRect};
18use std::any::{Any, TypeId};
19use std::cell::Cell;
20use std::rc::{Rc, Weak};
21use std::time::Duration;
22
23/// The four edges of a nine-grid, in the order left, top, right, bottom.
24pub type NineGridEdges = [f64; 4];
25
26/// The index of the centre cell among the nine cells of
27/// [`nine_grid_rects`].
28pub const NINE_GRID_CENTER: usize = 4;
29
30/// The lines that cut `length` from `start` into three: `first` from the
31/// start, `last` from the end. Negative insets count as zero; insets that
32/// together exceed the length are scaled down to meet.
33fn nine_grid_lines(start: f64, length: f64, first: f64, last: f64) -> [f64; 4] {
34    let (mut first, mut last) = (first.max(0.0), last.max(0.0));
35    let length = length.max(0.0);
36    if first + last > length && first + last > 0.0 {
37        let scale = length / (first + last);
38        first *= scale;
39        last *= scale;
40    }
41    [start, start + first, start + length - last, start + length]
42}
43
44/// The nine cells of a nine-grid, row by row from the top left corner (the
45/// centre is [`NINE_GRID_CENTER`]): each pair is the cell of `source` cut by
46/// `insets` and the cell of `dest` it is drawn into, cut by `insets` times
47/// `scales`. Insets that do not fit are scaled down to meet, in the source
48/// and in the destination alike.
49pub fn nine_grid_rects(source: Rect, dest: Rect, insets: NineGridEdges, scales: NineGridEdges) -> [(Rect, Rect); 9] {
50    let [left, top, right, bottom] = insets;
51    let [left_scale, top_scale, right_scale, bottom_scale] = scales;
52    let source_x = nine_grid_lines(source.x, source.width, left, right);
53    let source_y = nine_grid_lines(source.y, source.height, top, bottom);
54    let dest_x = nine_grid_lines(dest.x, dest.width, left * left_scale, right * right_scale);
55    let dest_y = nine_grid_lines(dest.y, dest.height, top * top_scale, bottom * bottom_scale);
56    let cell = |x: &[f64; 4], y: &[f64; 4], column: usize, row: usize| {
57        Rect::new(x[column], y[row], x[column + 1] - x[column], y[row + 1] - y[row])
58    };
59    std::array::from_fn(|index| {
60        let (row, column) = (index / 3, index % 3);
61        (cell(&source_x, &source_y, column, row), cell(&dest_x, &dest_y, column, row))
62    })
63}
64
65fn is_empty(rect: &Rect) -> bool {
66    rect.width <= 0.0 || rect.height <= 0.0
67}
68
69/// The server-side counterpart of a
70/// [`CompositionNineGridBrush`](super::CompositionNineGridBrush), painted
71/// by the visuals that show it ([`paint_composition_brush`]).
72pub struct ServerCompositionNineGridBrush {
73    base: ServerRenderResource,
74    props: ServerCompositionNineGridBrushProps,
75    /// Set while the brush paints: a source that contains the brush is not
76    /// painted inside itself.
77    painting: Cell<bool>,
78}
79
80impl ServerCompositionNineGridBrush {
81    /// Creates the brush, active: the UI-thread brush activates its server
82    /// object when it is created.
83    pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<ServerCompositionNineGridBrush> {
84        let brush = Rc::new_cyclic(|this: &Weak<ServerCompositionNineGridBrush>| ServerCompositionNineGridBrush {
85            base: ServerRenderResource::new(
86                compositor,
87                this.clone() as Weak<dyn IAnimatedServerObject>,
88                this.clone() as Weak<dyn IServerRenderResource>,
89            ),
90            props: ServerCompositionNineGridBrushProps::new(),
91            painting: Cell::new(false),
92        });
93        brush.base.object().activate();
94        brush
95    }
96
97    /// The properties of the brush.
98    pub fn props(&self) -> &ServerCompositionNineGridBrushProps {
99        &self.props
100    }
101
102    /// The properties of the generated `ServerCompositionBrush`.
103    pub fn brush_props(&self) -> &ServerCompositionBrushProps {
104        self.props.base()
105    }
106
107    pub fn is_disposed(&self) -> bool {
108        self.base.is_disposed()
109    }
110
111    /// The insets, left, top, right, bottom.
112    pub fn insets(&self) -> NineGridEdges {
113        [
114            self.props.left_inset() as f64,
115            self.props.top_inset() as f64,
116            self.props.right_inset() as f64,
117            self.props.bottom_inset() as f64,
118        ]
119    }
120
121    /// The inset scales, left, top, right, bottom.
122    pub fn inset_scales(&self) -> NineGridEdges {
123        [
124            self.props.left_inset_scale() as f64,
125            self.props.top_inset_scale() as f64,
126            self.props.right_inset_scale() as f64,
127            self.props.bottom_inset_scale() as f64,
128        ]
129    }
130
131    /// Paints the brush over `rect`, in the coordinates of `canvas`.
132    ///
133    /// A surface brush source has its surface cut at its natural size and
134    /// each cell drawn into its cell of `rect`. A solid colour source fills
135    /// the cells of `rect`; any other source is painted over `rect`, clipped
136    /// to the cells (all at once when the centre is not hollow).
137    pub fn paint(&self, canvas: &mut dyn IDrawingContextImpl, rect: Rect) {
138        let Some(source) = self.props.source() else { return };
139        let opacity = self.brush_props().opacity();
140        if opacity <= 0.0 || is_empty(&rect) || self.painting.replace(true) {
141            return;
142        }
143        if opacity != 1.0 {
144            canvas.push_opacity(opacity, Some(rect));
145        }
146        let hollow = self.props.is_center_hollow();
147        let insets = self.insets();
148        let scales = self.inset_scales();
149
150        if let Some(surface_brush) = source.as_any().downcast_ref::<ServerCompositionSurfaceBrush>() {
151            let surface = surface_brush.surface();
152            let natural_size = surface.as_ref().and_then(|surface| surface.natural_size());
153            let surface_opacity = surface_brush.brush_props().opacity();
154            if let (Some(surface), Some(natural_size)) = (surface, natural_size) {
155                let natural = Rect::new(0.0, 0.0, natural_size.width, natural_size.height);
156                for (index, (from, to)) in nine_grid_rects(natural, rect, insets, scales).iter().enumerate() {
157                    if (hollow && index == NINE_GRID_CENTER) || is_empty(from) || is_empty(to) {
158                        continue;
159                    }
160                    // The whole surface, placed so that the source cell covers
161                    // the destination cell, clipped to that cell.
162                    let scale_x = to.width / from.width;
163                    let scale_y = to.height / from.height;
164                    let whole = Rect::new(
165                        to.x - from.x * scale_x,
166                        to.y - from.y * scale_y,
167                        natural.width * scale_x,
168                        natural.height * scale_y,
169                    );
170                    canvas.push_clip(*to);
171                    surface.draw(canvas, whole, surface_opacity);
172                    canvas.pop_clip();
173                }
174            }
175        } else {
176            let solid = source.clone().as_brush().filter(|brush| brush.as_solid_color_brush().is_some());
177            let cells = nine_grid_rects(rect, rect, insets, scales);
178            if let Some(solid) = solid {
179                for (index, (_, to)) in cells.iter().enumerate() {
180                    if (hollow && index == NINE_GRID_CENTER) || is_empty(to) {
181                        continue;
182                    }
183                    canvas.draw_rectangle(Some(&*solid), None, RoundedRect::from_rect(*to), &BoxShadows::default());
184                }
185            } else if !hollow {
186                paint_composition_brush(canvas, &source, rect);
187            } else {
188                for (index, (_, to)) in cells.iter().enumerate() {
189                    if index == NINE_GRID_CENTER || is_empty(to) {
190                        continue;
191                    }
192                    canvas.push_clip(*to);
193                    paint_composition_brush(canvas, &source, rect);
194                    canvas.pop_clip();
195                }
196            }
197        }
198
199        if opacity != 1.0 {
200            canvas.pop_opacity();
201        }
202        self.painting.set(false);
203    }
204}
205
206impl_server_render_resource!(
207    ServerCompositionNineGridBrush,
208    base,
209    composition_property: ServerCompositionNineGridBrushProps::get_composition_property
210);
211
212impl ServerCompositionBrushHooks for ServerCompositionNineGridBrush {}
213
214impl ServerCompositionNineGridBrushHooks for ServerCompositionNineGridBrush {}
215
216impl IServerObject for ServerCompositionNineGridBrush {
217    fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
218        self.props.deserialize_changes_core(self, reader, committed_at);
219    }
220
221    fn values_invalidated(&self) {
222        self.base.core().invalidated(self);
223    }
224
225    fn dispose(&self) {
226        self.base.core().dispose();
227    }
228
229    fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
230        Some(self)
231    }
232
233    fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
234        Some(self)
235    }
236
237    fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
238        self.props.find_props(type_id)
239    }
240
241    fn as_any(&self) -> &dyn Any {
242        self
243    }
244
245    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
246        self
247    }
248}
249
250/// The server-side counterpart of a
251/// [`CompositionMaskBrush`](super::CompositionMaskBrush), painted by the
252/// visuals that show it ([`paint_composition_brush`]).
253pub struct ServerCompositionMaskBrush {
254    base: ServerRenderResource,
255    props: ServerCompositionMaskBrushProps,
256    /// Set while the brush paints: a source or mask that contains the brush
257    /// is not painted inside itself.
258    painting: Cell<bool>,
259}
260
261impl ServerCompositionMaskBrush {
262    /// Creates the brush, active: the UI-thread brush activates its server
263    /// object when it is created.
264    pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<ServerCompositionMaskBrush> {
265        let brush = Rc::new_cyclic(|this: &Weak<ServerCompositionMaskBrush>| ServerCompositionMaskBrush {
266            base: ServerRenderResource::new(
267                compositor,
268                this.clone() as Weak<dyn IAnimatedServerObject>,
269                this.clone() as Weak<dyn IServerRenderResource>,
270            ),
271            props: ServerCompositionMaskBrushProps::new(),
272            painting: Cell::new(false),
273        });
274        brush.base.object().activate();
275        brush
276    }
277
278    /// The properties of the brush.
279    pub fn props(&self) -> &ServerCompositionMaskBrushProps {
280        &self.props
281    }
282
283    /// The properties of the generated `ServerCompositionBrush`.
284    pub fn brush_props(&self) -> &ServerCompositionBrushProps {
285        self.props.base()
286    }
287
288    pub fn is_disposed(&self) -> bool {
289        self.base.is_disposed()
290    }
291
292    /// Paints the source over `rect` through the mask: a brush of the drawing
293    /// context (a colour or gradient) is pushed as the opacity mask; any other
294    /// mask is painted over the source in a layer with the destination-in
295    /// blending of bitmaps. Without a mask the source is painted as it is.
296    pub fn paint(&self, canvas: &mut dyn IDrawingContextImpl, rect: Rect) {
297        let Some(source) = self.props.source() else { return };
298        let opacity = self.brush_props().opacity();
299        if opacity <= 0.0 || is_empty(&rect) || self.painting.replace(true) {
300            return;
301        }
302        if opacity != 1.0 {
303            canvas.push_opacity(opacity, Some(rect));
304        }
305        match self.props.mask() {
306            None => paint_composition_brush(canvas, &source, rect),
307            Some(mask) => match mask.clone().as_brush() {
308                Some(mask_brush) => {
309                    canvas.push_opacity_mask(&*mask_brush, rect);
310                    paint_composition_brush(canvas, &source, rect);
311                    canvas.pop_opacity_mask();
312                }
313                None => {
314                    // Deviation (winui-composition.md): the drawing contexts
315                    // blend bitmaps only, so only the bitmaps of the mask mask.
316                    canvas.push_layer(rect);
317                    paint_composition_brush(canvas, &source, rect);
318                    canvas.push_render_options(RenderOptions {
319                        bitmap_blending_mode: BitmapBlendingMode::DestinationIn,
320                        ..RenderOptions::default()
321                    });
322                    paint_composition_brush(canvas, &mask, rect);
323                    canvas.pop_render_options();
324                    canvas.pop_layer();
325                }
326            },
327        }
328        if opacity != 1.0 {
329            canvas.pop_opacity();
330        }
331        self.painting.set(false);
332    }
333}
334
335impl_server_render_resource!(
336    ServerCompositionMaskBrush,
337    base,
338    composition_property: ServerCompositionMaskBrushProps::get_composition_property
339);
340
341impl ServerCompositionBrushHooks for ServerCompositionMaskBrush {}
342
343impl ServerCompositionMaskBrushHooks for ServerCompositionMaskBrush {}
344
345impl IServerObject for ServerCompositionMaskBrush {
346    fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
347        self.props.deserialize_changes_core(self, reader, committed_at);
348    }
349
350    fn values_invalidated(&self) {
351        self.base.core().invalidated(self);
352    }
353
354    fn dispose(&self) {
355        self.base.core().dispose();
356    }
357
358    fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
359        Some(self)
360    }
361
362    fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
363        Some(self)
364    }
365
366    fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
367        self.props.find_props(type_id)
368    }
369
370    fn as_any(&self) -> &dyn Any {
371        self
372    }
373
374    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
375        self
376    }
377}