Skip to main content

par_term_render/graphics_renderer/
layout.rs

1//! Graphics placement and scaling calculations.
2//!
3//! Extracted from the graphics_renderer.rs root per ARC-009: pure geometry
4//! with no renderer state, tested in isolation.
5
6/// Window and pane geometry for a single [`GraphicsRenderer::render_for_pane`] call.
7#[derive(Debug, Clone, Copy)]
8pub struct PaneRenderGeometry {
9    pub window_width: f32,
10    pub window_height: f32,
11    pub pane_origin_x: f32,
12    pub pane_origin_y: f32,
13}
14
15/// Compute UV coordinates and display size for an inline graphic instance.
16///
17/// Maps the source crop rectangle to the destination cell extent, applying
18/// scroll clipping in destination space (so a scrolled row removes the same
19/// fraction of source and destination). Returns `(tex_coords, size)`.
20#[allow(clippy::too_many_arguments)]
21pub(super) fn compute_graphic_geometry(
22    tex_w: f32,
23    tex_h: f32,
24    crop: [u32; 4],
25    width_cells: usize,
26    height_cells: usize,
27    cell_w: f32,
28    cell_h: f32,
29    clip_px: f32,
30    has_cols: bool,
31    has_rows: bool,
32    preserve_aspect: bool,
33    is_virtual: bool,
34    window_w: f32,
35    window_h: f32,
36) -> ([f32; 4], [f32; 2]) {
37    let has_crop = crop != [0, 0, 0, 0];
38
39    // Effective source rectangle (zero extents normalize to texture edges).
40    let (sx, sy, sw, sh) = if has_crop && tex_w > 0.0 && tex_h > 0.0 {
41        let x = (crop[0] as f32).min(tex_w);
42        let y = (crop[1] as f32).min(tex_h);
43        let w = if crop[2] > 0 {
44            (crop[2] as f32).min(tex_w - x)
45        } else {
46            tex_w - x
47        };
48        let h = if crop[3] > 0 {
49            (crop[3] as f32).min(tex_h - y)
50        } else {
51            tex_h - y
52        };
53        (x, y, w.max(0.0), h.max(0.0))
54    } else {
55        (0.0, 0.0, tex_w, tex_h)
56    };
57
58    // Un-clipped destination size in pixels, chosen per axis:
59    // - Virtual / both c+r: exact cell rectangle.
60    // - c-only: cell width × aspect-derived exact height (no cell rounding).
61    // - r-only: cell height × aspect-derived exact width.
62    // - neither: natural source crop size (or full texture when
63    //   preserve_aspect, else cell-derived fallback).
64    // Empty crop intersection (crop at the texture edge yields sw/sh=0)
65    // produces nothing to draw; return zero size immediately.
66    if sw <= 0.0 || sh <= 0.0 {
67        return ([0.0, 0.0, 0.0, 0.0], [0.0, 0.0]);
68    }
69    let aspect = sw / sh;
70    let (dest_w, dest_h) = if is_virtual || (has_cols && has_rows) {
71        (width_cells as f32 * cell_w, height_cells as f32 * cell_h)
72    } else if has_cols && !has_rows {
73        let dw = width_cells as f32 * cell_w;
74        (dw, dw / aspect)
75    } else if has_rows && !has_cols {
76        let dh = height_cells as f32 * cell_h;
77        (dh * aspect, dh)
78    } else if has_crop && sw > 0.0 && sh > 0.0 {
79        (sw, sh)
80    } else if preserve_aspect && tex_w > 0.0 && tex_h > 0.0 {
81        (tex_w, tex_h)
82    } else {
83        (width_cells as f32 * cell_w, height_cells as f32 * cell_h)
84    };
85
86    // Destination clip fraction.
87    let visible_frac = if dest_h > 0.0 {
88        ((dest_h - clip_px) / dest_h).clamp(0.0, 1.0)
89    } else {
90        0.0
91    };
92    let scrolled_frac = if dest_h > 0.0 {
93        (clip_px / dest_h).clamp(0.0, 1.0)
94    } else {
95        0.0
96    };
97
98    // UV: map scrolled/visible destination fractions onto the source rect.
99    let uv = if sw > 0.0 && sh > 0.0 && tex_w > 0.0 && tex_h > 0.0 {
100        [
101            sx / tex_w,
102            (sy + sh * scrolled_frac) / tex_h,
103            sw / tex_w,
104            (sh * visible_frac) / tex_h,
105        ]
106    } else {
107        [0.0, 0.0, 1.0, 1.0]
108    };
109
110    let size = (dest_w / window_w, dest_h * visible_frac / window_h);
111
112    (uv, size.into())
113}
114
115#[cfg(test)]
116mod geometry_tests {
117    use super::compute_graphic_geometry;
118
119    const WW: f32 = 800.0;
120    const WH: f32 = 600.0;
121    const CW: f32 = 10.0;
122    const CH: f32 = 20.0;
123
124    /// 100px source in 40px dest (r=2), scrolled 1 row (20px):
125    /// clip fraction 0.5, UV starts at pixel 50, 50px visible.
126    #[test]
127    fn no_crop_both_cells_uses_dest_fraction_for_uv() {
128        let (uv, size) = compute_graphic_geometry(
129            100.0,
130            100.0,
131            [0, 0, 0, 0],
132            10,
133            2,
134            CW,
135            CH,
136            20.0, // clip_px
137            true,
138            true, // has_cols, has_rows
139            false,
140            false, // preserve_aspect, is_virtual
141            WW,
142            WH,
143        );
144        let expected_uv_y = 50.0 / 100.0;
145        let expected_uv_h = 50.0 / 100.0;
146        assert!((uv[1] - expected_uv_y).abs() < 1e-5);
147        assert!((uv[3] - expected_uv_h).abs() < 1e-5);
148        assert!((size[1] - 20.0 / WH).abs() < 1e-5);
149    }
150
151    /// 25px source crop, no c/r, scrolled 20px: dest_h=25, 5px visible.
152    #[test]
153    fn natural_crop_without_cells_uses_crop_height_for_dest() {
154        let (uv, size) = compute_graphic_geometry(
155            100.0,
156            100.0,
157            [0, 0, 0, 25],
158            1,
159            1,
160            CW,
161            CH,
162            20.0,
163            false,
164            false,
165            false,
166            false,
167            WW,
168            WH,
169        );
170        let expected_uv_y = (0.0 + 25.0 * 0.8) / 100.0;
171        let expected_uv_h = (25.0 * 0.2) / 100.0;
172        assert!((uv[1] - expected_uv_y).abs() < 1e-5);
173        assert!((uv[3] - expected_uv_h).abs() < 1e-5);
174        assert!((size[1] - 5.0 / WH).abs() < 1e-5);
175    }
176
177    /// row=-1, Y=5: clip 15px, UV reflects 15/60 scrolled fraction.
178    #[test]
179    fn y_offset_produces_sub_row_clip() {
180        let top_px = -CH + 5.0;
181        let clip_px = (-top_px).max(0.0);
182        assert_eq!(clip_px, 15.0);
183
184        let (uv, size) = compute_graphic_geometry(
185            100.0,
186            100.0,
187            [0, 0, 0, 0],
188            10,
189            3,
190            CW,
191            CH,
192            clip_px,
193            true,
194            true,
195            false,
196            false,
197            WW,
198            WH,
199        );
200        assert!((uv[1] - 25.0 / 100.0).abs() < 1e-5);
201        assert!((uv[3] - 75.0 / 100.0).abs() < 1e-5);
202        assert!((size[1] - 45.0 / WH).abs() < 1e-5);
203    }
204
205    /// 100×100 source, c=5 only, cell 10×20: dest_w=50, dest_h=50 (aspect 1:1).
206    #[test]
207    fn c_only_computes_exact_height_from_aspect() {
208        let (_uv, size) = compute_graphic_geometry(
209            100.0,
210            100.0,
211            [0, 0, 0, 0],
212            5,
213            3,
214            CW,
215            CH,
216            0.0,
217            true,
218            false, // has_cols only
219            false,
220            false,
221            WW,
222            WH,
223        );
224        // dest_h = 50px / 600px (aspect-derived, not cell-rounded)
225        assert!((size[0] - 50.0 / WW).abs() < 1e-5);
226        assert!((size[1] - 50.0 / WH).abs() < 1e-5);
227    }
228
229    /// 100×100 source, r=2 only, cell 10×20: dest_h=40, dest_w=40 (aspect 1:1).
230    #[test]
231    fn r_only_computes_exact_width_from_aspect() {
232        let (_uv, size) = compute_graphic_geometry(
233            100.0,
234            100.0,
235            [0, 0, 0, 0],
236            4,
237            2,
238            CW,
239            CH,
240            0.0,
241            false,
242            true, // has_rows only
243            false,
244            false,
245            WW,
246            WH,
247        );
248        assert!((size[0] - 40.0 / WW).abs() < 1e-5);
249        assert!((size[1] - 40.0 / WH).abs() < 1e-5);
250    }
251
252    /// 100×50 source (2:1), c=5, cell 10×20: dest_w=50, dest_h=25.
253    #[test]
254    fn c_only_wide_source_computes_proportional_height() {
255        let (_uv, size) = compute_graphic_geometry(
256            100.0,
257            50.0,
258            [0, 0, 0, 0],
259            5,
260            1,
261            CW,
262            CH,
263            0.0,
264            true,
265            false,
266            false,
267            false,
268            WW,
269            WH,
270        );
271        assert!((size[0] - 50.0 / WW).abs() < 1e-5);
272        assert!((size[1] - 25.0 / WH).abs() < 1e-5);
273    }
274
275    /// Crop at the texture edge (source_x=100 on 100px image) yields sw=0.
276    /// Zero-size intersection must return zero output, not a full-image
277    /// fallback or NaN from aspect division.
278    #[test]
279    fn zero_size_crop_at_edge_returns_zero_output() {
280        let (uv, size) = compute_graphic_geometry(
281            100.0,
282            100.0,
283            [100, 0, 0, 0],
284            5,
285            3,
286            CW,
287            CH,
288            0.0,
289            true,
290            false,
291            false,
292            false,
293            WW,
294            WH,
295        );
296        // Zero crop → zero UV and zero size
297        assert_eq!(uv, [0.0, 0.0, 0.0, 0.0]);
298        assert_eq!(size, [0.0, 0.0]);
299    }
300}