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)]
28pub struct Paint {
29    pub fill_color: u32,
30    pub stroke_color: u32,
31    pub stroke_width: f32,
32}
33
34impl Paint {
35    pub fn fill(color: u32) -> Self {
36        Self {
37            fill_color: color,
38            stroke_color: 0,
39            stroke_width: 0.0,
40        }
41    }
42
43    pub fn stroke(color: u32, width: f32) -> Self {
44        Self {
45            fill_color: 0,
46            stroke_color: color,
47            stroke_width: width,
48        }
49    }
50
51    pub fn filled_stroke(fill_color: u32, stroke_color: u32, stroke_width: f32) -> Self {
52        Self {
53            fill_color,
54            stroke_color,
55            stroke_width,
56        }
57    }
58
59    pub fn has_fill(self) -> bool {
60        (self.fill_color & 0xff) != 0
61    }
62
63    pub fn has_stroke(self) -> bool {
64        self.stroke_width > 0.0 && (self.stroke_color & 0xff) != 0
65    }
66}
67
68struct PathResource {
69    id: u32,
70}
71
72impl Drop for PathResource {
73    fn drop(&mut self) {
74        unsafe { ffi::fui_path_destroy(self.id) };
75    }
76}
77
78#[derive(Clone)]
79pub struct Path {
80    resource: Rc<PathResource>,
81}
82
83impl Default for Path {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Path {
90    pub fn new() -> Self {
91        let id = unsafe { ffi::fui_path_create() };
92        Self {
93            resource: Rc::new(PathResource { id }),
94        }
95    }
96
97    pub fn id(&self) -> u32 {
98        self.resource.id
99    }
100
101    pub fn move_to(&mut self, x: f32, y: f32) -> &mut Self {
102        unsafe { ffi::fui_path_move_to(self.id(), x, y) };
103        self
104    }
105
106    pub fn line_to(&mut self, x: f32, y: f32) -> &mut Self {
107        unsafe { ffi::fui_path_line_to(self.id(), x, y) };
108        self
109    }
110
111    pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) -> &mut Self {
112        unsafe { ffi::fui_path_quad_to(self.id(), cx, cy, x, y) };
113        self
114    }
115
116    pub fn cubic_to(
117        &mut self,
118        cx1: f32,
119        cy1: f32,
120        cx2: f32,
121        cy2: f32,
122        x: f32,
123        y: f32,
124    ) -> &mut Self {
125        unsafe { ffi::fui_path_cubic_to(self.id(), cx1, cy1, cx2, cy2, x, y) };
126        self
127    }
128
129    pub fn close(&mut self) -> &mut Self {
130        unsafe { ffi::fui_path_close(self.id()) };
131        self
132    }
133
134    pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Self {
135        unsafe { ffi::fui_path_add_rect(self.id(), x, y, w, h) };
136        self
137    }
138
139    pub fn add_circle(&mut self, cx: f32, cy: f32, r: f32) -> &mut Self {
140        unsafe { ffi::fui_path_add_circle(self.id(), cx, cy, r) };
141        self
142    }
143}
144
145#[derive(Default)]
146struct DrawContextState {
147    canvas_ptr: usize,
148    words: Vec<u32>,
149    retained_paths: Vec<Path>,
150}
151
152#[derive(Clone, Default)]
153pub struct DrawContext {
154    inner: Rc<RefCell<DrawContextState>>,
155}
156
157impl DrawContext {
158    pub fn new(canvas_ptr: usize) -> Self {
159        Self {
160            inner: Rc::new(RefCell::new(DrawContextState {
161                canvas_ptr,
162                words: Vec::new(),
163                retained_paths: Vec::new(),
164            })),
165        }
166    }
167
168    fn push_float(words: &mut Vec<u32>, value: f32) {
169        words.push(value.to_bits());
170    }
171
172    pub fn flush(&self) {
173        let mut state = self.inner.borrow_mut();
174        if state.words.is_empty() {
175            return;
176        }
177        unsafe {
178            ffi::fui_canvas_draw_batch(
179                state.canvas_ptr,
180                state.words.as_ptr() as usize,
181                state.words.len() as u32,
182            )
183        };
184        state.words.clear();
185        state.retained_paths.clear();
186    }
187
188    pub fn save(&self) {
189        self.inner.borrow_mut().words.push(OP_SAVE);
190    }
191
192    pub fn restore(&self) {
193        self.inner.borrow_mut().words.push(OP_RESTORE);
194    }
195
196    pub fn translate(&self, x: f32, y: f32) {
197        let mut state = self.inner.borrow_mut();
198        state.words.push(OP_TRANSLATE);
199        Self::push_float(&mut state.words, x);
200        Self::push_float(&mut state.words, y);
201    }
202
203    pub fn scale(&self, sx: f32, sy: f32) {
204        let mut state = self.inner.borrow_mut();
205        state.words.push(OP_SCALE);
206        Self::push_float(&mut state.words, sx);
207        Self::push_float(&mut state.words, sy);
208    }
209
210    pub fn rotate(&self, degrees: f32) {
211        let mut state = self.inner.borrow_mut();
212        state.words.push(OP_ROTATE);
213        Self::push_float(&mut state.words, degrees);
214    }
215
216    pub fn clip_rect(&self, x: f32, y: f32, w: f32, h: f32) {
217        let mut state = self.inner.borrow_mut();
218        state.words.push(OP_CLIP_RECT);
219        Self::push_float(&mut state.words, x);
220        Self::push_float(&mut state.words, y);
221        Self::push_float(&mut state.words, w);
222        Self::push_float(&mut state.words, h);
223    }
224
225    pub fn clip_round_rect(
226        &self,
227        x: f32,
228        y: f32,
229        w: f32,
230        h: f32,
231        tl: f32,
232        tr: f32,
233        br: f32,
234        bl: f32,
235    ) {
236        let mut state = self.inner.borrow_mut();
237        state.words.push(OP_CLIP_ROUND_RECT);
238        Self::push_float(&mut state.words, x);
239        Self::push_float(&mut state.words, y);
240        Self::push_float(&mut state.words, w);
241        Self::push_float(&mut state.words, h);
242        Self::push_float(&mut state.words, tl);
243        Self::push_float(&mut state.words, tr);
244        Self::push_float(&mut state.words, br);
245        Self::push_float(&mut state.words, bl);
246    }
247
248    pub fn draw_rect(&self, x: f32, y: f32, w: f32, h: f32, paint: Paint) {
249        let mut state = self.inner.borrow_mut();
250        state.words.push(OP_DRAW_RECT);
251        Self::push_float(&mut state.words, x);
252        Self::push_float(&mut state.words, y);
253        Self::push_float(&mut state.words, w);
254        Self::push_float(&mut state.words, h);
255        state.words.push(paint.fill_color);
256        state.words.push(paint.stroke_color);
257        Self::push_float(&mut state.words, paint.stroke_width);
258    }
259
260    pub fn draw_circle(&self, cx: f32, cy: f32, radius: f32, paint: Paint) {
261        let mut state = self.inner.borrow_mut();
262        state.words.push(OP_DRAW_CIRCLE);
263        Self::push_float(&mut state.words, cx);
264        Self::push_float(&mut state.words, cy);
265        Self::push_float(&mut state.words, radius);
266        state.words.push(paint.fill_color);
267        state.words.push(paint.stroke_color);
268        Self::push_float(&mut state.words, paint.stroke_width);
269    }
270
271    pub fn draw_line(&self, x1: f32, y1: f32, x2: f32, y2: f32, color: u32, stroke_width: f32) {
272        let mut state = self.inner.borrow_mut();
273        state.words.push(OP_DRAW_LINE);
274        Self::push_float(&mut state.words, x1);
275        Self::push_float(&mut state.words, y1);
276        Self::push_float(&mut state.words, x2);
277        Self::push_float(&mut state.words, y2);
278        state.words.push(color);
279        Self::push_float(&mut state.words, stroke_width);
280    }
281
282    pub fn draw_round_rect(&self, x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32, paint: Paint) {
283        let mut state = self.inner.borrow_mut();
284        state.words.push(OP_DRAW_ROUND_RECT);
285        Self::push_float(&mut state.words, x);
286        Self::push_float(&mut state.words, y);
287        Self::push_float(&mut state.words, w);
288        Self::push_float(&mut state.words, h);
289        Self::push_float(&mut state.words, rx);
290        Self::push_float(&mut state.words, ry);
291        state.words.push(paint.fill_color);
292        state.words.push(paint.stroke_color);
293        Self::push_float(&mut state.words, paint.stroke_width);
294    }
295
296    pub fn draw_path(&self, path: &Path, paint: Paint) {
297        let mut state = self.inner.borrow_mut();
298        state.words.push(OP_DRAW_PATH);
299        state.words.push(path.id());
300        state.words.push(paint.fill_color);
301        state.words.push(paint.stroke_color);
302        Self::push_float(&mut state.words, paint.stroke_width);
303        state.retained_paths.push(path.clone());
304    }
305
306    pub fn draw_text_node<T: Node>(&self, node: &T, x: f32, y: f32) {
307        let handle = node.handle().raw();
308        let mut state = self.inner.borrow_mut();
309        state.words.push(OP_DRAW_TEXT_NODE);
310        state.words.push(handle as u32);
311        state.words.push((handle >> 32) as u32);
312        Self::push_float(&mut state.words, x);
313        Self::push_float(&mut state.words, y);
314    }
315
316    pub fn draw_text_layout(&self, layout: &TextLayout, x: f32, y: f32) {
317        if !layout.is_ready() {
318            error(
319                "TextLayout",
320                "DrawContext.draw_text_layout() called before the TextLayout was ready; register on_ready and draw after the callback.",
321            );
322            return;
323        }
324        let node = layout.draw_node();
325        self.draw_text_node(&node, x, y);
326    }
327
328    pub fn draw_dynamic_text_layout(&self, layout: &DynamicTextLayout, x: f32, y: f32) {
329        if !layout.is_ready() {
330            error(
331                "DynamicTextLayout",
332                "DrawContext.draw_dynamic_text_layout() called before the DynamicTextLayout was ready; register on_ready and draw after the callback.",
333            );
334            return;
335        }
336        let node = layout.draw_node();
337        self.draw_text_node(&node, x, y);
338    }
339
340    pub fn draw_image(&self, texture_id: u32, x: f32, y: f32, w: f32, h: f32) {
341        self.draw_image_sampling(texture_id, x, y, w, h, ImageSampling::linear());
342    }
343
344    pub fn draw_image_sampling(
345        &self,
346        texture_id: u32,
347        x: f32,
348        y: f32,
349        w: f32,
350        h: f32,
351        sampling: ImageSampling,
352    ) {
353        let mut state = self.inner.borrow_mut();
354        state.words.push(OP_DRAW_IMAGE);
355        state.words.push(texture_id);
356        Self::push_float(&mut state.words, x);
357        Self::push_float(&mut state.words, y);
358        Self::push_float(&mut state.words, w);
359        Self::push_float(&mut state.words, h);
360        state.words.push(sampling.ffi_kind() as u32);
361        state.words.push(sampling.max_aniso());
362    }
363
364    pub fn draw_svg(&self, svg_id: u32, x: f32, y: f32, w: f32, h: f32) {
365        let mut state = self.inner.borrow_mut();
366        state.words.push(OP_DRAW_SVG);
367        state.words.push(svg_id);
368        Self::push_float(&mut state.words, x);
369        Self::push_float(&mut state.words, y);
370        Self::push_float(&mut state.words, w);
371        Self::push_float(&mut state.words, h);
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::{DrawContext, Paint, Path};
378    use crate::assets;
379    use crate::ffi::{self, Call};
380    use crate::frame_scheduler;
381    use crate::image_sampling::ImageSampling;
382    use crate::text::DynamicTextLayout;
383    use crate::typography::FontStack;
384    use crate::Unit;
385
386    #[test]
387    fn path_and_draw_context_emit_batched_host_calls() {
388        ffi::test::reset();
389
390        let mut path = Path::new();
391        path.move_to(1.0, 2.0)
392            .line_to(3.0, 4.0)
393            .add_circle(8.0, 9.0, 10.0);
394
395        let ctx = DrawContext::new(77);
396        ctx.draw_rect(0.0, 1.0, 20.0, 30.0, Paint::fill(0xFF00FFFF));
397        ctx.draw_path(&path, Paint::stroke(0xFFFFFFFF, 2.0));
398        ctx.draw_image_sampling(9, 0.0, 0.0, 40.0, 50.0, ImageSampling::linear());
399        ctx.flush();
400
401        let calls = ffi::test::take_calls();
402        assert!(calls
403            .iter()
404            .any(|call| matches!(call, Call::PathCreate { .. })));
405        assert!(calls.iter().any(|call| matches!(
406            call,
407            Call::PathMoveTo { x, y, .. } if (*x - 1.0).abs() < f32::EPSILON && (*y - 2.0).abs() < f32::EPSILON
408        )));
409        assert!(calls.iter().any(|call| matches!(
410            call,
411            Call::CanvasDrawBatch { canvas_ptr: 77, words } if !words.is_empty()
412        )));
413    }
414
415    #[test]
416    fn draw_dynamic_text_layout_emits_text_command_without_exposing_draw_node() {
417        ffi::test::reset();
418        frame_scheduler::reset_commit_state();
419        assets::test_reset();
420
421        let layout = DynamicTextLayout::fixed_charset("0123456789");
422        layout
423            .font_stack(FontStack::from_id(1), 14.0)
424            .width(72.0, Unit::Pixel)
425            .height(20.0, Unit::Pixel)
426            .set_text("42");
427        layout.on_ready(|_| {});
428        frame_scheduler::fire_loaded_callbacks();
429        assert!(layout.is_ready());
430        ffi::test::take_calls();
431
432        let ctx = DrawContext::new(88);
433        ctx.draw_dynamic_text_layout(&layout, 5.0, 7.0);
434        ctx.flush();
435
436        let calls = ffi::test::take_calls();
437        assert!(calls.iter().any(|call| matches!(
438            call,
439            Call::CanvasDrawBatch { canvas_ptr: 88, words }
440                if words.first() == Some(&super::OP_DRAW_TEXT_NODE)
441        )));
442    }
443}