1use ailloli_ui_core::math::Scale;
4use ailloli_ui_core::Rect;
5use ailloli_ui_runtime::{DrawCmd, DrawPolyline, DrawText};
6
7use crate::frame_prep::PreparedResources;
8use crate::isolated_plan::IsolatedEffectChain;
9use crate::passes::primitives::text_origin_from_baseline;
10use crate::passes::to_ndc;
11
12pub const BLUR_PADDING_FACTOR: f32 = 3.0;
21
22pub fn union_cmd_bounds(cmds: &[DrawCmd], scale: Scale) -> Option<Rect> {
36 let mut acc: Option<Rect> = None;
37 for cmd in cmds {
38 let r = cmd_bounds(cmd, scale);
39 acc = Some(match acc {
40 None => r,
41 Some(a) => union_rect(a, r),
42 });
43 }
44 acc
45}
46
47pub fn union_cmd_bounds_prepared(
62 cmds: &[DrawCmd],
63 scale: Scale,
64 prepared: &PreparedResources,
65) -> Option<Rect> {
66 let mut acc: Option<Rect> = None;
67 for cmd in cmds {
68 let r = cmd_bounds_prepared(cmd, scale, prepared);
69 acc = Some(match acc {
70 None => r,
71 Some(a) => union_rect(a, r),
72 });
73 }
74 acc
75}
76
77fn cmd_bounds(cmd: &DrawCmd, scale: Scale) -> Rect {
79 match cmd {
80 DrawCmd::Rect(dr) => scale_rect(dr.rect, scale),
81 DrawCmd::RRect(rr) => scale_rect(rr.rect, scale),
82 DrawCmd::Border(border) => scale_rect(border.rect, scale),
83 DrawCmd::BoxShadow(shadow) => scale_rect(shadow.shadow.paint_bounds(shadow.rect), scale),
84 DrawCmd::RingProgress(ring) => scale_rect(ring.rect, scale),
85 DrawCmd::Polyline(polyline) => polyline_bounds(polyline, scale),
86 DrawCmd::Text(dt) => text_bounds(dt, scale),
87 DrawCmd::Image(img) => scale_rect(img.rect, scale),
88 }
89}
90
91fn cmd_bounds_prepared(cmd: &DrawCmd, scale: Scale, prepared: &PreparedResources) -> Rect {
93 match cmd {
94 DrawCmd::Text(dt) => {
95 text_bounds_prepared(dt, scale, prepared).unwrap_or_else(|| text_bounds(dt, scale))
96 }
97 _ => cmd_bounds(cmd, scale),
98 }
99}
100
101fn text_bounds(dt: &DrawText, scale: Scale) -> Rect {
103 let (ox, oy) = text_origin_from_baseline(dt);
104 let w = dt.layout.width();
105 let h = dt.layout.height();
106 let mut bounds = scale_rect(Rect::new(ox, oy, w, h), scale);
107 for decoration in dt.decoration_rects(scale.dpr) {
108 bounds = union_rect(bounds, scale_rect(decoration, scale));
109 }
110 bounds
111}
112
113fn text_bounds_prepared(dt: &DrawText, scale: Scale, prepared: &PreparedResources) -> Option<Rect> {
117 let scale_100 = (scale.dpr * 100.0).round().clamp(1.0, u16::MAX as f32) as u16;
118 let (origin_x, origin_y) = text_origin_from_baseline(dt);
119 let mut acc: Option<Rect> = None;
120 for gi in dt.layout.glyphs() {
121 let physical_px_size = ((gi.px_size as f32) * scale.dpr).round();
122 let key = crate::text::GlyphKey {
123 face_id: gi.face_id,
124 font_index: gi.font_index,
125 px_size: physical_px_size.clamp(8.0, 128.0) as u16,
126 glyph_id: gi.glyph_id,
127 scale_100,
128 };
129 let Some(&(_, g)) = prepared.glyphs.get(&key) else {
130 continue;
131 };
132 let pen_x = (origin_x + gi.x) * scale.dpr;
133 let pen_y = (origin_y + gi.y) * scale.dpr;
134 let x0 = pen_x + g.offset_px[0];
135 let y0 = pen_y + g.offset_px[1];
136 let r = Rect::new(x0, y0, g.size_px[0], g.size_px[1]);
137 acc = Some(match acc {
138 None => r,
139 Some(a) => union_rect(a, r),
140 });
141 }
142 for decoration in dt.decoration_rects(scale.dpr) {
143 let rect = scale_rect(decoration, scale);
144 acc = Some(match acc {
145 None => rect,
146 Some(bounds) => union_rect(bounds, rect),
147 });
148 }
149 acc
150}
151
152fn polyline_bounds(polyline: &DrawPolyline, scale: Scale) -> Rect {
156 let mut min_x = f32::INFINITY;
157 let mut min_y = f32::INFINITY;
158 let mut max_x = f32::NEG_INFINITY;
159 let mut max_y = f32::NEG_INFINITY;
160 for point in &polyline.points {
161 if !point.x.is_finite() || !point.y.is_finite() {
162 continue;
163 }
164 min_x = min_x.min(point.x);
165 min_y = min_y.min(point.y);
166 max_x = max_x.max(point.x);
167 max_y = max_y.max(point.y);
168 }
169
170 if !min_x.is_finite() {
171 return Rect::new(0.0, 0.0, 0.0, 0.0);
172 }
173
174 let pad = polyline.stroke.width.max(0.0) * 0.5 + 1.0;
175 scale_rect(
176 Rect::new(
177 min_x - pad,
178 min_y - pad,
179 (max_x - min_x) + pad * 2.0,
180 (max_y - min_y) + pad * 2.0,
181 ),
182 scale,
183 )
184}
185
186fn scale_rect(r: Rect, scale: Scale) -> Rect {
188 Rect::new(
189 r.x * scale.dpr,
190 r.y * scale.dpr,
191 r.w * scale.dpr,
192 r.h * scale.dpr,
193 )
194}
195
196fn union_rect(a: Rect, b: Rect) -> Rect {
198 let x0 = a.x.min(b.x);
199 let y0 = a.y.min(b.y);
200 let x1 = (a.x + a.w).max(b.x + b.w);
201 let y1 = (a.y + a.h).max(b.y + b.h);
202 Rect::new(x0, y0, x1 - x0, y1 - y0)
203}
204
205pub fn inflate_for_effects(bounds: Rect, effects: &IsolatedEffectChain) -> Rect {
221 let mut r = bounds;
222 for e in &effects.effects {
223 if let crate::isolated_plan::IsolatedEffect::Blur { radius_px } = e {
224 let pad = radius_px * BLUR_PADDING_FACTOR;
225 r = Rect::new(r.x - pad, r.y - pad, r.w + pad * 2.0, r.h + pad * 2.0);
226 }
227 }
228 r
229}
230
231pub fn snap_and_clamp_bounds(bounds: Rect, surface: [f32; 2]) -> (Rect, [f32; 2], [u32; 2]) {
250 let [sw, sh] = surface;
251 let x0 = bounds.x.floor().max(0.0);
252 let y0 = bounds.y.floor().max(0.0);
253 let x1 = (bounds.x + bounds.w).ceil().min(sw);
254 let y1 = (bounds.y + bounds.h).ceil().min(sh);
255 let w = (x1 - x0).max(1.0);
256 let h = (y1 - y0).max(1.0);
257 let snapped = Rect::new(x0, y0, w, h);
258 (snapped, [x0, y0], [w as u32, h as u32])
259}
260
261pub fn scissor_to_local(
277 scissor: Option<Rect>,
278 origin: [f32; 2],
279 local_size: [u32; 2],
280) -> Option<Rect> {
281 let s = scissor?;
282 let lw = local_size[0] as f32;
283 let lh = local_size[1] as f32;
284 let local = Rect::new(
285 (s.x - origin[0]).max(0.0),
286 (s.y - origin[1]).max(0.0),
287 s.w.min(lw),
288 s.h.min(lh),
289 );
290 if local.w <= 0.0 || local.h <= 0.0 {
291 return None;
292 }
293 Some(local)
294}
295
296pub fn push_composite_quad(
318 arena: &mut Vec<crate::vertices::TexVertex>,
319 surface: [f32; 2],
320 dest: Rect,
321 tint: [f32; 4],
322) -> std::ops::Range<u32> {
323 let [w, h] = surface;
324 let x0 = dest.x;
325 let y0 = dest.y;
326 let x1 = dest.x + dest.w;
327 let y1 = dest.y + dest.h;
328 let start = arena.len() as u32;
329 let p0 = to_ndc(w, h, x0, y0);
330 let p1 = to_ndc(w, h, x1, y0);
331 let p2 = to_ndc(w, h, x1, y1);
332 let p3 = to_ndc(w, h, x0, y1);
333 arena.extend_from_slice(&[
334 crate::vertices::TexVertex {
335 pos: p0,
336 uv: [0.0, 0.0],
337 tint,
338 },
339 crate::vertices::TexVertex {
340 pos: p1,
341 uv: [1.0, 0.0],
342 tint,
343 },
344 crate::vertices::TexVertex {
345 pos: p2,
346 uv: [1.0, 1.0],
347 tint,
348 },
349 crate::vertices::TexVertex {
350 pos: p0,
351 uv: [0.0, 0.0],
352 tint,
353 },
354 crate::vertices::TexVertex {
355 pos: p2,
356 uv: [1.0, 1.0],
357 tint,
358 },
359 crate::vertices::TexVertex {
360 pos: p3,
361 uv: [0.0, 1.0],
362 tint,
363 },
364 ]);
365 start..arena.len() as u32
366}
367
368pub fn push_composite_quad_local(
384 arena: &mut Vec<crate::vertices::TexVertex>,
385 local_surface: [f32; 2],
386 dest: Rect,
387 tint: [f32; 4],
388) -> std::ops::Range<u32> {
389 let [w, h] = local_surface;
390 let x0 = dest.x;
391 let y0 = dest.y;
392 let x1 = dest.x + dest.w;
393 let y1 = dest.y + dest.h;
394 let start = arena.len() as u32;
395 let p0 = to_ndc(w, h, x0, y0);
396 let p1 = to_ndc(w, h, x1, y0);
397 let p2 = to_ndc(w, h, x1, y1);
398 let p3 = to_ndc(w, h, x0, y1);
399 arena.extend_from_slice(&[
400 crate::vertices::TexVertex {
401 pos: p0,
402 uv: [0.0, 0.0],
403 tint,
404 },
405 crate::vertices::TexVertex {
406 pos: p1,
407 uv: [1.0, 0.0],
408 tint,
409 },
410 crate::vertices::TexVertex {
411 pos: p2,
412 uv: [1.0, 1.0],
413 tint,
414 },
415 crate::vertices::TexVertex {
416 pos: p0,
417 uv: [0.0, 0.0],
418 tint,
419 },
420 crate::vertices::TexVertex {
421 pos: p2,
422 uv: [1.0, 1.0],
423 tint,
424 },
425 crate::vertices::TexVertex {
426 pos: p3,
427 uv: [0.0, 1.0],
428 tint,
429 },
430 ]);
431 start..arena.len() as u32
432}
433
434#[cfg(test)]
435mod tests {
437 use super::*;
438 use crate::isolated_plan::{IsolatedEffect, IsolatedEffectChain};
439
440 #[test]
441 fn render_bounds_includes_blur_padding() {
442 let bounds = Rect::new(10.0, 10.0, 20.0, 20.0);
443 let effects = IsolatedEffectChain {
444 effects: vec![IsolatedEffect::Blur { radius_px: 8.0 }],
445 };
446 let inflated = inflate_for_effects(bounds, &effects);
447 let pad = 8.0 * BLUR_PADDING_FACTOR;
448 assert!((inflated.x - (bounds.x - pad)).abs() < 0.01);
449 assert!((inflated.w - (bounds.w + pad * 2.0)).abs() < 0.01);
450 }
451
452 #[test]
453 fn local_scissor_shifts_to_origin() {
454 let global = Rect::new(50.0, 50.0, 30.0, 30.0);
455 let local = scissor_to_local(Some(global), [40.0, 40.0], [128, 128]).unwrap();
456 assert!((local.x - 10.0).abs() < 0.01);
457 assert!((local.y - 10.0).abs() < 0.01);
458 }
459
460 #[test]
461 fn command_bounds_include_box_shadow_inflation() {
462 let cmds = vec![DrawCmd::BoxShadow(ailloli_ui_runtime::DrawBoxShadow {
463 rect: Rect::new(10.0, 20.0, 30.0, 10.0),
464 radius: ailloli_ui_core::Radius::uniform(4.0),
465 shadow: ailloli_ui_core::BoxShadow::new(
466 5.0,
467 -2.0,
468 3.0,
469 1.0,
470 ailloli_ui_core::Color::BLACK,
471 ),
472 })];
473
474 let bounds = union_cmd_bounds(&cmds, Scale::new(2.0)).expect("bounds");
475 assert_eq!(bounds, Rect::new(22.0, 28.0, 76.0, 36.0));
476 }
477
478 #[test]
479 fn command_bounds_include_ring_progress_rect() {
480 let cmds = vec![DrawCmd::RingProgress(
481 ailloli_ui_runtime::DrawRingProgress {
482 rect: Rect::new(10.0, 20.0, 30.0, 30.0),
483 thickness: 4.0,
484 fraction: 0.66,
485 track_color: ailloli_ui_core::Color::BLACK,
486 fill_color: ailloli_ui_core::Color::WHITE,
487 start_angle: -std::f32::consts::FRAC_PI_2,
488 },
489 )];
490
491 let bounds = union_cmd_bounds(&cmds, Scale::new(2.0)).expect("bounds");
492 assert_eq!(bounds, Rect::new(20.0, 40.0, 60.0, 60.0));
493 }
494
495 #[test]
496 fn command_bounds_include_polyline_width() {
497 let cmds = vec![DrawCmd::Polyline(ailloli_ui_runtime::DrawPolyline {
498 points: vec![
499 ailloli_ui_core::Point::new(10.0, 20.0),
500 ailloli_ui_core::Point::new(40.0, 30.0),
501 ],
502 stroke: ailloli_ui_core::StrokeStyle::new(4.0, ailloli_ui_core::Color::WHITE),
503 })];
504
505 let bounds = union_cmd_bounds(&cmds, Scale::new(2.0)).expect("bounds");
506 assert_eq!(bounds, Rect::new(14.0, 34.0, 72.0, 32.0));
507 }
508
509 #[test]
510 fn command_bounds_empty_polyline_is_zero() {
511 let cmds = vec![DrawCmd::Polyline(ailloli_ui_runtime::DrawPolyline {
512 points: vec![ailloli_ui_core::Point::new(f32::NAN, 20.0)],
513 stroke: ailloli_ui_core::StrokeStyle::new(4.0, ailloli_ui_core::Color::WHITE),
514 })];
515
516 let bounds = union_cmd_bounds(&cmds, Scale::new(2.0)).expect("bounds");
517 assert_eq!(bounds, Rect::new(0.0, 0.0, 0.0, 0.0));
518 }
519}