Skip to main content

densa_draw/
canvas.rs

1use crate::{Instance, InstanceBuffer};
2use densa_core::{Color, Point, Rect};
3use densa_text::{quantize_x, Font, GlyphCache, GlyphId, GlyphRun};
4
5/// How far from its pen a glyph's ink can reach, in ems: generous, so only glyphs certainly
6/// outside the clip are skipped unseen.
7const GLYPH_REACH_EMS: f32 = 4.0;
8
9/// One frame being drawn: everything painters emit goes through it.
10///
11/// [`Canvas::begin`] starts the frame on both the instance list and the glyph cache, so the
12/// cache's "used this frame" protection spans every painter of the frame: a glyph drawn by
13/// one painter cannot be evicted by the next. One glyph cache serves every painter of a
14/// frame loop and its updates feed one GPU atlas (`densa_render::GpuRenderer`).
15///
16/// Positions go through a translation and are clipped to a rectangle (see
17/// [`translate`](Self::translate) and [`clip`](Self::clip)); [`state`](Self::state) and
18/// [`restore`](Self::restore) save and bring back both, as a widget does around its children.
19/// Clipping happens here, on each rectangle and glyph, so a frame stays one draw call.
20///
21/// Allocation-free once the frame's glyphs are cached.
22#[derive(Debug)]
23pub struct Canvas<'a> {
24    cache: &'a mut GlyphCache,
25    out: &'a mut InstanceBuffer,
26    state: CanvasState,
27}
28
29/// A canvas's translation and clip, saved with [`Canvas::state`] and brought back with
30/// [`Canvas::restore`].
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct CanvasState {
33    /// Added to every position drawn.
34    offset: Point,
35    /// Where drawing shows, in frame coordinates (after the offset).
36    clip: Rect,
37}
38
39impl<'a> Canvas<'a> {
40    /// Starts a frame: clears `out` and begins a frame on `cache`.
41    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    /// The current translation and clip, to [`restore`](Self::restore) later.
55    pub fn state(&self) -> CanvasState {
56        self.state
57    }
58
59    /// Brings back a translation and clip saved with [`state`](Self::state).
60    pub fn restore(&mut self, state: CanvasState) {
61        self.state = state;
62    }
63
64    /// Moves everything drawn from now on by `dx`, `dy`.
65    pub fn translate(&mut self, dx: f32, dy: f32) {
66        self.state.offset = self.state.offset.offset(dx, dy);
67    }
68
69    /// Draws from now on only inside `rect` (in current coordinates), within the current
70    /// clip.
71    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    /// The area drawing still shows in, in current coordinates; empty when everything is
77    /// clipped away.
78    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    /// A filled rectangle.
85    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    /// `glyph` of `font` at `px` pixels per em, its pen at `at`: `x` is the pen position
93    /// (fractional: drawn at the nearest subpixel bin), `y` the baseline (rounded to a whole
94    /// pixel).
95    ///
96    /// Glyphs the cache cannot provide this frame are skipped (counted in its stats).
97    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        // A glyph lies within a few ems of its pen: one far outside the clip is skipped before
100        // the cache would rasterize and store it.
101        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; // No ink, e.g. a space.
112        }
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    /// Every glyph of `run`, offset by `origin`.
128    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    /// The glyph cache this frame draws through.
136    pub fn cache(&self) -> &GlyphCache {
137        self.cache
138    }
139
140    /// The instances emitted so far this frame.
141    pub fn instances(&self) -> &InstanceBuffer {
142        self.out
143    }
144}
145
146/// The part of atlas rectangle `uv` (`[x0, y0, x1, y1]`, mapped linearly onto `quad`) that
147/// `shown`, a part of `quad`, covers.
148fn 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}