Skip to main content

fui/
drawing.rs

1#![allow(clippy::too_many_arguments)]
2
3use crate::ffi;
4use crate::image_sampling::ImageSampling;
5use crate::logger::error;
6use crate::node::Node;
7use crate::text::{DynamicTextLayout, TextLayout};
8use std::cell::RefCell;
9use std::rc::Rc;
10
11const OP_SAVE: u32 = 1;
12const OP_RESTORE: u32 = 2;
13const OP_TRANSLATE: u32 = 3;
14const OP_SCALE: u32 = 4;
15const OP_ROTATE: u32 = 5;
16const OP_CLIP_RECT: u32 = 6;
17const OP_CLIP_ROUND_RECT: u32 = 7;
18const OP_DRAW_RECT: u32 = 10;
19const OP_DRAW_CIRCLE: u32 = 11;
20const OP_DRAW_LINE: u32 = 12;
21const OP_DRAW_ROUND_RECT: u32 = 13;
22const OP_DRAW_PATH: u32 = 20;
23const OP_DRAW_TEXT_NODE: u32 = 30;
24const OP_DRAW_IMAGE: u32 = 31;
25const OP_DRAW_SVG: u32 = 32;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
28/// Fill and stroke state for an immediate drawing command.
29pub struct Paint {
30    pub fill_color: u32,
31    pub stroke_color: u32,
32    pub stroke_width: f32,
33}
34
35impl Paint {
36    /// Creates a fill-only paint using packed RGBA color.
37    pub fn fill(color: u32) -> Self {
38        Self {
39            fill_color: color,
40            stroke_color: 0,
41            stroke_width: 0.0,
42        }
43    }
44
45    /// Creates a stroke-only paint using packed RGBA color.
46    pub fn stroke(color: u32, width: f32) -> Self {
47        Self {
48            fill_color: 0,
49            stroke_color: color,
50            stroke_width: width,
51        }
52    }
53
54    /// Creates a paint with both fill and stroke.
55    pub fn filled_stroke(fill_color: u32, stroke_color: u32, stroke_width: f32) -> Self {
56        Self {
57            fill_color,
58            stroke_color,
59            stroke_width,
60        }
61    }
62
63    /// Reports whether the fill has non-zero alpha.
64    pub fn has_fill(self) -> bool {
65        (self.fill_color & 0xff) != 0
66    }
67
68    /// Reports whether the stroke has positive width and non-zero alpha.
69    pub fn has_stroke(self) -> bool {
70        self.stroke_width > 0.0 && (self.stroke_color & 0xff) != 0
71    }
72}
73
74struct PathResource {
75    id: u32,
76}
77
78impl Drop for PathResource {
79    fn drop(&mut self) {
80        unsafe { ffi::fui_path_destroy(self.id) };
81    }
82}
83
84#[derive(Clone)]
85/// A shared retained vector path released when its final clone drops.
86pub struct Path {
87    resource: Rc<PathResource>,
88}
89
90impl Default for Path {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl Path {
97    /// Creates an empty host-neutral path.
98    pub fn new() -> Self {
99        let id = unsafe { ffi::fui_path_create() };
100        Self {
101            resource: Rc::new(PathResource { id }),
102        }
103    }
104
105    /// Returns the opaque engine path identifier.
106    pub fn id(&self) -> u32 {
107        self.resource.id
108    }
109
110    /// Starts a new contour at `(x, y)`.
111    pub fn move_to(&mut self, x: f32, y: f32) -> &mut Self {
112        unsafe { ffi::fui_path_move_to(self.id(), x, y) };
113        self
114    }
115
116    /// Adds a line segment to `(x, y)`.
117    pub fn line_to(&mut self, x: f32, y: f32) -> &mut Self {
118        unsafe { ffi::fui_path_line_to(self.id(), x, y) };
119        self
120    }
121
122    /// Adds a quadratic Bezier segment.
123    pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) -> &mut Self {
124        unsafe { ffi::fui_path_quad_to(self.id(), cx, cy, x, y) };
125        self
126    }
127
128    /// Adds a cubic Bezier segment.
129    pub fn cubic_to(
130        &mut self,
131        cx1: f32,
132        cy1: f32,
133        cx2: f32,
134        cy2: f32,
135        x: f32,
136        y: f32,
137    ) -> &mut Self {
138        unsafe { ffi::fui_path_cubic_to(self.id(), cx1, cy1, cx2, cy2, x, y) };
139        self
140    }
141
142    /// Closes the current contour.
143    pub fn close(&mut self) -> &mut Self {
144        unsafe { ffi::fui_path_close(self.id()) };
145        self
146    }
147
148    /// Adds an axis-aligned rectangle.
149    pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Self {
150        unsafe { ffi::fui_path_add_rect(self.id(), x, y, w, h) };
151        self
152    }
153
154    /// Adds a circle.
155    pub fn add_circle(&mut self, cx: f32, cy: f32, r: f32) -> &mut Self {
156        unsafe { ffi::fui_path_add_circle(self.id(), cx, cy, r) };
157        self
158    }
159}
160
161#[derive(Default)]
162struct DrawContextState {
163    canvas_ptr: usize,
164    words: Vec<u32>,
165    retained_paths: Vec<Path>,
166}
167
168#[derive(Clone, Default)]
169/// A batched immediate drawing command recorder.
170///
171/// Application code normally receives this from `CustomDrawable` or
172/// [`Bitmap::canvas`](crate::bitmap::Bitmap::canvas), rather than constructing
173/// it directly.
174pub struct DrawContext {
175    inner: Rc<RefCell<DrawContextState>>,
176}
177
178impl DrawContext {
179    #[doc(hidden)]
180    pub fn new(canvas_ptr: usize) -> Self {
181        Self {
182            inner: Rc::new(RefCell::new(DrawContextState {
183                canvas_ptr,
184                words: Vec::new(),
185                retained_paths: Vec::new(),
186            })),
187        }
188    }
189
190    fn push_float(words: &mut Vec<u32>, value: f32) {
191        words.push(value.to_bits());
192    }
193
194    /// Sends the current ordered command batch to the active host canvas.
195    /// Normal `CustomDrawable` callbacks are flushed automatically.
196    pub fn flush(&self) {
197        let mut state = self.inner.borrow_mut();
198        if state.words.is_empty() {
199            return;
200        }
201        unsafe {
202            ffi::fui_canvas_draw_batch(
203                state.canvas_ptr,
204                state.words.as_ptr() as usize,
205                state.words.len() as u32,
206            )
207        };
208        state.words.clear();
209        state.retained_paths.clear();
210    }
211
212    /// Saves transform and clip state.
213    pub fn save(&self) {
214        self.inner.borrow_mut().words.push(OP_SAVE);
215    }
216
217    /// Restores the most recently saved transform and clip state.
218    pub fn restore(&self) {
219        self.inner.borrow_mut().words.push(OP_RESTORE);
220    }
221
222    /// Translates subsequent commands.
223    pub fn translate(&self, x: f32, y: f32) {
224        let mut state = self.inner.borrow_mut();
225        state.words.push(OP_TRANSLATE);
226        Self::push_float(&mut state.words, x);
227        Self::push_float(&mut state.words, y);
228    }
229
230    /// Scales subsequent commands.
231    pub fn scale(&self, sx: f32, sy: f32) {
232        let mut state = self.inner.borrow_mut();
233        state.words.push(OP_SCALE);
234        Self::push_float(&mut state.words, sx);
235        Self::push_float(&mut state.words, sy);
236    }
237
238    /// Rotates subsequent commands in degrees.
239    pub fn rotate(&self, degrees: f32) {
240        let mut state = self.inner.borrow_mut();
241        state.words.push(OP_ROTATE);
242        Self::push_float(&mut state.words, degrees);
243    }
244
245    /// Intersects the current clip with an axis-aligned rectangle.
246    pub fn clip_rect(&self, x: f32, y: f32, w: f32, h: f32) {
247        let mut state = self.inner.borrow_mut();
248        state.words.push(OP_CLIP_RECT);
249        Self::push_float(&mut state.words, x);
250        Self::push_float(&mut state.words, y);
251        Self::push_float(&mut state.words, w);
252        Self::push_float(&mut state.words, h);
253    }
254
255    /// Intersects the current clip with a per-corner rounded rectangle.
256    pub fn clip_round_rect(
257        &self,
258        x: f32,
259        y: f32,
260        w: f32,
261        h: f32,
262        tl: f32,
263        tr: f32,
264        br: f32,
265        bl: f32,
266    ) {
267        let mut state = self.inner.borrow_mut();
268        state.words.push(OP_CLIP_ROUND_RECT);
269        Self::push_float(&mut state.words, x);
270        Self::push_float(&mut state.words, y);
271        Self::push_float(&mut state.words, w);
272        Self::push_float(&mut state.words, h);
273        Self::push_float(&mut state.words, tl);
274        Self::push_float(&mut state.words, tr);
275        Self::push_float(&mut state.words, br);
276        Self::push_float(&mut state.words, bl);
277    }
278
279    /// Draws a rectangle.
280    pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, paint: Paint) {
281        let mut state = self.inner.borrow_mut();
282        state.words.push(OP_DRAW_RECT);
283        Self::push_float(&mut state.words, x);
284        Self::push_float(&mut state.words, y);
285        Self::push_float(&mut state.words, w);
286        Self::push_float(&mut state.words, h);
287        state.words.push(paint.fill_color);
288        state.words.push(paint.stroke_color);
289        Self::push_float(&mut state.words, paint.stroke_width);
290    }
291
292    /// Draws a circle.
293    pub fn draw_circle(&self, cx: f32, cy: f32, radius: f32, paint: Paint) {
294        let mut state = self.inner.borrow_mut();
295        state.words.push(OP_DRAW_CIRCLE);
296        Self::push_float(&mut state.words, cx);
297        Self::push_float(&mut state.words, cy);
298        Self::push_float(&mut state.words, radius);
299        state.words.push(paint.fill_color);
300        state.words.push(paint.stroke_color);
301        Self::push_float(&mut state.words, paint.stroke_width);
302    }
303
304    /// Draws a line segment.
305    pub fn draw_line(&self, x1: f32, y1: f32, x2: f32, y2: f32, color: u32, stroke_width: f32) {
306        let mut state = self.inner.borrow_mut();
307        state.words.push(OP_DRAW_LINE);
308        Self::push_float(&mut state.words, x1);
309        Self::push_float(&mut state.words, y1);
310        Self::push_float(&mut state.words, x2);
311        Self::push_float(&mut state.words, y2);
312        state.words.push(color);
313        Self::push_float(&mut state.words, stroke_width);
314    }
315
316    /// Draws a rounded rectangle.
317    pub fn draw_round_rect(&self, x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32, paint: Paint) {
318        let mut state = self.inner.borrow_mut();
319        state.words.push(OP_DRAW_ROUND_RECT);
320        Self::push_float(&mut state.words, x);
321        Self::push_float(&mut state.words, y);
322        Self::push_float(&mut state.words, w);
323        Self::push_float(&mut state.words, h);
324        Self::push_float(&mut state.words, rx);
325        Self::push_float(&mut state.words, ry);
326        state.words.push(paint.fill_color);
327        state.words.push(paint.stroke_color);
328        Self::push_float(&mut state.words, paint.stroke_width);
329    }
330
331    /// Draws a retained path and keeps it alive through the current batch.
332    pub fn draw_path(&self, path: &Path, paint: Paint) {
333        let mut state = self.inner.borrow_mut();
334        state.words.push(OP_DRAW_PATH);
335        state.words.push(path.id());
336        state.words.push(paint.fill_color);
337        state.words.push(paint.stroke_color);
338        Self::push_float(&mut state.words, paint.stroke_width);
339        state.retained_paths.push(path.clone());
340    }
341
342    /// Draws a built retained text node at logical coordinates.
343    pub fn draw_text_node<T: Node>(&self, node: &T, x: f32, y: f32) {
344        let handle = node.handle().raw();
345        let mut state = self.inner.borrow_mut();
346        state.words.push(OP_DRAW_TEXT_NODE);
347        state.words.push(handle as u32);
348        state.words.push((handle >> 32) as u32);
349        Self::push_float(&mut state.words, x);
350        Self::push_float(&mut state.words, y);
351    }
352
353    /// Draws a ready retained text layout.
354    pub fn draw_text_layout(&self, layout: &TextLayout, x: f32, y: f32) {
355        if !layout.is_ready() {
356            error(
357                "TextLayout",
358                "DrawContext.draw_text_layout() called before the TextLayout was ready; register on_ready and draw after the callback.",
359            );
360            return;
361        }
362        let node = layout.draw_node();
363        self.draw_text_node(&node, x, y);
364    }
365
366    /// Draws a ready frequently-changing fixed-charset text layout.
367    pub fn draw_dynamic_text_layout(&self, layout: &DynamicTextLayout, x: f32, y: f32) {
368        if !layout.is_ready() {
369            error(
370                "DynamicTextLayout",
371                "DrawContext.draw_dynamic_text_layout() called before the DynamicTextLayout was ready; register on_ready and draw after the callback.",
372            );
373            return;
374        }
375        let node = layout.draw_node();
376        self.draw_text_node(&node, x, y);
377    }
378
379    /// Draws a texture with default linear sampling.
380    pub fn draw_image(&self, texture_id: u32, x: f32, y: f32, w: f32, h: f32) {
381        self.draw_image_sampling(texture_id, x, y, w, h, ImageSampling::linear());
382    }
383
384    /// Draws a texture with explicit sampling policy.
385    pub fn draw_image_sampling(
386        &self,
387        texture_id: u32,
388        x: f32,
389        y: f32,
390        w: f32,
391        h: f32,
392        sampling: ImageSampling,
393    ) {
394        let mut state = self.inner.borrow_mut();
395        state.words.push(OP_DRAW_IMAGE);
396        state.words.push(texture_id);
397        Self::push_float(&mut state.words, x);
398        Self::push_float(&mut state.words, y);
399        Self::push_float(&mut state.words, w);
400        Self::push_float(&mut state.words, h);
401        state.words.push(sampling.ffi_kind() as u32);
402        state.words.push(sampling.max_aniso());
403    }
404
405    /// Draws a retained SVG resource.
406    pub fn draw_svg(&self, svg_id: u32, x: f32, y: f32, w: f32, h: f32) {
407        let mut state = self.inner.borrow_mut();
408        state.words.push(OP_DRAW_SVG);
409        state.words.push(svg_id);
410        Self::push_float(&mut state.words, x);
411        Self::push_float(&mut state.words, y);
412        Self::push_float(&mut state.words, w);
413        Self::push_float(&mut state.words, h);
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::{DrawContext, Paint, Path};
420    use crate::assets;
421    use crate::ffi::{self, Call};
422    use crate::frame_scheduler;
423    use crate::image_sampling::ImageSampling;
424    use crate::text::DynamicTextLayout;
425    use crate::typography::FontStack;
426    use crate::Unit;
427
428    #[test]
429    fn path_and_draw_context_emit_batched_host_calls() {
430        ffi::test::reset();
431
432        let mut path = Path::new();
433        path.move_to(1.0, 2.0)
434            .line_to(3.0, 4.0)
435            .add_circle(8.0, 9.0, 10.0);
436
437        let ctx = DrawContext::new(77);
438        ctx.draw_rect(0.0, 1.0, 20.0, 30.0, Paint::fill(0xFF00FFFF));
439        ctx.draw_path(&path, Paint::stroke(0xFFFFFFFF, 2.0));
440        ctx.draw_image_sampling(9, 0.0, 0.0, 40.0, 50.0, ImageSampling::linear());
441        ctx.flush();
442
443        let calls = ffi::test::take_calls();
444        assert!(calls
445            .iter()
446            .any(|call| matches!(call, Call::PathCreate { .. })));
447        assert!(calls.iter().any(|call| matches!(
448            call,
449            Call::PathMoveTo { x, y, .. } if (*x - 1.0).abs() < f32::EPSILON && (*y - 2.0).abs() < f32::EPSILON
450        )));
451        assert!(calls.iter().any(|call| matches!(
452            call,
453            Call::CanvasDrawBatch { canvas_ptr: 77, words } if !words.is_empty()
454        )));
455    }
456
457    #[test]
458    fn draw_dynamic_text_layout_emits_text_command_without_exposing_draw_node() {
459        ffi::test::reset();
460        frame_scheduler::reset_commit_state();
461        assets::test_reset();
462
463        let layout = DynamicTextLayout::fixed_charset("0123456789");
464        layout
465            .font_stack(FontStack::from_id(1), 14.0)
466            .width(72.0, Unit::Pixel)
467            .height(20.0, Unit::Pixel)
468            .set_text("42");
469        layout.on_ready(|_| {});
470        frame_scheduler::fire_loaded_callbacks();
471        assert!(layout.is_ready());
472        ffi::test::take_calls();
473
474        let ctx = DrawContext::new(88);
475        ctx.draw_dynamic_text_layout(&layout, 5.0, 7.0);
476        ctx.flush();
477
478        let calls = ffi::test::take_calls();
479        assert!(calls.iter().any(|call| matches!(
480            call,
481            Call::CanvasDrawBatch { canvas_ptr: 88, words }
482                if words.first() == Some(&super::OP_DRAW_TEXT_NODE)
483        )));
484    }
485}