Skip to main content

ferroui_base/media/
drawing_context.rs

1//! The drawing context: the surface a visual draws itself onto.
2//!
3//! # Shape
4//!
5//! Upstream this is an abstract class: public drawing members validate
6//! their arguments and forward to protected abstract `*Core` members, which
7//! the platform context and the recording (render data) context implement;
8//! pushed states are RAII tokens kept on a per-context stack.
9//!
10//! Here the abstract part is the [`IDrawingContextCore`] trait and
11//! [`DrawingContext`] is one concrete struct over a `dyn` core:
12//!
13//! * `Visual::render` receives `&mut DrawingContext`, a nameable type that
14//!   does not depend on which core is behind it.
15//! * The core is either borrowed ([`DrawingContext::new`]) or owned
16//!   ([`DrawingContext::owned`]). A borrowed core lets the caller keep the
17//!   concrete core (a recording context is reused for many visuals and
18//!   queried for its result after each one); an owned core is disposed with
19//!   the context, like `using var context = ...` upstream.
20//! * A push returns a [`PushedState`] token, a plain `Copy` value recording
21//!   the stack level. It is popped explicitly with [`DrawingContext::pop`]
22//!   (C# `state.Dispose()`), which checks the order exactly as upstream.
23//!   [`PushedState::NONE`] is C#'s `default(PushedState)`: popping it does
24//!   nothing, which makes conditional pushes straightforward. For lexical
25//!   scoping there is [`DrawingContext::scope`], a guard that derefs to the
26//!   context and pops on drop.
27//! * Text (formatted text, text layouts, lines and runs) draws through
28//!   `ITextDrawingSink`, whose members are the drawing context members text
29//!   needs. The context implements the sink, so text is drawn by passing
30//!   the context itself; the sink's unpaired `pop_transform` pops the state
31//!   pushed last.
32//! * No draw call allocates: brushes and pens are passed as borrowed `Rc`
33//!   handles (the platform core derefs them, the recording core clones the
34//!   handle), and the state stack is taken from a per-thread pool.
35
36use crate::media::immutable::ImmutableSolidColorBrush;
37use crate::media::text_formatting::ITextDrawingSink;
38use crate::media::IEffect;
39use crate::media::{
40    BoxShadows, FormattedText, Geometry, GlyphRun, IBrush, IExperimentalAcrylicMaterial, IImage, IPen, RenderOptions,
41    TextOptions,
42};
43use crate::platform::IGeometryImpl;
44use crate::rendering::scene_graph::ICustomDrawOperation;
45use crate::utilities::MathUtilities;
46use crate::{Matrix, Point, Rect, Ref, RoundedRect};
47use std::cell::RefCell;
48use std::ops::{Deref, DerefMut};
49use std::rc::Rc;
50use std::sync::Arc;
51
52/// The overridable part of a drawing context: the `*Core` members.
53///
54/// Implemented by the platform drawing context and by the recording
55/// context. The arguments have been validated by [`DrawingContext`].
56pub trait IDrawingContextCore {
57    /// Draws a line. The pen is visible.
58    fn draw_line_core(&mut self, pen: &Rc<dyn IPen>, p1: Point, p2: Point);
59
60    /// Draws a geometry object. The default implementation draws its
61    /// platform implementation, if it has one.
62    fn draw_geometry_core(
63        &mut self,
64        brush: Option<&Rc<dyn IBrush>>,
65        pen: Option<&Rc<dyn IPen>>,
66        geometry: &Ref<Geometry>,
67    ) {
68        if let Some(geometry_impl) = geometry.platform_impl() {
69            self.draw_geometry_impl_core(brush, pen, &geometry_impl);
70        }
71    }
72
73    /// Draws a platform geometry.
74    fn draw_geometry_impl_core(
75        &mut self,
76        brush: Option<&Rc<dyn IBrush>>,
77        pen: Option<&Rc<dyn IPen>>,
78        geometry: &Arc<dyn IGeometryImpl>,
79    );
80
81    /// Draws a rectangle with the specified brush and pen.
82    fn draw_rectangle_core(
83        &mut self,
84        brush: Option<&Rc<dyn IBrush>>,
85        pen: Option<&Rc<dyn IPen>>,
86        rrect: RoundedRect,
87        box_shadows: &BoxShadows,
88    );
89
90    /// Draws an ellipse with the specified brush and pen.
91    fn draw_ellipse_core(&mut self, brush: Option<&Rc<dyn IBrush>>, pen: Option<&Rc<dyn IPen>>, rect: Rect);
92
93    /// Draws a platform bitmap, given by a counted reference to it (upstream's `IRef<IBitmapImpl>`): a context
94    /// that keeps the bitmap beyond the call takes a counted reference of its own.
95    fn draw_bitmap(&mut self, source: &crate::utilities::RefCounted<crate::platform::SharedBitmapImpl>, opacity: f64, source_rect: Rect, dest_rect: Rect);
96
97    /// Draws a custom drawing operation.
98    fn custom(&mut self, custom: &std::sync::Arc<dyn ICustomDrawOperation>);
99
100    /// Draws a glyph run.
101    fn draw_glyph_run(&mut self, foreground: Option<&Rc<dyn IBrush>>, glyph_run: &Rc<GlyphRun>);
102
103    /// Draws a rectangle filled with an acrylic-like material.
104    ///
105    /// Upstream only the platform drawing context has this member and
106    /// callers cast the context to reach it. Here it is a core member so
107    /// that it is reachable through [`DrawingContext`]: the platform core
108    /// uses the backend's acrylic support, every other core gets the
109    /// rectangle filled with the fallback color of the material.
110    fn draw_rectangle_acrylic_core(&mut self, material: &dyn IExperimentalAcrylicMaterial, rect: RoundedRect) {
111        let brush: Rc<dyn IBrush> = Rc::new(ImmutableSolidColorBrush::new(material.fallback_color()));
112        self.draw_rectangle_core(Some(&brush), None, rect, &BoxShadows::default());
113    }
114
115    fn push_clip_core(&mut self, rect: Rect);
116    fn push_rounded_clip_core(&mut self, rect: RoundedRect);
117    fn push_geometry_clip_core(&mut self, clip: &Ref<Geometry>);
118    fn push_opacity_core(&mut self, opacity: f64);
119    fn push_opacity_mask_core(&mut self, mask: &Rc<dyn IBrush>, bounds: Rect);
120    fn push_transform_core(&mut self, matrix: Matrix);
121    fn push_render_options_core(&mut self, render_options: RenderOptions);
122    fn push_text_options_core(&mut self, text_options: TextOptions);
123    fn push_effect_core(&mut self, effect: &Rc<dyn IEffect>, bounds: Rect);
124
125    fn pop_clip_core(&mut self);
126    fn pop_geometry_clip_core(&mut self);
127    fn pop_opacity_core(&mut self);
128    fn pop_opacity_mask_core(&mut self);
129    fn pop_transform_core(&mut self);
130    fn pop_render_options_core(&mut self);
131    fn pop_text_options_core(&mut self);
132    fn pop_effect_core(&mut self);
133
134    /// Releases the core. Called once, after every pushed state has been
135    /// popped, when an owned context is disposed.
136    fn dispose_core(&mut self);
137}
138
139/// The kind of a pushed state; decides which `pop_*_core` restores it.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141enum PushedStateType {
142    Transform,
143    Opacity,
144    Clip,
145    GeometryClip,
146    OpacityMask,
147    RenderOptions,
148    TextOptions,
149    Effect,
150}
151
152/// A token for a pushed state. Pop it with [`DrawingContext::pop`], in the
153/// reverse order of the pushes.
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155#[must_use = "a pushed state must be popped with `DrawingContext::pop` (or left to the context's disposal)"]
156pub struct PushedState {
157    /// The state stack depth right after the push; zero for [`Self::NONE`].
158    level: usize,
159}
160
161impl PushedState {
162    /// The state that represents "nothing was pushed"; popping it does
163    /// nothing.
164    pub const NONE: PushedState = PushedState { level: 0 };
165}
166
167impl Default for PushedState {
168    fn default() -> Self {
169        Self::NONE
170    }
171}
172
173enum CoreRef<'a> {
174    Borrowed(&'a mut dyn IDrawingContextCore),
175    Owned(Box<dyn IDrawingContextCore + 'a>),
176}
177
178crate::ferro_thread_local! {
179    static STATE_STACK_POOL: RefCell<Vec<Vec<PushedStateType>>> = const { RefCell::new(Vec::new()) };
180}
181
182const STATE_STACK_POOL_LIMIT: usize = 16;
183
184/// The surface a visual draws itself onto during rendering.
185pub struct DrawingContext<'a> {
186    core: CoreRef<'a>,
187    states: Option<Vec<PushedStateType>>,
188    disposed: bool,
189}
190
191impl<'a> DrawingContext<'a> {
192    /// Creates a context over a borrowed core. Dropping (or disposing) the
193    /// context pops the states still pushed but leaves the core alive.
194    pub fn new(core: &'a mut dyn IDrawingContextCore) -> Self {
195        Self { core: CoreRef::Borrowed(core), states: None, disposed: false }
196    }
197
198    /// Creates a context that owns its core: disposing (or dropping) the
199    /// context pops the states still pushed and disposes the core.
200    pub fn owned(core: Box<dyn IDrawingContextCore + 'a>) -> Self {
201        Self { core: CoreRef::Owned(core), states: None, disposed: false }
202    }
203
204    #[inline]
205    fn core(&mut self) -> &mut dyn IDrawingContextCore {
206        match &mut self.core {
207            CoreRef::Borrowed(core) => &mut **core,
208            CoreRef::Owned(core) => &mut **core,
209        }
210    }
211
212    /// Pops every state still pushed and releases the context (and the core,
213    /// if the context owns it).
214    pub fn dispose(mut self) {
215        self.dispose_in_place();
216    }
217
218    fn dispose_in_place(&mut self) {
219        if self.disposed {
220            return;
221        }
222        self.disposed = true;
223        if let Some(mut states) = self.states.take() {
224            while let Some(state) = states.pop() {
225                self.restore(state);
226            }
227            STATE_STACK_POOL.with(|pool| {
228                let mut pool = pool.borrow_mut();
229                if pool.len() < STATE_STACK_POOL_LIMIT {
230                    pool.push(states);
231                }
232            });
233        }
234        if let CoreRef::Owned(core) = &mut self.core {
235            core.dispose_core();
236        }
237    }
238
239    /// Draws an image into `rect`.
240    pub fn draw_image(&mut self, source: &dyn IImage, rect: Rect) {
241        self.draw_image_with_rects(source, Rect::from_size(source.size()), rect);
242    }
243
244    /// Draws the part of an image inside `source_rect` into `dest_rect`.
245    pub fn draw_image_with_rects(&mut self, source: &dyn IImage, source_rect: Rect, dest_rect: Rect) {
246        source.draw(self, source_rect, dest_rect);
247    }
248
249    /// Draws a platform bitmap.
250    pub fn draw_bitmap(&mut self, source: &crate::utilities::RefCounted<crate::platform::SharedBitmapImpl>, opacity: f64, source_rect: Rect, dest_rect: Rect) {
251        self.core().draw_bitmap(source, opacity, source_rect, dest_rect);
252    }
253
254    /// Draws a line.
255    pub fn draw_line(&mut self, pen: &Rc<dyn IPen>, p1: Point, p2: Point) {
256        if pen_is_visible(Some(pen)) {
257            self.core().draw_line_core(pen, p1, p2);
258        }
259    }
260
261    /// Draws a geometry.
262    pub fn draw_geometry(
263        &mut self,
264        brush: Option<&Rc<dyn IBrush>>,
265        pen: Option<&Rc<dyn IPen>>,
266        geometry: &Ref<Geometry>,
267    ) {
268        if brush.is_some() || pen_is_visible(pen) {
269            self.core().draw_geometry_core(brush, pen, geometry);
270        }
271    }
272
273    /// Draws a platform geometry.
274    pub fn draw_geometry_impl(
275        &mut self,
276        brush: Option<&Rc<dyn IBrush>>,
277        pen: Option<&Rc<dyn IPen>>,
278        geometry: &Arc<dyn IGeometryImpl>,
279    ) {
280        if brush.is_some() || pen_is_visible(pen) {
281            self.core().draw_geometry_impl_core(brush, pen, geometry);
282        }
283    }
284
285    /// Draws a rectangle with the specified brush and pen.
286    ///
287    /// The brush and the pen can both be `None`. If the brush is `None`, no
288    /// fill is performed. If the pen is `None`, no stroke is performed. If
289    /// both are `None` and there are no box shadows, the call is a no-op.
290    pub fn draw_rectangle(
291        &mut self,
292        brush: Option<&Rc<dyn IBrush>>,
293        pen: Option<&Rc<dyn IPen>>,
294        rect: Rect,
295        mut radius_x: f64,
296        mut radius_y: f64,
297        box_shadows: &BoxShadows,
298    ) {
299        if brush.is_none() && !pen_is_visible(pen) && box_shadows.count() == 0 {
300            return;
301        }
302        if !MathUtilities::is_zero(radius_x) {
303            radius_x = radius_x.min(rect.width / 2.0);
304        }
305        if !MathUtilities::is_zero(radius_y) {
306            radius_y = radius_y.min(rect.height / 2.0);
307        }
308        self.core().draw_rectangle_core(brush, pen, RoundedRect::from_radius_xy(rect, radius_x, radius_y), box_shadows);
309    }
310
311    /// Draws a rounded rectangle with the specified brush and pen.
312    pub fn draw_rounded_rectangle(
313        &mut self,
314        brush: Option<&Rc<dyn IBrush>>,
315        pen: Option<&Rc<dyn IPen>>,
316        rrect: RoundedRect,
317        box_shadows: &BoxShadows,
318    ) {
319        if brush.is_none() && !pen_is_visible(pen) && box_shadows.count() == 0 {
320            return;
321        }
322        self.core().draw_rectangle_core(brush, pen, rrect, box_shadows);
323    }
324
325    /// Draws the outline of a rectangle.
326    pub fn draw_rectangle_outline(&mut self, pen: &Rc<dyn IPen>, rect: Rect, corner_radius: f32) {
327        let radius = f64::from(corner_radius);
328        self.draw_rectangle(None, Some(pen), rect, radius, radius, &BoxShadows::default());
329    }
330
331    /// Draws a filled rectangle.
332    pub fn fill_rectangle(&mut self, brush: &Rc<dyn IBrush>, rect: Rect, corner_radius: f32) {
333        let radius = f64::from(corner_radius);
334        self.draw_rectangle(Some(brush), None, rect, radius, radius, &BoxShadows::default());
335    }
336
337    /// Draws an ellipse given its center and radii.
338    pub fn draw_ellipse_at(
339        &mut self,
340        brush: Option<&Rc<dyn IBrush>>,
341        pen: Option<&Rc<dyn IPen>>,
342        center: Point,
343        radius_x: f64,
344        radius_y: f64,
345    ) {
346        if brush.is_some() || pen_is_visible(pen) {
347            let origin_x = center.x - radius_x;
348            let origin_y = center.y - radius_y;
349            let width = radius_x * 2.0;
350            let height = radius_y * 2.0;
351            self.core().draw_ellipse_core(brush, pen, Rect::new(origin_x, origin_y, width, height));
352        }
353    }
354
355    /// Draws an ellipse inscribed in a rectangle.
356    pub fn draw_ellipse(&mut self, brush: Option<&Rc<dyn IBrush>>, pen: Option<&Rc<dyn IPen>>, rect: Rect) {
357        if brush.is_some() || pen_is_visible(pen) {
358            self.core().draw_ellipse_core(brush, pen, rect);
359        }
360    }
361
362    /// Draws a custom drawing operation.
363    pub fn custom(&mut self, custom: &std::sync::Arc<dyn ICustomDrawOperation>) {
364        self.core().custom(custom);
365    }
366
367    /// Draws text. `origin` is the upper-left corner of the text.
368    pub fn draw_text(&mut self, text: &FormattedText, origin: Point) {
369        text.draw(self, origin);
370    }
371
372    /// Draws a glyph run.
373    pub fn draw_glyph_run(&mut self, foreground: Option<&Rc<dyn IBrush>>, glyph_run: &Rc<GlyphRun>) {
374        self.core().draw_glyph_run(foreground, glyph_run);
375    }
376
377    /// Draws a rectangle filled with an acrylic-like material. A core
378    /// without acrylic support fills the rectangle with the fallback color
379    /// of the material; see
380    /// [`IDrawingContextCore::draw_rectangle_acrylic_core`].
381    pub fn draw_rectangle_acrylic(&mut self, material: &dyn IExperimentalAcrylicMaterial, rect: RoundedRect) {
382        self.core().draw_rectangle_acrylic_core(material, rect);
383    }
384
385    // --- pushed states ----------------------------------------------------
386
387    fn pushed(&mut self, state: PushedStateType) -> PushedState {
388        let states = self
389            .states
390            .get_or_insert_with(|| STATE_STACK_POOL.with(|pool| pool.borrow_mut().pop()).unwrap_or_default());
391        states.push(state);
392        PushedState { level: states.len() }
393    }
394
395    fn restore(&mut self, state: PushedStateType) {
396        let core = self.core();
397        match state {
398            PushedStateType::Transform => core.pop_transform_core(),
399            PushedStateType::Clip => core.pop_clip_core(),
400            PushedStateType::Opacity => core.pop_opacity_core(),
401            PushedStateType::GeometryClip => core.pop_geometry_clip_core(),
402            PushedStateType::OpacityMask => core.pop_opacity_mask_core(),
403            PushedStateType::RenderOptions => core.pop_render_options_core(),
404            PushedStateType::TextOptions => core.pop_text_options_core(),
405            PushedStateType::Effect => core.pop_effect_core(),
406        }
407    }
408
409    /// Pops a pushed state (C# `PushedState.Dispose`).
410    ///
411    /// Panics if the state is not the most recently pushed one. Popping
412    /// [`PushedState::NONE`], or any state after the context has been
413    /// disposed, does nothing.
414    pub fn pop(&mut self, state: PushedState) {
415        if state.level == 0 {
416            return;
417        }
418        let Some(states) = self.states.as_mut() else { return };
419        if states.len() != state.level {
420            panic!("Wrong Push/Pop state order");
421        }
422        let restored = states.pop().expect("level is non-zero");
423        self.restore(restored);
424    }
425
426    /// Turns a pushed state into a guard that derefs to the context and pops
427    /// the state when dropped.
428    pub fn scope(&mut self, state: PushedState) -> PushedScope<'_, 'a> {
429        PushedScope { context: self, state }
430    }
431
432    /// Pushes a rounded clip rectangle.
433    pub fn push_rounded_clip(&mut self, clip: RoundedRect) -> PushedState {
434        self.core().push_rounded_clip_core(clip);
435        self.pushed(PushedStateType::Clip)
436    }
437
438    /// Pushes a clip rectangle.
439    pub fn push_clip(&mut self, clip: Rect) -> PushedState {
440        self.core().push_clip_core(clip);
441        self.pushed(PushedStateType::Clip)
442    }
443
444    /// Pushes a clip geometry.
445    pub fn push_geometry_clip(&mut self, clip: &Ref<Geometry>) -> PushedState {
446        self.core().push_geometry_clip_core(clip);
447        self.pushed(PushedStateType::GeometryClip)
448    }
449
450    /// Pushes an opacity value.
451    pub fn push_opacity(&mut self, opacity: f64) -> PushedState {
452        self.core().push_opacity_core(opacity);
453        self.pushed(PushedStateType::Opacity)
454    }
455
456    /// Pushes an opacity mask.
457    ///
458    /// `bounds` are the bounds of the control, used to place brushes
459    /// correctly.
460    pub fn push_opacity_mask(&mut self, mask: &Rc<dyn IBrush>, bounds: Rect) -> PushedState {
461        self.core().push_opacity_mask_core(mask, bounds);
462        self.pushed(PushedStateType::OpacityMask)
463    }
464
465    /// Pushes a matrix transformation.
466    pub fn push_transform(&mut self, matrix: Matrix) -> PushedState {
467        self.core().push_transform_core(matrix);
468        self.pushed(PushedStateType::Transform)
469    }
470
471    /// Pushes render options.
472    pub fn push_render_options(&mut self, render_options: RenderOptions) -> PushedState {
473        self.core().push_render_options_core(render_options);
474        self.pushed(PushedStateType::RenderOptions)
475    }
476
477    /// Pushes text options.
478    pub fn push_text_options(&mut self, text_options: TextOptions) -> PushedState {
479        self.core().push_text_options_core(text_options);
480        self.pushed(PushedStateType::TextOptions)
481    }
482
483    /// Pushes an effect applied to everything drawn until the state is
484    /// popped. `bounds` are the bounds of the affected content.
485    pub fn push_effect(&mut self, effect: &Rc<dyn IEffect>, bounds: Rect) -> PushedState {
486        self.core().push_effect_core(effect, bounds);
487        self.pushed(PushedStateType::Effect)
488    }
489}
490
491/// Text draws itself through the sink members, which are the corresponding
492/// drawing context members. A sink push has no state token: its pop restores
493/// the state pushed last, which is the transform the matching push pushed
494/// because text balances its pushes.
495impl ITextDrawingSink for DrawingContext<'_> {
496    fn draw_glyph_run(&mut self, foreground: Option<&Rc<dyn IBrush>>, glyph_run: &Rc<GlyphRun>) {
497        DrawingContext::draw_glyph_run(self, foreground, glyph_run);
498    }
499
500    fn draw_rectangle(&mut self, brush: Option<&Rc<dyn IBrush>>, pen: Option<&Rc<dyn IPen>>, rect: Rect) {
501        DrawingContext::draw_rectangle(self, brush, pen, rect, 0.0, 0.0, &BoxShadows::default());
502    }
503
504    fn draw_line(&mut self, pen: &Rc<dyn IPen>, p1: Point, p2: Point) {
505        DrawingContext::draw_line(self, pen, p1, p2);
506    }
507
508    fn push_transform(&mut self, matrix: Matrix) {
509        let _ = DrawingContext::push_transform(self, matrix);
510    }
511
512    fn pop_transform(&mut self) {
513        let Some(states) = self.states.as_mut() else { return };
514        match states.pop() {
515            Some(PushedStateType::Transform) => self.restore(PushedStateType::Transform),
516            Some(_) => panic!("Wrong Push/Pop state order"),
517            None => {}
518        }
519    }
520}
521
522impl Drop for DrawingContext<'_> {
523    fn drop(&mut self) {
524        // The equivalent of leaving a `using` block. When unwinding, the
525        // core is left alone: popping could panic again.
526        if !std::thread::panicking() {
527            self.dispose_in_place();
528        }
529    }
530}
531
532/// A pushed state that is popped when the guard goes out of scope. Derefs
533/// to the drawing context.
534pub struct PushedScope<'c, 'a> {
535    context: &'c mut DrawingContext<'a>,
536    state: PushedState,
537}
538
539impl<'a> Deref for PushedScope<'_, 'a> {
540    type Target = DrawingContext<'a>;
541
542    fn deref(&self) -> &Self::Target {
543        self.context
544    }
545}
546
547impl DerefMut for PushedScope<'_, '_> {
548    fn deref_mut(&mut self) -> &mut Self::Target {
549        self.context
550    }
551}
552
553impl Drop for PushedScope<'_, '_> {
554    fn drop(&mut self) {
555        if !std::thread::panicking() {
556            self.context.pop(self.state);
557        }
558    }
559}
560
561/// Whether a pen draws anything: it has a brush and a positive thickness.
562pub(crate) fn pen_is_visible(pen: Option<&Rc<dyn IPen>>) -> bool {
563    pen.is_some_and(|pen| pen.brush().is_some() && pen.thickness() > 0.0)
564}