1use std::f64::consts::PI;
37use std::sync::Arc;
38
39use agg_rust::arc::Arc as AggArc;
40use agg_rust::basics::PATH_FLAGS_NONE;
41use agg_rust::comp_op::CompOp;
42use agg_rust::conv_curve::ConvCurve;
43use agg_rust::conv_stroke::ConvStroke;
44use agg_rust::gsv_text::GsvText;
45use agg_rust::math_stroke::{LineCap, LineJoin};
46use agg_rust::path_storage::PathStorage;
47use agg_rust::rounded_rect::RoundedRect;
48use agg_rust::trans_affine::TransAffine;
49
50use crate::color::Color;
51use crate::draw_ctx::DrawCtx;
52use crate::lcd_coverage::{rasterize_text_lcd_cached, LcdBuffer, LcdMask};
53use crate::text::{measure_text_metrics, Font, TextMetrics};
54
55#[derive(Clone)]
63struct LcdState {
64 transform: TransAffine,
65 fill_color: Color,
66 stroke_color: Color,
67 line_width: f64,
68 line_join: LineJoin,
69 line_cap: LineCap,
70 blend_mode: CompOp,
71 global_alpha: f64,
72 font: Option<Arc<Font>>,
73 font_size: f64,
74 clip: Option<(f64, f64, f64, f64)>,
78}
79
80impl Default for LcdState {
81 fn default() -> Self {
82 Self {
83 transform: TransAffine::new(),
84 fill_color: Color::black(),
85 stroke_color: Color::black(),
86 line_width: 1.0,
87 line_join: LineJoin::Round,
88 line_cap: LineCap::Round,
89 blend_mode: CompOp::SrcOver,
90 global_alpha: 1.0,
91 font: None,
92 font_size: 16.0,
93 clip: None,
94 }
95 }
96}
97
98struct LcdLayer {
113 buffer: LcdBuffer,
114 saved_state: LcdState,
118 saved_stack: Vec<LcdState>,
119 origin_x: f64,
122 origin_y: f64,
123}
124
125pub struct LcdGfxCtx<'a> {
132 base_buffer: &'a mut LcdBuffer,
133 layer_stack: Vec<LcdLayer>,
138 state: LcdState,
139 state_stack: Vec<LcdState>,
140 path: PathStorage,
143}
144
145impl<'a> LcdGfxCtx<'a> {
146 pub fn new(buffer: &'a mut LcdBuffer) -> Self {
147 Self {
148 base_buffer: buffer,
149 layer_stack: Vec::new(),
150 state: LcdState::default(),
151 state_stack: Vec::new(),
152 path: PathStorage::new(),
153 }
154 }
155
156 pub fn buffer(&self) -> &LcdBuffer { self.base_buffer }
161
162 fn active_buffer(&mut self) -> &mut LcdBuffer {
166 if let Some(layer) = self.layer_stack.last_mut() {
167 &mut layer.buffer
168 } else {
169 &mut *self.base_buffer
170 }
171 }
172}
173
174impl<'a> DrawCtx for LcdGfxCtx<'a> {
177 fn set_fill_color (&mut self, color: Color) { self.state.fill_color = color; }
179 fn set_stroke_color(&mut self, color: Color) { self.state.stroke_color = color; }
180 fn set_line_width (&mut self, w: f64) { self.state.line_width = w; }
181 fn set_line_join (&mut self, j: LineJoin) { self.state.line_join = j; }
182 fn set_line_cap (&mut self, c: LineCap) { self.state.line_cap = c; }
183 fn set_blend_mode (&mut self, m: CompOp) { self.state.blend_mode = m; }
184 fn set_global_alpha(&mut self, a: f64) { self.state.global_alpha = a.clamp(0.0, 1.0); }
185
186 fn set_font (&mut self, f: Arc<Font>) { self.state.font = Some(f); }
188 fn set_font_size(&mut self, s: f64) { self.state.font_size = s.max(1.0); }
189
190 fn clip_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
192 let t = &self.state.transform;
195 let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)];
196 let mut sx_min = f64::INFINITY;
197 let mut sy_min = f64::INFINITY;
198 let mut sx_max = f64::NEG_INFINITY;
199 let mut sy_max = f64::NEG_INFINITY;
200 for (lx, ly) in corners {
201 let mut sx = lx; let mut sy = ly;
202 t.transform(&mut sx, &mut sy);
203 if sx < sx_min { sx_min = sx; }
204 if sx > sx_max { sx_max = sx; }
205 if sy < sy_min { sy_min = sy; }
206 if sy > sy_max { sy_max = sy; }
207 }
208 let new_clip = (sx_min, sy_min, (sx_max - sx_min).max(0.0), (sy_max - sy_min).max(0.0));
209 self.state.clip = Some(match self.state.clip {
210 Some((cx, cy, cw, ch)) => {
211 let x1 = sx_min.max(cx);
212 let y1 = sy_min.max(cy);
213 let x2 = (new_clip.0 + new_clip.2).min(cx + cw);
214 let y2 = (new_clip.1 + new_clip.3).min(cy + ch);
215 (x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0))
216 }
217 None => new_clip,
218 });
219 }
220 fn reset_clip(&mut self) { self.state.clip = None; }
221
222 fn clear(&mut self, color: Color) { self.active_buffer().clear(color); }
224
225 fn begin_path(&mut self) { self.path = PathStorage::new(); }
227 fn move_to(&mut self, x: f64, y: f64) { self.path.move_to(x, y); }
228 fn line_to(&mut self, x: f64, y: f64) { self.path.line_to(x, y); }
229 fn cubic_to(&mut self, cx1: f64, cy1: f64, cx2: f64, cy2: f64, x: f64, y: f64) {
230 self.path.curve4(cx1, cy1, cx2, cy2, x, y);
231 }
232 fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
233 self.path.curve3(cx, cy, x, y);
234 }
235 fn arc_to(&mut self, cx: f64, cy: f64, r: f64, start_angle: f64, end_angle: f64, ccw: bool) {
236 let mut arc = AggArc::new(cx, cy, r, r, start_angle, end_angle, ccw);
237 self.path.concat_path(&mut arc, 0);
238 }
239 fn circle(&mut self, cx: f64, cy: f64, r: f64) {
240 self.arc_to(cx, cy, r, 0.0, 2.0 * PI, true);
241 self.path.close_polygon(PATH_FLAGS_NONE);
242 }
243 fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
244 self.path.move_to(x, y);
245 self.path.line_to(x + w, y);
246 self.path.line_to(x + w, y + h);
247 self.path.line_to(x, y + h);
248 self.path.close_polygon(PATH_FLAGS_NONE);
249 }
250 fn rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, r: f64) {
251 let r = r.min(w * 0.5).min(h * 0.5).max(0.0);
252 let mut rr = RoundedRect::new(x, y, x + w, y + h, r);
253 rr.normalize_radius();
254 self.path.concat_path(&mut rr, 0);
255 }
256 fn close_path(&mut self) { self.path.close_polygon(PATH_FLAGS_NONE); }
257
258 fn fill(&mut self) {
260 let mut color = self.state.fill_color;
261 color.a *= self.state.global_alpha as f32;
262 let xform = self.state.transform;
263 let clip = self.state.clip;
264 let mut path = std::mem::replace(&mut self.path, PathStorage::new());
269 self.active_buffer().fill_path(&mut path, color, &xform, clip);
270 self.path = path;
271 }
272 fn stroke(&mut self) {
273 let mut color = self.state.stroke_color;
284 color.a *= self.state.global_alpha as f32;
285 let mut materialized = PathStorage::new();
286 {
287 let mut curves = ConvCurve::new(&mut self.path);
288 let mut stroke = ConvStroke::new(&mut curves);
289 stroke.set_width(self.state.line_width);
290 stroke.set_line_join(self.state.line_join);
291 stroke.set_line_cap(self.state.line_cap);
292 materialized.concat_path(&mut stroke, 0);
293 }
294 let xform = self.state.transform;
295 let clip = self.state.clip;
296 self.active_buffer().fill_path(&mut materialized, color, &xform, clip);
297 }
298 fn fill_and_stroke(&mut self) {
299 self.fill();
300 self.stroke();
301 }
302
303 fn draw_triangles_aa(
304 &mut self,
305 vertices: &[[f32; 3]],
306 indices: &[u32],
307 color: crate::color::Color,
308 ) {
309 let saved_fill = self.state.fill_color;
313 self.state.fill_color = color;
314 let n = indices.len() / 3;
315 for t in 0..n {
316 let i0 = indices[t * 3 ] as usize;
317 let i1 = indices[t * 3 + 1] as usize;
318 let i2 = indices[t * 3 + 2] as usize;
319 if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() { continue; }
320 let v0 = vertices[i0];
321 let v1 = vertices[i1];
322 let v2 = vertices[i2];
323 self.begin_path();
324 self.move_to(v0[0] as f64, v0[1] as f64);
325 self.line_to(v1[0] as f64, v1[1] as f64);
326 self.line_to(v2[0] as f64, v2[1] as f64);
327 self.close_path();
328 self.fill();
329 }
330 self.state.fill_color = saved_fill;
331 }
332
333 fn fill_text(&mut self, text: &str, x: f64, y: f64) {
335 let font = match self.state.font.clone() {
336 Some(f) => f,
337 None => return,
338 };
339 let mut color = self.state.fill_color;
340 color.a *= self.state.global_alpha as f32;
341
342 let t = &self.state.transform;
347 let ctm_scale = (t.sx * t.sx + t.shy * t.shy).sqrt().max(1e-6);
348 let phys_size = self.state.font_size * ctm_scale;
349 let cached = rasterize_text_lcd_cached(&font, text, phys_size);
350 let dst_x = x - cached.baseline_x_in_mask / ctm_scale;
357 let dst_y = y - cached.baseline_y_in_mask / ctm_scale;
358 let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
359 let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
360
361 let mask = LcdMask {
366 data: (*cached.pixels).clone(),
367 width: cached.width,
368 height: cached.height,
369 };
370 let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
371 self.active_buffer().composite_mask(&mask, color, sx, sy, clip_i);
372 }
373
374 fn fill_text_gsv(&mut self, text: &str, x: f64, y: f64, size: f64) {
375 let mut color = self.state.fill_color;
381 color.a *= self.state.global_alpha as f32;
382 let mut gsv = GsvText::new();
383 gsv.size(size, 0.0);
384 gsv.start_point(x, y);
385 gsv.text(text);
386 let mut materialized = PathStorage::new();
387 {
388 let mut stroke = ConvStroke::new(&mut gsv);
389 stroke.set_width(size * 0.1);
390 materialized.concat_path(&mut stroke, 0);
391 }
392 let xform = self.state.transform;
393 let clip = self.state.clip;
394 self.active_buffer().fill_path(&mut materialized, color, &xform, clip);
395 }
396
397 fn measure_text(&self, text: &str) -> Option<TextMetrics> {
398 let font = self.state.font.as_ref()?;
399 Some(measure_text_metrics(font, text, self.state.font_size))
400 }
401
402 fn transform(&self) -> TransAffine { self.state.transform }
404 fn save (&mut self) { self.state_stack.push(self.state.clone()); }
405 fn restore(&mut self) {
406 if let Some(s) = self.state_stack.pop() { self.state = s; }
407 }
408 fn translate(&mut self, tx: f64, ty: f64) {
409 self.state.transform.premultiply(&TransAffine::new_translation(tx, ty));
410 }
411 fn rotate(&mut self, radians: f64) {
412 self.state.transform.premultiply(&TransAffine::new_rotation(radians));
413 }
414 fn scale(&mut self, sx: f64, sy: f64) {
415 self.state.transform.premultiply(&TransAffine::new_scaling(sx, sy));
416 }
417 fn set_transform(&mut self, m: TransAffine) { self.state.transform = m; }
418 fn reset_transform(&mut self) { self.state.transform = TransAffine::new(); }
419
420 fn push_layer(&mut self, width: f64, height: f64) {
430 let origin_x = self.state.transform.tx;
431 let origin_y = self.state.transform.ty;
432 let lw = width.ceil().max(1.0) as u32;
433 let lh = height.ceil().max(1.0) as u32;
434 let mut layer_buffer = LcdBuffer::new(lw, lh);
435
436 let dx = -(origin_x.round() as i32);
444 let dy = -(origin_y.round() as i32);
445 let parent_ref: &LcdBuffer = if let Some(layer) = self.layer_stack.last() {
446 &layer.buffer
447 } else {
448 &*self.base_buffer
449 };
450 layer_buffer.composite_buffer(parent_ref, dx, dy, None);
451
452 let saved_state = self.state.clone();
453 let saved_stack = std::mem::take(&mut self.state_stack);
454 self.layer_stack.push(LcdLayer {
455 buffer: layer_buffer,
456 saved_state,
457 saved_stack,
458 origin_x,
459 origin_y,
460 });
461 self.state.transform = TransAffine::new();
465 self.state.clip = None;
466 }
467
468 fn pop_layer(&mut self) {
469 let Some(layer) = self.layer_stack.pop() else { return; };
470 self.state = layer.saved_state;
472 self.state_stack = layer.saved_stack;
473 let dst_x = layer.origin_x.round() as i32;
478 let dst_y = layer.origin_y.round() as i32;
479 let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
480 self.active_buffer().composite_buffer(&layer.buffer, dst_x, dst_y, clip_i);
481 }
482
483 fn draw_lcd_mask(
490 &mut self,
491 mask: &[u8],
492 mask_w: u32,
493 mask_h: u32,
494 src_color: Color,
495 dst_x: f64,
496 dst_y: f64,
497 ) {
498 if mask.len() < (mask_w as usize) * (mask_h as usize) * 3 { return; }
499 let lcd_mask = LcdMask { data: mask.to_vec(), width: mask_w, height: mask_h };
500 let t = &self.state.transform;
501 let sx = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
502 let sy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
503 let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
504 self.active_buffer().composite_mask(&lcd_mask, src_color, sx, sy, clip_i);
505 }
506
507 fn has_lcd_mask_composite(&self) -> bool { true }
508
509 fn has_image_blit(&self) -> bool { true }
521
522 fn draw_image_rgba(
523 &mut self,
524 data: &[u8],
525 img_w: u32,
526 img_h: u32,
527 dst_x: f64,
528 dst_y: f64,
529 dst_w: f64,
530 dst_h: f64,
531 ) {
532 if img_w == 0 || img_h == 0 { return; }
533 if dst_w <= 0.0 || dst_h <= 0.0 { return; }
534 if data.len() < (img_w as usize) * (img_h as usize) * 4 { return; }
535
536 let t = &self.state.transform;
543 let ox = (dst_x * t.sx + dst_y * t.shx + t.tx).round() as i32;
544 let oy = (dst_x * t.shy + dst_y * t.sy + t.ty).round() as i32;
545 let scaled_w = ((dst_w * t.sx).abs()).round() as i32;
546 let scaled_h = ((dst_h * t.sy).abs()).round() as i32;
547 if scaled_w <= 0 || scaled_h <= 0 { return; }
548
549 let global_alpha = (self.state.global_alpha as f32).clamp(0.0, 1.0);
550 let clip_i = self.state.clip.map(crate::lcd_coverage::rect_to_pixel_clip);
551
552 let buf = self.active_buffer();
553 let buf_w = buf.width() as i32;
554 let buf_h = buf.height() as i32;
555 let buf_w_u = buf_w as usize;
556 let img_w_u = img_w as usize;
557
558 let (cx1, cy1, cx2, cy2) = match clip_i {
561 Some((x1, y1, x2, y2)) => (x1.max(0), y1.max(0), x2.min(buf_w), y2.min(buf_h)),
562 None => (0, 0, buf_w, buf_h),
563 };
564 if cx1 >= cx2 || cy1 >= cy2 { return; }
565
566 let (color_plane, alpha_plane) = buf.planes_mut();
567 for ly in 0..scaled_h {
568 let dy = oy + ly;
569 if dy < cy1 || dy >= cy2 { continue; }
570 let frac_y = (ly as f64 + 0.5) / (scaled_h as f64);
574 let sy_visual = (frac_y * img_h as f64) as u32;
575 let sy_visual = sy_visual.min(img_h - 1);
576 let sy_storage = (img_h - 1 - sy_visual) as usize;
577
578 for lx in 0..scaled_w {
579 let dx = ox + lx;
580 if dx < cx1 || dx >= cx2 { continue; }
581 let frac_x = (lx as f64 + 0.5) / (scaled_w as f64);
582 let sx_storage = ((frac_x * img_w as f64) as u32).min(img_w - 1) as usize;
583
584 let si = (sy_storage * img_w_u + sx_storage) * 4;
592 let sa = (data[si + 3] as f32 / 255.0) * global_alpha;
593 if sa <= 0.0 { continue; }
594 let sr = (data[si] as f32 / 255.0) * sa; let sg = (data[si + 1] as f32 / 255.0) * sa;
596 let sb = (data[si + 2] as f32 / 255.0) * sa;
597
598 let di = ((dy as usize) * buf_w_u + (dx as usize)) * 3;
599
600 let bc_r = color_plane[di] as f32 / 255.0;
602 let bc_g = color_plane[di + 1] as f32 / 255.0;
603 let bc_b = color_plane[di + 2] as f32 / 255.0;
604 let ba_r = alpha_plane[di] as f32 / 255.0;
605 let ba_g = alpha_plane[di + 1] as f32 / 255.0;
606 let ba_b = alpha_plane[di + 2] as f32 / 255.0;
607
608 let rc_r = sr + bc_r * (1.0 - sa);
611 let rc_g = sg + bc_g * (1.0 - sa);
612 let rc_b = sb + bc_b * (1.0 - sa);
613 let ra_r = sa + ba_r * (1.0 - sa);
614 let ra_g = sa + ba_g * (1.0 - sa);
615 let ra_b = sa + ba_b * (1.0 - sa);
616
617 color_plane[di] = (rc_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
618 color_plane[di + 1] = (rc_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
619 color_plane[di + 2] = (rc_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
620 alpha_plane[di] = (ra_r * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
621 alpha_plane[di + 1] = (ra_g * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
622 alpha_plane[di + 2] = (ra_b * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
623 }
624 }
625 }
626}
627
628#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::framebuffer::Framebuffer;
634 use crate::gfx_ctx::GfxCtx;
635
636 const FONT_BYTES: &[u8] = include_bytes!("../../demo/assets/CascadiaCode.ttf");
637
638 fn font() -> Arc<Font> {
639 Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
640 }
641
642 #[test]
647 fn test_lcd_gfx_ctx_basic_fill_text_smoke() {
648 let mut buf = LcdBuffer::new(80, 24);
649 {
650 let mut ctx = LcdGfxCtx::new(&mut buf);
651 ctx.clear(Color::white());
652 ctx.set_fill_color(Color::black());
653 ctx.set_font(font());
654 ctx.set_font_size(16.0);
655 ctx.fill_text("ABC", 4.0, 14.0);
656 }
657 let any_dark = buf.color_plane().chunks_exact(3)
659 .any(|p| p[0] < 250 || p[1] < 250 || p[2] < 250);
660 assert!(any_dark, "fill_text via LcdGfxCtx left buffer fully white");
661 }
662
663 #[test]
679 fn test_lcd_gfx_ctx_text_matches_legacy_lcd_mode() {
680 let f = font();
681 let w = 120u32;
682 let h = 28u32;
683
684 let mut fb = Framebuffer::new(w, h);
686 {
687 let mut ctx = GfxCtx::new(&mut fb);
688 ctx.set_lcd_mode(true);
689 ctx.clear(Color::white());
690 ctx.set_fill_color(Color::black());
691 ctx.set_font(Arc::clone(&f));
692 ctx.set_font_size(18.0);
693 <GfxCtx as DrawCtx>::fill_text(&mut ctx, "Hello!", 4.0, 18.0);
694 }
695
696 let mut buf = LcdBuffer::new(w, h);
698 {
699 let mut ctx = LcdGfxCtx::new(&mut buf);
700 ctx.clear(Color::white());
701 ctx.set_fill_color(Color::black());
702 ctx.set_font(Arc::clone(&f));
703 ctx.set_font_size(18.0);
704 ctx.fill_text("Hello!", 4.0, 18.0);
705 }
706
707 for y in 0..h as usize {
711 for x in 0..w as usize {
712 let ai = (y * w as usize + x) * 4;
713 let bi = (y * w as usize + x) * 3;
714 let a_rgb = (fb.pixels()[ai], fb.pixels()[ai + 1], fb.pixels()[ai + 2]);
715 let b_rgb = (buf.color_plane()[bi], buf.color_plane()[bi + 1], buf.color_plane()[bi + 2]);
716 assert_eq!(a_rgb, b_rgb,
717 "pixel mismatch at ({x},{y}): legacy={a_rgb:?} LcdGfxCtx={b_rgb:?}");
718 }
719 }
720 }
721
722 #[test]
728 fn test_lcd_gfx_ctx_stroke_horizontal_line() {
729 let mut buf = LcdBuffer::new(20, 11);
730 {
731 let mut ctx = LcdGfxCtx::new(&mut buf);
732 ctx.clear(Color::white());
733 ctx.set_stroke_color(Color::black());
734 ctx.set_line_width(1.0);
735 ctx.begin_path();
736 ctx.move_to(2.0, 5.0);
737 ctx.line_to(18.0, 5.0);
738 ctx.stroke();
739 }
740 let row_brightness = |y: usize| -> u32 {
741 (4..16).map(|x| {
742 let i = (y * 20 + x) * 3;
743 buf.color_plane()[i] as u32 + buf.color_plane()[i + 1] as u32 + buf.color_plane()[i + 2] as u32
744 }).sum()
745 };
746 let line = row_brightness(5); let above = row_brightness(8);
748 let below = row_brightness(2);
749 assert!(line < above, "stroke row should be darker than row above (line={line}, above={above})");
750 assert!(line < below, "stroke row should be darker than row below (line={line}, below={below})");
751 }
752
753 #[test]
757 fn test_lcd_gfx_ctx_circle_darkens_center_not_corner() {
758 let mut buf = LcdBuffer::new(20, 20);
759 {
760 let mut ctx = LcdGfxCtx::new(&mut buf);
761 ctx.clear(Color::white());
762 ctx.set_fill_color(Color::black());
763 ctx.begin_path();
764 ctx.circle(10.0, 10.0, 5.0);
765 ctx.fill();
766 }
767 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
768 let i = (y * 20 + x) * 3;
769 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
770 };
771 let (cr, cg, cb) = pixel(10, 10);
772 assert!(cr < 60 && cg < 60 && cb < 60,
773 "circle centre should be dark; got ({cr}, {cg}, {cb})");
774 let (xr, xg, xb) = pixel(1, 1);
775 assert!(xr > 240 && xg > 240 && xb > 240,
776 "outside-circle corner should stay white; got ({xr}, {xg}, {xb})");
777 }
778
779 #[test]
791 fn test_lcd_gfx_ctx_rounded_rect_clips_corners() {
792 let mut buf = LcdBuffer::new(20, 20);
793 {
794 let mut ctx = LcdGfxCtx::new(&mut buf);
795 ctx.clear(Color::white());
796 ctx.set_fill_color(Color::black());
797 ctx.begin_path();
798 ctx.rounded_rect(0.0, 0.0, 20.0, 20.0, 8.0);
799 ctx.fill();
800 }
801 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
802 let i = (y * 20 + x) * 3;
803 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
804 };
805 let (cr, cg, cb) = pixel(10, 10);
807 assert!(cr < 50 && cg < 50 && cb < 50,
808 "rounded rect centre should be dark; got ({cr}, {cg}, {cb})");
809 let (xr, xg, xb) = pixel(1, 1);
812 assert!(xr > 240 && xg > 240 && xb > 240,
813 "rounded rect corner area should stay white; got ({xr}, {xg}, {xb})");
814 let (er, eg, eb) = pixel(10, 1);
817 assert!(er < 50 && eg < 50 && eb < 50,
818 "rounded rect mid-edge should be dark; got ({er}, {eg}, {eb})");
819 }
820
821 #[test]
828 fn test_lcd_gfx_ctx_image_blit_y_flips_correctly() {
829 let img: Vec<u8> = vec![
831 255, 0, 0, 255, 0, 255, 0, 255,
833 0, 0, 255, 255, 128, 128, 128, 255,
835 ];
836 let mut buf = LcdBuffer::new(8, 8);
837 {
838 let mut ctx = LcdGfxCtx::new(&mut buf);
839 ctx.clear(Color::black());
840 ctx.draw_image_rgba(&img, 2, 2, 1.0, 1.0, 2.0, 2.0);
841 }
842 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
843 let i = (y * 8 + x) * 3;
844 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
845 };
846 assert_eq!(pixel(1, 2), (255, 0, 0), "top-left source must land at top-left of dst rect (Y-up high)");
850 assert_eq!(pixel(2, 2), ( 0, 255, 0), "top-right source must land at top-right of dst rect");
851 assert_eq!(pixel(1, 1), ( 0, 0, 255), "bottom-left source must land at bottom-left of dst rect (Y-up low)");
852 assert_eq!(pixel(2, 1), (128, 128, 128), "bottom-right source must land at bottom-right of dst rect");
853 assert_eq!(pixel(0, 0), (0, 0, 0), "pixel outside blit rect should be untouched");
855 }
856
857 #[test]
861 fn test_lcd_gfx_ctx_image_blit_alpha_blends_with_destination() {
862 let img: Vec<u8> = vec![255, 0, 0, 128];
864 let mut buf = LcdBuffer::new(4, 4);
865 {
866 let mut ctx = LcdGfxCtx::new(&mut buf);
867 ctx.clear(Color::white());
868 ctx.draw_image_rgba(&img, 1, 1, 1.0, 1.0, 1.0, 1.0);
869 }
870 let i = (1 * 4 + 1) * 3;
871 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
872 assert!(r > 250, "R should be near 255 (bg + src red); got {r}");
875 assert!(g > 120 && g < 140, "G should be near 127 (white minus alpha-attenuated red); got {g}");
876 assert!(b > 120 && b < 140, "B should be near 127; got {b}");
877 }
878
879 #[test]
886 fn test_lcd_gfx_ctx_clip_rect_constrains_fill() {
887 let mut buf = LcdBuffer::new(20, 10);
888 {
889 let mut ctx = LcdGfxCtx::new(&mut buf);
890 ctx.clear(Color::white());
891 ctx.set_fill_color(Color::black());
892 ctx.clip_rect(0.0, 0.0, 10.0, 10.0); ctx.begin_path();
894 ctx.rect(2.0, 2.0, 16.0, 6.0); ctx.fill();
896 }
897 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
898 let i = (y * 20 + x) * 3;
899 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
900 };
901 let (lr, lg, lb) = pixel(5, 5);
903 assert!(lr < 50 && lg < 50 && lb < 50,
904 "pixel inside clip + rect should be dark; got ({lr}, {lg}, {lb})");
905 let (rr, rg, rb) = pixel(15, 5);
907 assert!(rr > 240 && rg > 240 && rb > 240,
908 "pixel outside clip should stay white; got ({rr}, {rg}, {rb})");
909 }
910
911 #[test]
915 fn test_lcd_gfx_ctx_clip_rect_constrains_fill_text() {
916 let mut buf = LcdBuffer::new(120, 24);
917 {
918 let mut ctx = LcdGfxCtx::new(&mut buf);
919 ctx.clear(Color::white());
920 ctx.set_fill_color(Color::black());
921 ctx.set_font(font());
922 ctx.set_font_size(18.0);
923 ctx.clip_rect(0.0, 0.0, 40.0, 24.0); ctx.fill_text("MMMMMMMMMMMM", 2.0, 18.0);
925 }
926 let mut saw_dark_inside = false;
929 for x in 0..40 {
930 for y in 0..24 {
931 let i = (y * 120 + x) * 3;
932 if buf.color_plane()[i] < 100 { saw_dark_inside = true; break; }
933 }
934 if saw_dark_inside { break; }
935 }
936 assert!(saw_dark_inside, "expected some dark text pixel inside the clip");
937
938 for x in 42..120 {
942 for y in 0..24 {
943 let i = (y * 120 + x) * 3;
944 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
945 assert!(r > 240 && g > 240 && b > 240,
946 "pixel at ({x},{y}) outside clip should stay white; got ({r}, {g}, {b})");
947 }
948 }
949 }
950
951 #[test]
955 fn test_lcd_gfx_ctx_clip_rect_constrains_image_blit() {
956 let img: Vec<u8> = (0..10*10).flat_map(|_| [255u8, 0, 0, 255]).collect();
958 let mut buf = LcdBuffer::new(20, 10);
959 {
960 let mut ctx = LcdGfxCtx::new(&mut buf);
961 ctx.clear(Color::white());
962 ctx.clip_rect(0.0, 0.0, 5.0, 10.0); ctx.draw_image_rgba(&img, 10, 10, 0.0, 0.0, 10.0, 10.0);
964 }
965 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
966 let i = (y * 20 + x) * 3;
967 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
968 };
969 assert_eq!(pixel(2, 5), (255, 0, 0), "inside clip should show source red");
971 assert_eq!(pixel(7, 5), (255, 255, 255), "outside clip should stay white");
973 }
974
975 #[test]
978 fn test_lcd_gfx_ctx_reset_clip_restores_full_buffer() {
979 let mut buf = LcdBuffer::new(20, 10);
980 {
981 let mut ctx = LcdGfxCtx::new(&mut buf);
982 ctx.clear(Color::white());
983 ctx.set_fill_color(Color::black());
984 ctx.clip_rect(0.0, 0.0, 5.0, 10.0);
985 ctx.reset_clip();
986 ctx.begin_path();
987 ctx.rect(2.0, 2.0, 16.0, 6.0); ctx.fill();
989 }
990 let i = (5 * 20 + 15) * 3;
992 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
993 assert!(r < 50 && g < 50 && b < 50,
994 "after reset_clip, fill at x=15 should be dark; got ({r}, {g}, {b})");
995 }
996
997 #[test]
1001 fn test_lcd_gfx_ctx_clip_rect_nests_via_intersection() {
1002 let mut buf = LcdBuffer::new(20, 20);
1003 {
1004 let mut ctx = LcdGfxCtx::new(&mut buf);
1005 ctx.clear(Color::white());
1006 ctx.set_fill_color(Color::black());
1007 ctx.clip_rect(0.0, 0.0, 10.0, 20.0);
1009 ctx.clip_rect(0.0, 10.0, 20.0, 10.0);
1011 ctx.begin_path();
1012 ctx.rect(0.0, 0.0, 20.0, 20.0); ctx.fill();
1014 }
1015 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
1016 let i = (y * 20 + x) * 3;
1017 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
1018 };
1019 let (tlr, tlg, tlb) = pixel(2, 17);
1021 assert!(tlr < 50 && tlg < 50 && tlb < 50,
1022 "top-left should be dark; got ({tlr}, {tlg}, {tlb})");
1023 let (trr, trg, trb) = pixel(17, 17);
1025 assert!(trr > 240 && trg > 240 && trb > 240,
1026 "top-right should stay white; got ({trr}, {trg}, {trb})");
1027 let (blr, blg, blb) = pixel(2, 2);
1029 assert!(blr > 240 && blg > 240 && blb > 240,
1030 "bottom-left should stay white; got ({blr}, {blg}, {blb})");
1031 }
1032
1033 #[test]
1039 fn test_lcd_gfx_ctx_push_pop_layer_flushes_into_parent() {
1040 let mut buf = LcdBuffer::new(20, 20);
1041 {
1042 let mut ctx = LcdGfxCtx::new(&mut buf);
1043 ctx.clear(Color::white());
1044 ctx.translate(5.0, 5.0);
1048 ctx.push_layer(8.0, 8.0);
1049 ctx.set_fill_color(Color::black());
1050 ctx.begin_path();
1051 ctx.rect(0.0, 0.0, 8.0, 8.0); ctx.fill();
1053 ctx.pop_layer();
1054 }
1055 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
1056 let i = (y * 20 + x) * 3;
1057 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
1058 };
1059 assert_eq!(pixel(8, 8), (0, 0, 0), "interior of flushed layer should be dark");
1061 assert_eq!(pixel(2, 2), (255, 255, 255), "outside layer region should stay white");
1063 assert_eq!(pixel(15, 15), (255, 255, 255), "outside layer region should stay white");
1064 }
1065
1066 #[test]
1071 fn test_lcd_gfx_ctx_push_pop_layer_restores_state() {
1072 let mut buf = LcdBuffer::new(20, 20);
1073 {
1074 let mut ctx = LcdGfxCtx::new(&mut buf);
1075 ctx.clear(Color::white());
1076
1077 ctx.set_fill_color(Color::white()); ctx.translate(3.0, 4.0);
1079 assert_eq!((ctx.transform().tx, ctx.transform().ty), (3.0, 4.0));
1080
1081 ctx.push_layer(10.0, 10.0);
1082 assert_eq!((ctx.transform().tx, ctx.transform().ty), (0.0, 0.0),
1084 "push_layer must reset transform inside the layer");
1085 ctx.set_fill_color(Color::rgba(0.1, 0.2, 0.3, 1.0));
1087 ctx.translate(1.0, 1.0);
1088 ctx.pop_layer();
1089
1090 assert_eq!((ctx.transform().tx, ctx.transform().ty), (3.0, 4.0),
1093 "pop_layer must restore transform to its push-time value");
1094
1095 ctx.begin_path();
1100 ctx.rect(0.0, 0.0, 4.0, 4.0);
1101 ctx.fill();
1102 }
1103 let i = (5 * 20 + 5) * 3;
1106 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
1107 assert_eq!((r, g, b), (255, 255, 255), "post-pop fill must use restored white colour");
1108 }
1109
1110 #[test]
1114 fn test_lcd_gfx_ctx_push_layer_isolates_paint_until_pop() {
1115 let mut buf = LcdBuffer::new(20, 20);
1116 {
1117 let mut ctx = LcdGfxCtx::new(&mut buf);
1118 ctx.clear(Color::white());
1119 ctx.push_layer(10.0, 10.0);
1120 ctx.set_fill_color(Color::black());
1121 ctx.begin_path();
1122 ctx.rect(0.0, 0.0, 10.0, 10.0);
1123 ctx.fill();
1124 let base = ctx.buffer();
1126 assert!(base.color_plane().chunks_exact(3).all(|p| p[0] == 255 && p[1] == 255 && p[2] == 255),
1127 "base buffer must not see layer paint until pop_layer");
1128 ctx.pop_layer();
1129 }
1130 let i = (5 * 20 + 5) * 3;
1132 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
1133 assert_eq!((r, g, b), (0, 0, 0), "after pop_layer, painted pixels should appear in base");
1134 }
1135
1136 #[test]
1141 fn test_lcd_gfx_ctx_push_layer_nests() {
1142 let mut buf = LcdBuffer::new(30, 30);
1143 {
1144 let mut ctx = LcdGfxCtx::new(&mut buf);
1145 ctx.clear(Color::white());
1146 ctx.translate(2.0, 2.0);
1147 ctx.push_layer(20.0, 20.0); ctx.set_fill_color(Color::black());
1149
1150 ctx.translate(4.0, 4.0);
1151 ctx.push_layer(8.0, 8.0); ctx.begin_path();
1153 ctx.rect(0.0, 0.0, 8.0, 8.0);
1154 ctx.fill();
1155 ctx.pop_layer(); ctx.pop_layer(); }
1159 let pixel = |x: usize, y: usize| -> (u8, u8, u8) {
1164 let i = (y * 30 + x) * 3;
1165 (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2])
1166 };
1167 assert_eq!(pixel(10, 10), (0, 0, 0), "centre of nested layer region should be dark");
1168 assert_eq!(pixel(2, 2), (255, 255, 255), "well outside nested region should stay white");
1169 assert_eq!(pixel(20, 20), (255, 255, 255), "well outside nested region should stay white");
1170 }
1171
1172 #[test]
1175 fn test_lcd_gfx_ctx_unmatched_pop_layer_is_noop() {
1176 let mut buf = LcdBuffer::new(8, 8);
1177 {
1178 let mut ctx = LcdGfxCtx::new(&mut buf);
1179 ctx.clear(Color::white());
1180 ctx.pop_layer(); ctx.set_fill_color(Color::black());
1182 ctx.begin_path();
1183 ctx.rect(0.0, 0.0, 8.0, 8.0);
1184 ctx.fill();
1185 }
1186 let i = (4 * 8 + 4) * 3;
1191 let (r, g, b) = (buf.color_plane()[i], buf.color_plane()[i + 1], buf.color_plane()[i + 2]);
1192 assert_eq!((r, g, b), (0, 0, 0), "subsequent paint after unmatched pop should still work");
1193 }
1194
1195 #[test]
1201 fn test_lcd_gfx_ctx_fill_text_honours_translation() {
1202 let f = font();
1203 let w = 100u32;
1204 let h = 24u32;
1205
1206 let mut buf_a = LcdBuffer::new(w, h);
1207 {
1208 let mut ctx = LcdGfxCtx::new(&mut buf_a);
1209 ctx.clear(Color::white());
1210 ctx.set_fill_color(Color::black());
1211 ctx.set_font(Arc::clone(&f));
1212 ctx.set_font_size(16.0);
1213 ctx.translate(10.0, 4.0);
1214 ctx.fill_text("Hi", 0.0, 12.0);
1215 }
1216
1217 let mut buf_b = LcdBuffer::new(w, h);
1218 {
1219 let mut ctx = LcdGfxCtx::new(&mut buf_b);
1220 ctx.clear(Color::white());
1221 ctx.set_fill_color(Color::black());
1222 ctx.set_font(f);
1223 ctx.set_font_size(16.0);
1224 ctx.fill_text("Hi", 10.0, 16.0);
1225 }
1226
1227 assert_eq!(buf_a.color_plane(), buf_b.color_plane(),
1228 "translate(10,4) + fill_text(0,12) must equal fill_text(10,16)");
1229 }
1230}