1use crate::{Instance, InstanceBuffer};
2use densa_core::{Color, Point, Rect};
3use densa_text::{quantize_x, Font, GlyphCache, GlyphId, GlyphRun};
4
5const GLYPH_REACH_EMS: f32 = 4.0;
8
9#[derive(Debug)]
23pub struct Canvas<'a> {
24 cache: &'a mut GlyphCache,
25 out: &'a mut InstanceBuffer,
26 state: CanvasState,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct CanvasState {
33 offset: Point,
35 clip: Rect,
37}
38
39impl<'a> Canvas<'a> {
40 pub fn begin(cache: &'a mut GlyphCache, out: &'a mut InstanceBuffer) -> Self {
42 cache.begin_frame();
43 out.clear();
44 Self {
45 cache,
46 out,
47 state: CanvasState {
48 offset: Point::ORIGIN,
49 clip: Rect::EVERYTHING,
50 },
51 }
52 }
53
54 pub fn state(&self) -> CanvasState {
56 self.state
57 }
58
59 pub fn restore(&mut self, state: CanvasState) {
61 self.state = state;
62 }
63
64 pub fn translate(&mut self, dx: f32, dy: f32) {
66 self.state.offset = self.state.offset.offset(dx, dy);
67 }
68
69 pub fn clip(&mut self, rect: Rect) {
72 let rect = rect.translate(self.state.offset);
73 self.state.clip = self.state.clip.intersect(rect);
74 }
75
76 pub fn clip_rect(&self) -> Rect {
79 let offset = self.state.offset;
80 let back = Point::new(-offset.x, -offset.y);
81 self.state.clip.translate(back)
82 }
83
84 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
86 let rect = rect.translate(self.state.offset).intersect(self.state.clip);
87 if !rect.is_empty() {
88 self.out.push(Instance::solid(rect.to_xywh(), color));
89 }
90 }
91
92 pub fn draw_glyph(&mut self, font: &Font, glyph: GlyphId, px: f32, at: Point, color: Color) {
98 let at = at.offset(self.state.offset.x, self.state.offset.y);
99 let reach = GLYPH_REACH_EMS * px;
102 let near = Rect::from_edges(at.x - reach, at.y - reach, at.x + reach, at.y + reach);
103 if near.intersect(self.state.clip).is_empty() {
104 return;
105 }
106 let (pixel, bin) = quantize_x(at.x);
107 let Ok(cached) = self.cache.get(font, glyph, px, bin) else {
108 return;
109 };
110 if cached.size[0] == 0.0 {
111 return; }
113 let quad = Rect::new(
114 pixel as f32 + cached.offset[0],
115 at.y.round() + cached.offset[1],
116 cached.size[0],
117 cached.size[1],
118 );
119 let shown = quad.intersect(self.state.clip);
120 if shown.is_empty() {
121 return;
122 }
123 let uv = clipped_uv(quad, shown, cached.atlas_rect);
124 self.out.push(Instance::glyph(shown.to_xywh(), uv, color));
125 }
126
127 pub fn draw_run(&mut self, run: GlyphRun<'_>, origin: Point) {
129 for glyph in run.glyphs {
130 let at = origin.offset(glyph.x, glyph.y);
131 self.draw_glyph(run.font, glyph.id, run.px, at, run.color);
132 }
133 }
134
135 pub fn cache(&self) -> &GlyphCache {
137 self.cache
138 }
139
140 pub fn instances(&self) -> &InstanceBuffer {
142 self.out
143 }
144}
145
146fn clipped_uv(quad: Rect, shown: Rect, uv: [f32; 4]) -> [f32; 4] {
149 if shown == quad {
150 return uv;
151 }
152 let along = |edge: f32, from: f32, length: f32, start: f32, span: f32| {
153 start + (edge - from) / length * span
154 };
155 let (du, dv) = (uv[2] - uv[0], uv[3] - uv[1]);
156 [
157 along(shown.left(), quad.left(), quad.width(), uv[0], du),
158 along(shown.top(), quad.top(), quad.height(), uv[1], dv),
159 along(shown.right(), quad.left(), quad.width(), uv[0], du),
160 along(shown.bottom(), quad.top(), quad.height(), uv[1], dv),
161 ]
162}