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