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    // Must match the software renderer's blur padding (ceil(blur_radius * 1.5) + 1); using only blur_radius leaves stale shadow pixels outside the dirty rect when shadows move.
40    let expand = (blur_radius * 1.5).ceil() + 1.0 + spread;
41    let expanded = Rect::new(
42        rect.x - expand,
43        rect.y - expand,
44        rect.width + expand * 2.0,
45        rect.height + expand * 2.0,
46    );
47    let shifted = Rect::new(
48        expanded.x + offset_x,
49        expanded.y + offset_y,
50        expanded.width,
51        expanded.height,
52    );
53    rect.union(shifted)
54}
55
56pub fn command_visual_rect(
57    cmd: &DrawCommand,
58    matrix: [f32; 6],
59    font_metrics: &FontMetrics,
60) -> Option<Rect> {
61    match cmd {
62        DrawCommand::Rect { rect, style } => {
63            let r = transform_clip_rect(matrix, *rect);
64            let shadow = style.shadow;
65            Some(match shadow {
66                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
67                None => r,
68            })
69        }
70        DrawCommand::Text { rect, style, .. } => {
71            // 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.
72            let font_size = style.font_size;
73            let shadow = style.shadow;
74            let line_h = font_size * font_metrics.line_height_factor;
75            let ascender_overshoot = font_size * font_metrics.ascender_ratio;
76            let extra_bottom = (line_h - rect.height).max(0.0);
77            let r = transform_clip_rect(
78                matrix,
79                Rect::new(
80                    rect.x,
81                    rect.y - ascender_overshoot,
82                    rect.width,
83                    rect.height + ascender_overshoot + extra_bottom,
84                ),
85            );
86            Some(match shadow {
87                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
88                None => r,
89            })
90        }
91        DrawCommand::RichText { rect, base, .. } => {
92            // Same glyph overshoot as `Text`, driven by the paragraph's base metrics.
93            let font_size = base.font_size;
94            let line_h = font_size * font_metrics.line_height_factor;
95            let ascender_overshoot = font_size * font_metrics.ascender_ratio;
96            let extra_bottom = (line_h - rect.height).max(0.0);
97            let r = transform_clip_rect(
98                matrix,
99                Rect::new(
100                    rect.x,
101                    rect.y - ascender_overshoot,
102                    rect.width,
103                    rect.height + ascender_overshoot + extra_bottom,
104                ),
105            );
106            Some(match base.shadow {
107                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
108                None => r,
109            })
110        }
111        DrawCommand::Image { rect, .. } => Some(transform_clip_rect(matrix, *rect)),
112        DrawCommand::Line { p1, p2, style } => {
113            let half_w = style.width / 2.0;
114            let t = Transform::from_array(matrix);
115            let m1 = t.apply(*p1);
116            let m2 = t.apply(*p2);
117            let x = m1.x.min(m2.x) - half_w;
118            let y = m1.y.min(m2.y) - half_w;
119            let right = m1.x.max(m2.x) + half_w;
120            let bottom = m1.y.max(m2.y) + half_w;
121            Some(Rect::new(x, y, right - x, bottom - y))
122        }
123        DrawCommand::Path { data, style } => {
124            let base = data.bounds()?;
125            let r = transform_clip_rect(matrix, base);
126            let shadow = style.shadow;
127            Some(match shadow {
128                Some(s) => expand_for_shadow(r, s.blur_radius, s.spread, s.offset_x, s.offset_y),
129                None => r,
130            })
131        }
132        DrawCommand::PushClip { .. }
133        | DrawCommand::PopClip
134        | DrawCommand::PushMatrix { .. }
135        | DrawCommand::PopMatrix
136        | DrawCommand::PushLayer { .. }
137        | DrawCommand::PopLayer => None,
138    }
139}
140
141pub fn extend_bounds(current: Option<Rect>, new_rect: Rect) -> Option<Rect> {
142    Some(current.map_or(new_rect, |b| b.union(new_rect)))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn overlaps_no_clip() {
151        assert!(overlaps(0.0, 0.0, 10.0, 10.0, None));
152    }
153
154    #[test]
155    fn overlaps_inside_clip() {
156        let clip = Rect::new(0.0, 0.0, 100.0, 100.0);
157        assert!(overlaps(10.0, 10.0, 20.0, 20.0, Some(clip)));
158    }
159
160    #[test]
161    fn overlaps_outside_clip() {
162        let clip = Rect::new(0.0, 0.0, 10.0, 10.0);
163        assert!(!overlaps(20.0, 20.0, 5.0, 5.0, Some(clip)));
164    }
165
166    #[test]
167    fn expand_for_shadow_expands_all_sides() {
168        let r = Rect::new(10.0, 10.0, 20.0, 20.0);
169        let result = expand_for_shadow(r, 5.0, 2.0, 0.0, 0.0);
170        assert!(result.x < r.x);
171        assert!(result.y < r.y);
172        assert!(result.x + result.width > r.x + r.width);
173        assert!(result.y + result.height > r.y + r.height);
174    }
175
176    #[test]
177    fn transform_clip_rect_identity() {
178        let r = transform_clip_rect(
179            Transform::IDENTITY.to_array(),
180            Rect::new(10.0, 20.0, 30.0, 40.0),
181        );
182        assert_eq!(r.x, 10.0);
183        assert_eq!(r.y, 20.0);
184        assert_eq!(r.width, 30.0);
185        assert_eq!(r.height, 40.0);
186    }
187
188    #[test]
189    fn transform_clip_rect_scale() {
190        let scale = [2.0, 0.0, 0.0, 2.0, 0.0, 0.0];
191        let r = transform_clip_rect(scale, Rect::new(5.0, 5.0, 10.0, 10.0));
192        assert!((r.x - 10.0).abs() < 1e-4);
193        assert!((r.y - 10.0).abs() < 1e-4);
194        assert!((r.width - 20.0).abs() < 1e-4);
195        assert!((r.height - 20.0).abs() < 1e-4);
196    }
197}