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 {
30 pub fill_color: u32,
31 pub stroke_color: u32,
32 pub stroke_width: f32,
33}
34
35impl Paint {
36 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 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 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 pub fn has_fill(self) -> bool {
65 (self.fill_color & 0xff) != 0
66 }
67
68 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)]
85pub 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 pub fn new() -> Self {
99 let id = unsafe { ffi::fui_path_create() };
100 Self {
101 resource: Rc::new(PathResource { id }),
102 }
103 }
104
105 pub fn id(&self) -> u32 {
107 self.resource.id
108 }
109
110 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 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 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 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 pub fn close(&mut self) -> &mut Self {
144 unsafe { ffi::fui_path_close(self.id()) };
145 self
146 }
147
148 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 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)]
169pub 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 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 pub fn save(&self) {
214 self.inner.borrow_mut().words.push(OP_SAVE);
215 }
216
217 pub fn restore(&self) {
219 self.inner.borrow_mut().words.push(OP_RESTORE);
220 }
221
222 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}