Skip to main content

telar_renderer_core/
culling.rs

1use geometry_core::{Rect, Transform};
2
3use crate::DrawCommand;
4use crate::transform_clip_rect;
5
6/// Font ascender/line-height metrics expressed as ratios relative to `font_size`.
7/// Default values are conservative approximations that hold for most common fonts.
8#[derive(Clone, Copy)]
9pub struct FontMetrics {
10    /// Multiplier for line height: `font_size * line_height_factor` gives the full line height.
11    pub line_height_factor: f32,
12    /// Fraction of `font_size` by which glyphs can extend above the rect's top edge (ascender overshoot).
13    pub ascender_ratio: f32,
14}
15
16impl Default for FontMetrics {
17    fn default() -> Self {
18        Self {
19            line_height_factor: 1.2,
20            ascender_ratio: 0.25,
21        }
22    }
23}
24
25pub fn overlaps(x: f32, y: f32, w: f32, h: f32, clip: Option<Rect>) -> bool {
26    match clip {
27        None => true,
28        Some(c) => Rect::new(x, y, w, h).overlaps(c),
29    }
30}
31
32pub fn expand_for_shadow(
33    rect: Rect,
34    blur_radius: f32,
35    spread: f32,
36    offset_x: f32,
37    offset_y: f32,
38) -> Rect {
39    // Through the renderers' own padding: a smaller expansion here leaves stale shadow pixels outside the dirty rect when a shadow moves.
40    let expand =
41        crate::preprocess::blur_padding(crate::preprocess::blur_sigma(blur_radius)) as f32 + spread;
42    let expanded = Rect::new(
43        rect.x - expand,
44        rect.y - expand,
45        rect.width + expand * 2.0,
46        rect.height + expand * 2.0,
47    );
48    let shifted = Rect::new(
49        expanded.x + offset_x,
50        expanded.y + offset_y,
51        expanded.width,
52        expanded.height,
53    );
54    rect.union(shifted)
55}
56
57pub fn command_visual_rect(
58    cmd: &DrawCommand,
59    matrix: [f32; 6],
60    font_metrics: &FontMetrics,
61) -> Option<Rect> {
62    match cmd {
63        DrawCommand::Rect { rect, style } => {
64            let r = transform_clip_rect(matrix, *rect);
65            let shadow = style.shadow;
66            Some(match shadow {
67                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
68                None => r,
69            })
70        }
71        DrawCommand::Text { rect, style, .. } => {
72            // Glyphs can extend outside rect: ascenders above rect.y and the line height may exceed rect.height. Expand the visual rect to cover the real glyph extent so that dirty-rect computation and culling never under-estimate the painted area.
73            let font_size = style.font_size;
74            let shadow = style.shadow;
75            let line_h = font_size * font_metrics.line_height_factor;
76            let ascender_overshoot = font_size * font_metrics.ascender_ratio;
77            let extra_bottom = (line_h - rect.height).max(0.0);
78            let r = transform_clip_rect(
79                matrix,
80                Rect::new(
81                    rect.x,
82                    rect.y - ascender_overshoot,
83                    rect.width,
84                    rect.height + ascender_overshoot + extra_bottom,
85                ),
86            );
87            Some(match shadow {
88                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
89                None => r,
90            })
91        }
92        DrawCommand::RichText { rect, base, .. } => {
93            // Same glyph overshoot as `Text`, driven by the paragraph's base metrics.
94            let font_size = base.font_size;
95            let line_h = font_size * font_metrics.line_height_factor;
96            let ascender_overshoot = font_size * font_metrics.ascender_ratio;
97            let extra_bottom = (line_h - rect.height).max(0.0);
98            let r = transform_clip_rect(
99                matrix,
100                Rect::new(
101                    rect.x,
102                    rect.y - ascender_overshoot,
103                    rect.width,
104                    rect.height + ascender_overshoot + extra_bottom,
105                ),
106            );
107            Some(match base.shadow {
108                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
109                None => r,
110            })
111        }
112        DrawCommand::Image { rect, .. } => Some(transform_clip_rect(matrix, *rect)),
113        DrawCommand::Line { p1, p2, style } => {
114            let half_w = style.width / 2.0;
115            let t = Transform::from_array(matrix);
116            let m1 = t.apply(*p1);
117            let m2 = t.apply(*p2);
118            let x = m1.x.min(m2.x) - half_w;
119            let y = m1.y.min(m2.y) - half_w;
120            let right = m1.x.max(m2.x) + half_w;
121            let bottom = m1.y.max(m2.y) + half_w;
122            Some(Rect::new(x, y, right - x, bottom - y))
123        }
124        DrawCommand::Path { data, style } => {
125            let base = data.bounds()?;
126            let r = transform_clip_rect(matrix, base);
127            let shadow = style.shadow;
128            Some(match shadow {
129                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
130                None => r,
131            })
132        }
133        DrawCommand::PushClip { .. }
134        | DrawCommand::PopClip
135        | DrawCommand::PushMatrix { .. }
136        | DrawCommand::PopMatrix
137        | DrawCommand::PushLayer { .. }
138        | DrawCommand::PopLayer => None,
139    }
140}
141
142pub fn extend_bounds(current: Option<Rect>, new_rect: Rect) -> Option<Rect> {
143    Some(current.map_or(new_rect, |b| b.union(new_rect)))
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn overlaps_no_clip() {
152        assert!(overlaps(0.0, 0.0, 10.0, 10.0, None));
153    }
154
155    #[test]
156    fn overlaps_inside_clip() {
157        let clip = Rect::new(0.0, 0.0, 100.0, 100.0);
158        assert!(overlaps(10.0, 10.0, 20.0, 20.0, Some(clip)));
159    }
160
161    #[test]
162    fn overlaps_outside_clip() {
163        let clip = Rect::new(0.0, 0.0, 10.0, 10.0);
164        assert!(!overlaps(20.0, 20.0, 5.0, 5.0, Some(clip)));
165    }
166
167    #[test]
168    fn expand_for_shadow_expands_all_sides() {
169        let r = Rect::new(10.0, 10.0, 20.0, 20.0);
170        let result = expand_for_shadow(r, 5.0, 2.0, 0.0, 0.0);
171        assert!(result.x < r.x);
172        assert!(result.y < r.y);
173        assert!(result.x + result.width > r.x + r.width);
174        assert!(result.y + result.height > r.y + r.height);
175    }
176
177    #[test]
178    fn transform_clip_rect_identity() {
179        let r = transform_clip_rect(
180            Transform::IDENTITY.to_array(),
181            Rect::new(10.0, 20.0, 30.0, 40.0),
182        );
183        assert_eq!(r.x, 10.0);
184        assert_eq!(r.y, 20.0);
185        assert_eq!(r.width, 30.0);
186        assert_eq!(r.height, 40.0);
187    }
188
189    #[test]
190    fn transform_clip_rect_scale() {
191        let scale = [2.0, 0.0, 0.0, 2.0, 0.0, 0.0];
192        let r = transform_clip_rect(scale, Rect::new(5.0, 5.0, 10.0, 10.0));
193        assert!((r.x - 10.0).abs() < 1e-4);
194        assert!((r.y - 10.0).abs() < 1e-4);
195        assert!((r.width - 20.0).abs() < 1e-4);
196        assert!((r.height - 20.0).abs() < 1e-4);
197    }
198}