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}