Skip to main content

ferroui_base/rendering/composition/brushes/
server_composition_surface_brush.rs

1//! The server-side counterpart of the composition surface brush, and the
2//! painting of composition brushes by the visuals that show them.
3
4use crate::media::imaging::BitmapInterpolationMode;
5use crate::media::{BoxShadows, RenderOptions};
6use crate::platform::IDrawingContextImpl;
7use crate::rendering::composition::generated::{
8    ServerCompositionBrushHooks, ServerCompositionBrushProps, ServerCompositionSurfaceBrushHooks,
9    ServerCompositionSurfaceBrushProps,
10};
11use crate::rendering::composition::server::{
12    composition_transform, impl_server_render_resource, IAnimatedServerObject, IServerCompositionSurface,
13    IServerObject, IServerRenderResource, ServerCompositor, ServerRenderResource,
14};
15use crate::rendering::composition::transport::BatchStreamReader;
16use crate::rendering::composition::CompositionStretch;
17use crate::{Matrix, Rect, RoundedRect, Size, Vector};
18use std::any::{Any, TypeId};
19use std::rc::{Rc, Weak};
20use std::time::Duration;
21
22/// The server-side counterpart of a
23/// [`CompositionSurfaceBrush`](super::CompositionSurfaceBrush).
24///
25/// Unlike the colour and gradient brushes it is not a brush of the drawing
26/// context (`as_brush` is `None`): the visuals that show it paint it with
27/// [`paint_composition_brush`].
28pub struct ServerCompositionSurfaceBrush {
29    this: Weak<ServerCompositionSurfaceBrush>,
30    base: ServerRenderResource,
31    props: ServerCompositionSurfaceBrushProps,
32}
33
34impl ServerCompositionSurfaceBrush {
35    /// Creates the brush, active: the UI-thread brush activates its server
36    /// object when it is created.
37    pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<ServerCompositionSurfaceBrush> {
38        let brush = Rc::new_cyclic(|this: &Weak<ServerCompositionSurfaceBrush>| ServerCompositionSurfaceBrush {
39            this: this.clone(),
40            base: ServerRenderResource::new(
41                compositor,
42                this.clone() as Weak<dyn IAnimatedServerObject>,
43                this.clone() as Weak<dyn IServerRenderResource>,
44            ),
45            props: ServerCompositionSurfaceBrushProps::new(),
46        });
47        brush.base.object().activate();
48        brush
49    }
50
51    /// The properties of the brush.
52    pub fn props(&self) -> &ServerCompositionSurfaceBrushProps {
53        &self.props
54    }
55
56    /// The properties of the generated `ServerCompositionBrush`.
57    pub fn brush_props(&self) -> &ServerCompositionBrushProps {
58        self.props.base()
59    }
60
61    pub fn is_disposed(&self) -> bool {
62        self.base.is_disposed()
63    }
64
65    /// The surface the brush shows.
66    pub fn surface(&self) -> Option<Rc<dyn IServerCompositionSurface>> {
67        self.props.surface().and_then(|surface| surface.as_surface())
68    }
69
70    fn subscription_key(&self) -> usize {
71        self.this.as_ptr() as *const () as usize
72    }
73
74    /// Follows the `Changed` notification of the surface: a change of its
75    /// content invalidates the brush and so the visuals that show it. (A
76    /// surface that is a render resource is observed by the brush as well.)
77    fn subscribe(&self) {
78        if let Some(surface) = self.surface() {
79            let weak = self.this.clone();
80            surface.changed().add(
81                self.subscription_key(),
82                Rc::new(move || {
83                    if let Some(brush) = weak.upgrade() {
84                        brush.base.core().invalidated(&*brush);
85                    }
86                }),
87            );
88        }
89    }
90
91    fn unsubscribe(&self) {
92        if let Some(surface) = self.surface() {
93            surface.changed().remove(self.subscription_key());
94        }
95    }
96
97    /// The transform of the content of the brush in an area of `size`
98    /// (`AnchorPoint`, `CenterPoint`, `Offset`, `RotationAngle`, `Scale`,
99    /// `TransformMatrix`); `None` for the identity.
100    pub fn content_transform(&self, size: Vector) -> Option<Matrix> {
101        composition_transform(
102            size,
103            self.props.anchor_point(),
104            self.props.center_point(),
105            self.props.transform_matrix(),
106            self.props.scale(),
107            self.props.rotation_angle(),
108            self.props.offset(),
109        )
110    }
111
112    /// Paints the brush over `rect`, in the coordinates of `canvas`: the
113    /// surface at its natural size placed by the stretch and the alignment
114    /// ratios, transformed by the brush transform, clipped to `rect`.
115    ///
116    /// Deviation (winui-composition.md): the `Transform` and
117    /// `RelativeTransform` the composition brushes of the upstream framework
118    /// have are not applied to a surface brush; its own WinUI transform is.
119    pub fn paint(&self, canvas: &mut dyn IDrawingContextImpl, rect: Rect) {
120        let Some(surface) = self.surface() else { return };
121        let Some(natural_size) = surface.natural_size() else { return };
122        let Some(mut content) = surface_brush_content_rect(
123            natural_size,
124            rect,
125            self.props.stretch(),
126            self.props.horizontal_alignment_ratio() as f64,
127            self.props.vertical_alignment_ratio() as f64,
128        ) else {
129            return;
130        };
131        let opacity = self.brush_props().opacity();
132        if opacity <= 0.0 {
133            return;
134        }
135
136        canvas.push_clip(rect);
137
138        // Deviation (winui-composition.md): the drawing context has one
139        // interpolation quality; the WinUI default (`Linear`) leaves the
140        // quality of the context as it is.
141        let interpolation = self.props.bitmap_interpolation_mode().to_bitmap_interpolation_mode();
142        let push_options = interpolation != BitmapInterpolationMode::LowQuality;
143        if push_options {
144            canvas.push_render_options(RenderOptions {
145                bitmap_interpolation_mode: interpolation,
146                ..RenderOptions::default()
147            });
148        }
149
150        let saved = canvas.transform();
151        let mut world = saved;
152        if let Some(transform) = self.content_transform(Vector::new(rect.width, rect.height)) {
153            // The brush transform is in the space of the area: its origin is
154            // the top left corner of `rect`.
155            let local = Matrix::create_translation(-rect.x, -rect.y)
156                * transform
157                * Matrix::create_translation(rect.x, rect.y);
158            world = local * saved;
159        }
160        if self.props.snap_to_pixels() {
161            // Snaps the top left corner of the content to a device pixel.
162            let corner = content.top_left().transform(world);
163            let (dx, dy) = (corner.x.round() - corner.x, corner.y.round() - corner.y);
164            if dx != 0.0 || dy != 0.0 {
165                world = world * Matrix::create_translation(dx, dy);
166            }
167            content = Rect::new(content.x, content.y, content.width.round(), content.height.round());
168        }
169        let transformed = world != saved;
170        if transformed {
171            canvas.set_transform(world);
172        }
173        surface.draw(canvas, content, opacity);
174        if transformed {
175            canvas.set_transform(saved);
176        }
177
178        if push_options {
179            canvas.pop_render_options();
180        }
181        canvas.pop_clip();
182    }
183}
184
185impl ServerCompositionSurfaceBrush {
186    /// Paints the surface tiled over `rect` (a border effect that wraps,
187    /// winui-materials.md): tiles of the natural size of the surface times
188    /// the scale of the brush, the first at the top left corner of `rect`,
189    /// clipped to `rect`. At most 4096 tiles are drawn.
190    pub fn paint_tiled(&self, canvas: &mut dyn IDrawingContextImpl, rect: Rect) {
191        let Some(surface) = self.surface() else { return };
192        let Some(natural_size) = surface.natural_size() else { return };
193        let scale = self.props.scale();
194        let (width, height) = (natural_size.width * scale.x.abs(), natural_size.height * scale.y.abs());
195        let opacity = self.brush_props().opacity();
196        if width < 1.0 || height < 1.0 || opacity <= 0.0 || rect.width <= 0.0 || rect.height <= 0.0 {
197            return;
198        }
199        let (columns, rows) = ((rect.width / width).ceil(), (rect.height / height).ceil());
200        if columns * rows > 4096.0 {
201            return;
202        }
203        canvas.push_clip(rect);
204        for row in 0..rows as usize {
205            for column in 0..columns as usize {
206                let tile = Rect::new(rect.x + column as f64 * width, rect.y + row as f64 * height, width, height);
207                surface.draw(canvas, tile, opacity);
208            }
209        }
210        canvas.pop_clip();
211    }
212}
213
214/// Where a surface brush puts the whole content of a surface of
215/// `natural_size` in the area `area`: the content is sized by `stretch`
216/// (`None` keeps the natural size, `Fill` takes the size of the area,
217/// `Uniform` fits it inside, `UniformToFill` covers the area), then placed so
218/// that `horizontal_ratio` / `vertical_ratio` of the free space (negative
219/// when the content is larger) is left of / above it: 0 aligns the left or
220/// top edges, 0.5 centres, 1 aligns the right or bottom edges. `None` for an
221/// empty surface or area.
222pub fn surface_brush_content_rect(
223    natural_size: Size,
224    area: Rect,
225    stretch: CompositionStretch,
226    horizontal_ratio: f64,
227    vertical_ratio: f64,
228) -> Option<Rect> {
229    if natural_size.width <= 0.0 || natural_size.height <= 0.0 || area.width <= 0.0 || area.height <= 0.0 {
230        return None;
231    }
232    let (width, height) = match stretch {
233        CompositionStretch::None => (natural_size.width, natural_size.height),
234        CompositionStretch::Fill => (area.width, area.height),
235        CompositionStretch::Uniform => {
236            let scale = (area.width / natural_size.width).min(area.height / natural_size.height);
237            (natural_size.width * scale, natural_size.height * scale)
238        }
239        CompositionStretch::UniformToFill => {
240            let scale = (area.width / natural_size.width).max(area.height / natural_size.height);
241            (natural_size.width * scale, natural_size.height * scale)
242        }
243    };
244    Some(Rect::new(
245        area.x + (area.width - width) * horizontal_ratio,
246        area.y + (area.height - height) * vertical_ratio,
247        width,
248        height,
249    ))
250}
251
252/// Paints a composition brush over `rect`, in the coordinates of `canvas`:
253/// what a sprite visual (and later a shape) does with its brush. A surface
254/// brush is painted by [`ServerCompositionSurfaceBrush::paint`], a nine-grid,
255/// mask, effect, backdrop, host backdrop or XAML brush by its own `paint`; a brush of
256/// the drawing context (the colour and gradient brushes) fills the
257/// rectangle.
258pub fn paint_composition_brush(canvas: &mut dyn IDrawingContextImpl, brush: &Rc<dyn IServerObject>, rect: Rect) {
259    if let Some(surface_brush) = brush.as_any().downcast_ref::<ServerCompositionSurfaceBrush>() {
260        surface_brush.paint(canvas, rect);
261    } else if let Some(nine_grid) = brush.as_any().downcast_ref::<super::ServerCompositionNineGridBrush>() {
262        nine_grid.paint(canvas, rect);
263    } else if let Some(mask) = brush.as_any().downcast_ref::<super::ServerCompositionMaskBrush>() {
264        mask.paint(canvas, rect);
265    } else if let Some(effect) = brush.as_any().downcast_ref::<super::ServerCompositionEffectBrush>() {
266        effect.paint(canvas, rect);
267    } else if let Some(backdrop) = brush.as_any().downcast_ref::<super::ServerCompositionBackdropBrush>() {
268        backdrop.paint(canvas, rect);
269    } else if let Some(host_backdrop) = brush.as_any().downcast_ref::<super::ServerCompositionHostBackdropBrush>() {
270        host_backdrop.paint(canvas, rect);
271    } else if let Some(xaml) = brush.as_any().downcast_ref::<super::ServerXamlCompositionBrush>() {
272        xaml.paint(canvas, rect);
273    } else if let Some(platform_brush) = brush.clone().as_brush() {
274        canvas.draw_rectangle(Some(&*platform_brush), None, RoundedRect::from_rect(rect), &BoxShadows::default());
275    }
276}
277
278impl_server_render_resource!(
279    ServerCompositionSurfaceBrush,
280    base,
281    composition_property: ServerCompositionSurfaceBrushProps::get_composition_property
282);
283
284impl ServerCompositionBrushHooks for ServerCompositionSurfaceBrush {}
285
286impl ServerCompositionSurfaceBrushHooks for ServerCompositionSurfaceBrush {
287    fn on_surface_changing(&self) {
288        self.unsubscribe();
289    }
290
291    fn on_surface_changed(&self) {
292        self.subscribe();
293    }
294}
295
296impl IServerObject for ServerCompositionSurfaceBrush {
297    fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
298        self.props.deserialize_changes_core(self, reader, committed_at);
299    }
300
301    fn values_invalidated(&self) {
302        self.base.core().invalidated(self);
303    }
304
305    fn dispose(&self) {
306        self.unsubscribe();
307        self.base.core().dispose();
308    }
309
310    fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
311        Some(self)
312    }
313
314    fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
315        Some(self)
316    }
317
318    fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
319        self.props.find_props(type_id)
320    }
321
322    fn as_any(&self) -> &dyn Any {
323        self
324    }
325
326    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
327        self
328    }
329}