Skip to main content

ferroui_base/media/
immediate_drawing_context.rs

1use super::platform_drawing_context::ImplRef;
2use crate::media::imaging::Bitmap;
3use crate::media::immutable::ImmutablePen;
4use crate::media::{BoxShadows, IBrush, IImmutableBrush, IImmutableGlyphRunReference, IPen};
5use crate::platform::{IBitmapImpl, IDrawingContextImpl};
6use crate::utilities::MathUtilities;
7use crate::{Matrix, Point, Rect, RoundedRect};
8use std::any::{Any, TypeId};
9use std::cell::RefCell;
10use std::rc::Rc;
11
12#[derive(Clone, Copy)]
13struct TransformContainer {
14    local_transform: Matrix,
15    container_transform: Matrix,
16}
17
18#[derive(Clone, Copy)]
19enum PushedStateType {
20    Matrix(Matrix),
21    Opacity,
22    Clip,
23    MatrixContainer,
24    #[allow(dead_code)]
25    GeometryClip,
26    OpacityMask,
27}
28
29/// A token for a state pushed on an [`ImmediateDrawingContext`]. Pop it with
30/// [`ImmediateDrawingContext::pop`], in the reverse order of the pushes.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32#[must_use = "a pushed state must be popped with `ImmediateDrawingContext::pop`"]
33pub struct ImmediatePushedState {
34    level: usize,
35}
36
37impl ImmediatePushedState {
38    /// The state that represents "nothing was pushed"; popping it does
39    /// nothing.
40    pub const NONE: ImmediatePushedState = ImmediatePushedState { level: 0 };
41}
42
43crate::ferro_thread_local! {
44    static STATE_STACK_POOL: RefCell<Vec<Vec<PushedStateType>>> = const { RefCell::new(Vec::new()) };
45    static TRANSFORM_STACK_POOL: RefCell<Vec<Vec<TransformContainer>>> = const { RefCell::new(Vec::new()) };
46}
47
48const STACK_POOL_LIMIT: usize = 16;
49
50/// A drawing context that draws immediately to a platform drawing context
51/// implementation, using immutable resources only. This is what custom draw
52/// operations render with.
53pub struct ImmediateDrawingContext<'a> {
54    platform_impl: ImplRef<'a>,
55    states: Option<Vec<PushedStateType>>,
56    transform_containers: Option<Vec<TransformContainer>>,
57    current_transform: Matrix,
58    current_container_transform: Matrix,
59}
60
61impl<'a> ImmediateDrawingContext<'a> {
62    /// Creates a context that owns `platform_impl` and disposes it with the
63    /// context.
64    pub fn new(platform_impl: Box<dyn IDrawingContextImpl + 'a>) -> Self {
65        let transform = platform_impl.transform();
66        Self::create(ImplRef::Owned(platform_impl), transform)
67    }
68
69    /// Creates a context over a borrowed implementation.
70    pub fn borrowed(platform_impl: &'a mut dyn IDrawingContextImpl) -> Self {
71        let transform = platform_impl.transform();
72        Self::create(ImplRef::Borrowed(platform_impl), transform)
73    }
74
75    /// Creates a context over a borrowed implementation with an explicit
76    /// container transform.
77    pub fn borrowed_with_transform(platform_impl: &'a mut dyn IDrawingContextImpl, transform: Matrix) -> Self {
78        Self::create(ImplRef::Borrowed(platform_impl), transform)
79    }
80
81    fn create(platform_impl: ImplRef<'a>, transform: Matrix) -> Self {
82        Self {
83            platform_impl,
84            states: Some(STATE_STACK_POOL.with(|pool| pool.borrow_mut().pop()).unwrap_or_default()),
85            transform_containers: Some(TRANSFORM_STACK_POOL.with(|pool| pool.borrow_mut().pop()).unwrap_or_default()),
86            current_transform: Matrix::IDENTITY,
87            current_container_transform: transform,
88        }
89    }
90
91    /// The platform drawing context implementation.
92    pub fn platform_impl(&mut self) -> &mut (dyn IDrawingContextImpl + 'a) {
93        self.platform_impl.get()
94    }
95
96    /// The current transform of the drawing context.
97    pub fn current_transform(&self) -> Matrix {
98        self.current_transform
99    }
100
101    fn set_current_transform(&mut self, value: Matrix) {
102        self.current_transform = value;
103        let transform = self.current_transform * self.current_container_transform;
104        self.platform_impl.get().set_transform(transform);
105    }
106
107    /// Draws a bitmap into `rect`.
108    pub fn draw_bitmap(&mut self, source: &Bitmap, rect: Rect) {
109        self.draw_bitmap_rects(source, Rect::from_size(source.size()), rect);
110    }
111
112    /// Draws the part of a bitmap inside `source_rect` into `dest_rect`.
113    pub fn draw_bitmap_rects(&mut self, source: &Bitmap, source_rect: Rect, dest_rect: Rect) {
114        self.platform_impl.get().draw_bitmap(&*source.platform_impl().item(), 1.0, source_rect, dest_rect);
115    }
116
117    /// Draws a platform bitmap: [`draw_bitmap_rects`](Self::draw_bitmap_rects)
118    /// for callers that hold the platform implementation only.
119    pub fn draw_bitmap_impl(&mut self, source: &dyn IBitmapImpl, source_rect: Rect, dest_rect: Rect) {
120        self.platform_impl.get().draw_bitmap(source, 1.0, source_rect, dest_rect);
121    }
122
123    /// Draws a line.
124    pub fn draw_line(&mut self, pen: &ImmutablePen, p1: Point, p2: Point) {
125        if pen_is_visible(Some(pen)) {
126            self.platform_impl.get().draw_line(Some(pen), p1, p2);
127        }
128    }
129
130    /// Draws a rectangle with the specified brush and pen.
131    pub fn draw_rectangle(
132        &mut self,
133        brush: Option<&dyn IImmutableBrush>,
134        pen: Option<&ImmutablePen>,
135        rect: Rect,
136        mut radius_x: f64,
137        mut radius_y: f64,
138        box_shadows: &BoxShadows,
139    ) {
140        if brush.is_none() && !pen_is_visible(pen) {
141            return;
142        }
143        if !MathUtilities::is_zero(radius_x) {
144            radius_x = radius_x.min(rect.width / 2.0);
145        }
146        if !MathUtilities::is_zero(radius_y) {
147            radius_y = radius_y.min(rect.height / 2.0);
148        }
149        self.platform_impl.get().draw_rectangle(
150            brush.map(|b| b as &dyn IBrush),
151            pen.map(|p| p as &dyn IPen),
152            RoundedRect::from_radius_xy(rect, radius_x, radius_y),
153            box_shadows,
154        );
155    }
156
157    /// Draws the outline of a rectangle.
158    pub fn draw_rectangle_outline(&mut self, pen: &ImmutablePen, rect: Rect, corner_radius: f32) {
159        let radius = f64::from(corner_radius);
160        self.draw_rectangle(None, Some(pen), rect, radius, radius, &BoxShadows::default());
161    }
162
163    /// Draws an ellipse with the specified brush and pen.
164    pub fn draw_ellipse(
165        &mut self,
166        brush: Option<&dyn IImmutableBrush>,
167        pen: Option<&ImmutablePen>,
168        center: Point,
169        radius_x: f64,
170        radius_y: f64,
171    ) {
172        if brush.is_none() && !pen_is_visible(pen) {
173            return;
174        }
175        let origin_x = center.x - radius_x;
176        let origin_y = center.y - radius_y;
177        let width = radius_x * 2.0;
178        let height = radius_y * 2.0;
179        self.platform_impl.get().draw_ellipse(
180            brush.map(|b| b as &dyn IBrush),
181            pen.map(|p| p as &dyn IPen),
182            Rect::new(origin_x, origin_y, width, height),
183        );
184    }
185
186    /// Draws a glyph run, if the reference still holds its platform glyph
187    /// run.
188    pub fn draw_glyph_run(&mut self, foreground: &dyn IImmutableBrush, glyph_run: &dyn IImmutableGlyphRunReference) {
189        if let Some(glyph_run) = glyph_run.glyph_run() {
190            self.platform_impl.get().draw_glyph_run(Some(foreground as &dyn IBrush), &*glyph_run);
191        }
192    }
193
194    /// Draws a filled rectangle.
195    pub fn fill_rectangle(&mut self, brush: &dyn IImmutableBrush, rect: Rect, corner_radius: f32) {
196        let radius = f64::from(corner_radius);
197        self.draw_rectangle(Some(brush), None, rect, radius, radius, &BoxShadows::default());
198    }
199
200    fn pushed(&mut self, state: PushedStateType) -> ImmediatePushedState {
201        let Some(states) = self.states.as_mut() else {
202            panic!("the drawing context has been disposed");
203        };
204        states.push(state);
205        ImmediatePushedState { level: states.len() }
206    }
207
208    /// Pops a pushed state (C# `PushedState.Dispose`). Panics if the state
209    /// is not the most recently pushed one.
210    pub fn pop(&mut self, state: ImmediatePushedState) {
211        if state.level == 0 {
212            return;
213        }
214        let (Some(states), Some(_)) = (self.states.as_mut(), self.transform_containers.as_ref()) else {
215            panic!("the drawing context has been disposed");
216        };
217        if states.len() != state.level {
218            panic!("Wrong Push/Pop state order");
219        }
220        let restored = states.pop().expect("level is non-zero");
221        self.restore(restored);
222    }
223
224    fn restore(&mut self, state: PushedStateType) {
225        match state {
226            PushedStateType::Matrix(matrix) => self.set_current_transform(matrix),
227            PushedStateType::Clip => self.platform_impl.get().pop_clip(),
228            PushedStateType::Opacity => self.platform_impl.get().pop_opacity(),
229            PushedStateType::GeometryClip => self.platform_impl.get().pop_geometry_clip(),
230            PushedStateType::OpacityMask => self.platform_impl.get().pop_opacity_mask(),
231            PushedStateType::MatrixContainer => {
232                let container = self
233                    .transform_containers
234                    .as_mut()
235                    .and_then(Vec::pop)
236                    .expect("a transform container was pushed");
237                self.current_container_transform = container.container_transform;
238                self.set_current_transform(container.local_transform);
239            }
240        }
241    }
242
243    /// Pushes a rounded clip rectangle.
244    pub fn push_rounded_clip(&mut self, clip: RoundedRect) -> ImmediatePushedState {
245        self.platform_impl.get().push_clip_rounded(clip);
246        self.pushed(PushedStateType::Clip)
247    }
248
249    /// Pushes a clip rectangle.
250    pub fn push_clip(&mut self, clip: Rect) -> ImmediatePushedState {
251        self.platform_impl.get().push_clip(clip);
252        self.pushed(PushedStateType::Clip)
253    }
254
255    /// Pushes an opacity value.
256    pub fn push_opacity(&mut self, opacity: f64, bounds: Rect) -> ImmediatePushedState {
257        self.platform_impl.get().push_opacity(opacity, Some(bounds));
258        self.pushed(PushedStateType::Opacity)
259    }
260
261    /// Pushes an opacity mask.
262    pub fn push_opacity_mask(&mut self, mask: &dyn IImmutableBrush, bounds: Rect) -> ImmediatePushedState {
263        self.platform_impl.get().push_opacity_mask(mask as &dyn IBrush, bounds);
264        self.pushed(PushedStateType::OpacityMask)
265    }
266
267    /// Pushes a matrix post-transformation.
268    pub fn push_post_transform(&mut self, matrix: Matrix) -> ImmediatePushedState {
269        self.push_set_transform(self.current_transform * matrix)
270    }
271
272    /// Pushes a matrix pre-transformation.
273    pub fn push_pre_transform(&mut self, matrix: Matrix) -> ImmediatePushedState {
274        self.push_set_transform(matrix * self.current_transform)
275    }
276
277    /// Sets the current matrix transformation.
278    pub fn push_set_transform(&mut self, matrix: Matrix) -> ImmediatePushedState {
279        let old_matrix = self.current_transform;
280        self.set_current_transform(matrix);
281        self.pushed(PushedStateType::Matrix(old_matrix))
282    }
283
284    /// Pushes a new transform context.
285    pub fn push_transform_container(&mut self) -> ImmediatePushedState {
286        let Some(containers) = self.transform_containers.as_mut() else {
287            panic!("the drawing context has been disposed");
288        };
289        containers.push(TransformContainer {
290            local_transform: self.current_transform,
291            container_transform: self.current_container_transform,
292        });
293        self.current_container_transform = self.current_transform * self.current_container_transform;
294        self.current_transform = Matrix::IDENTITY;
295        self.pushed(PushedStateType::MatrixContainer)
296    }
297
298    /// Pops every state still pushed and releases the context (and the
299    /// platform implementation, if the context owns it).
300    pub fn dispose(mut self) {
301        self.dispose_in_place();
302    }
303
304    fn dispose_in_place(&mut self) {
305        let (Some(mut states), Some(containers)) = (self.states.take(), self.transform_containers.take()) else {
306            return;
307        };
308        // Restoring needs the container stack in place.
309        self.transform_containers = Some(containers);
310        while let Some(state) = states.pop() {
311            self.restore(state);
312        }
313        let containers = self.transform_containers.take().expect("just set");
314        STATE_STACK_POOL.with(|pool| {
315            let mut pool = pool.borrow_mut();
316            if pool.len() < STACK_POOL_LIMIT {
317                pool.push(states);
318            }
319        });
320        if !containers.is_empty() {
321            panic!("Transform container stack is non-empty");
322        }
323        TRANSFORM_STACK_POOL.with(|pool| {
324            let mut pool = pool.borrow_mut();
325            if pool.len() < STACK_POOL_LIMIT {
326                pool.push(containers);
327            }
328        });
329        if self.platform_impl.is_owned() {
330            self.platform_impl.get().dispose();
331        }
332    }
333
334    /// Queries the platform drawing context implementation for an optional
335    /// feature.
336    pub fn try_get_feature(&mut self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
337        self.platform_impl.get().get_feature(feature_type)
338    }
339}
340
341impl Drop for ImmediateDrawingContext<'_> {
342    fn drop(&mut self) {
343        if !std::thread::panicking() {
344            self.dispose_in_place();
345        }
346    }
347}
348
349fn pen_is_visible(pen: Option<&ImmutablePen>) -> bool {
350    pen.is_some_and(|pen| pen.brush().is_some() && pen.thickness() > 0.0)
351}