1use std::collections::HashMap;
4use std::sync::Arc;
5
6use ailloli_ui_core::math::{snap_rect_to_physical, Scale};
7use ailloli_ui_core::{Color, Rect};
8use ailloli_ui_runtime::{
9 DrawBorder, DrawBoxShadow, DrawImage, DrawRRect, DrawRingProgress, DrawText,
10};
11
12use crate::text::{GlyphKey, TextAtlas};
13use crate::vertices::{
14 BorderRRectVertex, BoxShadowVertex, RRectVertex, RingProgressVertex, TexVertex, Vertex,
15};
16
17pub fn now_ms() -> u128 {
30 use std::time::{SystemTime, UNIX_EPOCH};
31 SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .unwrap_or_default()
34 .as_millis()
35}
36
37pub fn to_ndc(w: f32, h: f32, x: f32, y: f32) -> [f32; 2] {
50 let nx = (x / w) * 2.0 - 1.0;
51 let ny = 1.0 - (y / h) * 2.0;
52 [nx, ny]
53}
54
55fn scale_rect_to_physical(r: Rect, scale: Scale) -> Rect {
57 Rect::new(
58 r.x * scale.dpr,
59 r.y * scale.dpr,
60 r.w * scale.dpr,
61 r.h * scale.dpr,
62 )
63}
64
65pub fn push_rect(out: &mut Vec<Vertex>, w: f32, h: f32, r: Rect, c: Color) {
80 push_rect_scaled(out, w, h, Scale::new(1.0), r, c);
81}
82
83pub fn push_rect_scaled(out: &mut Vec<Vertex>, w: f32, h: f32, scale: Scale, r: Rect, c: Color) {
98 let r = scale_rect_to_physical(r, scale);
99 let x0 = r.x;
100 let y0 = r.y;
101 let x1 = r.x + r.w;
102 let y1 = r.y + r.h;
103
104 let p0 = to_ndc(w, h, x0, y0);
105 let p1 = to_ndc(w, h, x1, y0);
106 let p2 = to_ndc(w, h, x1, y1);
107 let p3 = to_ndc(w, h, x0, y1);
108
109 let color = c.to_array();
110
111 out.push(Vertex { pos: p0, color });
112 out.push(Vertex { pos: p1, color });
113 out.push(Vertex { pos: p2, color });
114 out.push(Vertex { pos: p0, color });
115 out.push(Vertex { pos: p2, color });
116 out.push(Vertex { pos: p3, color });
117}
118
119pub fn make_tex_rect(w: f32, h: f32, img: DrawImage) -> [TexVertex; 6] {
129 make_tex_rect_scaled(w, h, Scale::new(1.0), img)
130}
131
132pub fn make_tex_rect_scaled(w: f32, h: f32, scale: Scale, img: DrawImage) -> [TexVertex; 6] {
148 let mut img = img;
149 img.rect = scale_rect_to_physical(img.rect, scale);
150 let x0 = img.rect.x;
151 let y0 = img.rect.y;
152 let x1 = img.rect.x + img.rect.w;
153 let y1 = img.rect.y + img.rect.h;
154
155 let p0 = to_ndc(w, h, x0, y0);
156 let p1 = to_ndc(w, h, x1, y0);
157 let p2 = to_ndc(w, h, x1, y1);
158 let p3 = to_ndc(w, h, x0, y1);
159
160 let uv0 = [0.0, 0.0];
161 let uv1 = [1.0, 0.0];
162 let uv2 = [1.0, 1.0];
163 let uv3 = [0.0, 1.0];
164
165 let tint = [img.tint.r, img.tint.g, img.tint.b, img.tint.a];
166
167 [
168 TexVertex {
169 pos: p0,
170 uv: uv0,
171 tint,
172 },
173 TexVertex {
174 pos: p1,
175 uv: uv1,
176 tint,
177 },
178 TexVertex {
179 pos: p2,
180 uv: uv2,
181 tint,
182 },
183 TexVertex {
184 pos: p0,
185 uv: uv0,
186 tint,
187 },
188 TexVertex {
189 pos: p2,
190 uv: uv2,
191 tint,
192 },
193 TexVertex {
194 pos: p3,
195 uv: uv3,
196 tint,
197 },
198 ]
199}
200
201pub fn push_rrect(out: &mut Vec<RRectVertex>, w: f32, h: f32, rr: DrawRRect) {
211 push_rrect_scaled(out, w, h, Scale::new(1.0), rr);
212}
213
214pub fn push_rrect_scaled(
228 out: &mut Vec<RRectVertex>,
229 w: f32,
230 h: f32,
231 scale: Scale,
232 mut rr: DrawRRect,
233) {
234 rr.rect = scale_rect_to_physical(rr.rect, scale);
235 rr.radius *= scale.dpr;
236 let x0 = rr.rect.x;
237 let y0 = rr.rect.y;
238 let x1 = rr.rect.x + rr.rect.w;
239 let y1 = rr.rect.y + rr.rect.h;
240
241 let p0 = to_ndc(w, h, x0, y0);
242 let p1 = to_ndc(w, h, x1, y0);
243 let p2 = to_ndc(w, h, x1, y1);
244 let p3 = to_ndc(w, h, x0, y1);
245
246 let uv0 = [0.0, 0.0];
247 let uv1 = [1.0, 0.0];
248 let uv2 = [1.0, 1.0];
249 let uv3 = [0.0, 1.0];
250
251 let color = rr.color.to_array();
252 let size_px = [rr.rect.w.max(1.0), rr.rect.h.max(1.0)];
253 let radius = rr.radius.max(0.0);
254
255 out.push(RRectVertex {
256 pos: p0,
257 uv: uv0,
258 color,
259 size_px,
260 radius_px: radius,
261 });
262 out.push(RRectVertex {
263 pos: p1,
264 uv: uv1,
265 color,
266 size_px,
267 radius_px: radius,
268 });
269 out.push(RRectVertex {
270 pos: p2,
271 uv: uv2,
272 color,
273 size_px,
274 radius_px: radius,
275 });
276 out.push(RRectVertex {
277 pos: p0,
278 uv: uv0,
279 color,
280 size_px,
281 radius_px: radius,
282 });
283 out.push(RRectVertex {
284 pos: p2,
285 uv: uv2,
286 color,
287 size_px,
288 radius_px: radius,
289 });
290 out.push(RRectVertex {
291 pos: p3,
292 uv: uv3,
293 color,
294 size_px,
295 radius_px: radius,
296 });
297}
298
299pub fn push_border_rrect_scaled(
313 out: &mut Vec<BorderRRectVertex>,
314 w: f32,
315 h: f32,
316 scale: Scale,
317 mut border: DrawBorder,
318 width: f32,
319 color: Color,
320) {
321 border.rect = scale_rect_to_physical(border.rect, scale);
322 let width = (width * scale.dpr).max(0.0);
323 let radius = (border.radius.tl * scale.dpr).max(0.0);
324 let x0 = border.rect.x;
325 let y0 = border.rect.y;
326 let x1 = border.rect.x + border.rect.w;
327 let y1 = border.rect.y + border.rect.h;
328
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
334 let uv0 = [0.0, 0.0];
335 let uv1 = [1.0, 0.0];
336 let uv2 = [1.0, 1.0];
337 let uv3 = [0.0, 1.0];
338
339 let color = color.to_array();
340 let size_px = [border.rect.w.max(1.0), border.rect.h.max(1.0)];
341
342 out.push(BorderRRectVertex {
343 pos: p0,
344 uv: uv0,
345 color,
346 size_px,
347 radius_px: radius,
348 width_px: width,
349 });
350 out.push(BorderRRectVertex {
351 pos: p1,
352 uv: uv1,
353 color,
354 size_px,
355 radius_px: radius,
356 width_px: width,
357 });
358 out.push(BorderRRectVertex {
359 pos: p2,
360 uv: uv2,
361 color,
362 size_px,
363 radius_px: radius,
364 width_px: width,
365 });
366 out.push(BorderRRectVertex {
367 pos: p0,
368 uv: uv0,
369 color,
370 size_px,
371 radius_px: radius,
372 width_px: width,
373 });
374 out.push(BorderRRectVertex {
375 pos: p2,
376 uv: uv2,
377 color,
378 size_px,
379 radius_px: radius,
380 width_px: width,
381 });
382 out.push(BorderRRectVertex {
383 pos: p3,
384 uv: uv3,
385 color,
386 size_px,
387 radius_px: radius,
388 width_px: width,
389 });
390}
391
392pub fn push_box_shadow_scaled(
407 out: &mut Vec<BoxShadowVertex>,
408 w: f32,
409 h: f32,
410 scale: Scale,
411 shadow: DrawBoxShadow,
412) {
413 if shadow.shadow.inset || shadow.shadow.color.a <= 0.0 {
414 return;
415 }
416
417 let paint_rect = scale_rect_to_physical(shadow.shadow.paint_bounds(shadow.rect), scale);
418 let shape_rect = scale_rect_to_physical(shadow.shadow.shape_rect(shadow.rect), scale);
419 if paint_rect.w <= 0.0 || paint_rect.h <= 0.0 || shape_rect.w <= 0.0 || shape_rect.h <= 0.0 {
420 return;
421 }
422
423 let x0 = paint_rect.x;
424 let y0 = paint_rect.y;
425 let x1 = paint_rect.x + paint_rect.w;
426 let y1 = paint_rect.y + paint_rect.h;
427
428 let p0 = to_ndc(w, h, x0, y0);
429 let p1 = to_ndc(w, h, x1, y0);
430 let p2 = to_ndc(w, h, x1, y1);
431 let p3 = to_ndc(w, h, x0, y1);
432
433 let uv0 = [0.0, 0.0];
434 let uv1 = [1.0, 0.0];
435 let uv2 = [1.0, 1.0];
436 let uv3 = [0.0, 1.0];
437
438 let color = shadow.shadow.color.to_array();
439 let paint_size_px = [paint_rect.w.max(1.0), paint_rect.h.max(1.0)];
440 let shape_offset_px = [shape_rect.x - paint_rect.x, shape_rect.y - paint_rect.y];
441 let shape_size_px = [shape_rect.w.max(1.0), shape_rect.h.max(1.0)];
442 let radius_px = ((shadow.radius.tl + shadow.shadow.spread).max(0.0) * scale.dpr)
443 .min(shape_size_px[0].min(shape_size_px[1]) * 0.5);
444 let blur_px = (shadow.shadow.blur_radius * scale.dpr).max(0.0);
445
446 out.push(BoxShadowVertex {
447 pos: p0,
448 uv: uv0,
449 color,
450 paint_size_px,
451 shape_offset_px,
452 shape_size_px,
453 radius_px,
454 blur_px,
455 });
456 out.push(BoxShadowVertex {
457 pos: p1,
458 uv: uv1,
459 color,
460 paint_size_px,
461 shape_offset_px,
462 shape_size_px,
463 radius_px,
464 blur_px,
465 });
466 out.push(BoxShadowVertex {
467 pos: p2,
468 uv: uv2,
469 color,
470 paint_size_px,
471 shape_offset_px,
472 shape_size_px,
473 radius_px,
474 blur_px,
475 });
476 out.push(BoxShadowVertex {
477 pos: p0,
478 uv: uv0,
479 color,
480 paint_size_px,
481 shape_offset_px,
482 shape_size_px,
483 radius_px,
484 blur_px,
485 });
486 out.push(BoxShadowVertex {
487 pos: p2,
488 uv: uv2,
489 color,
490 paint_size_px,
491 shape_offset_px,
492 shape_size_px,
493 radius_px,
494 blur_px,
495 });
496 out.push(BoxShadowVertex {
497 pos: p3,
498 uv: uv3,
499 color,
500 paint_size_px,
501 shape_offset_px,
502 shape_size_px,
503 radius_px,
504 blur_px,
505 });
506}
507
508pub fn push_ring_progress_scaled(
522 out: &mut Vec<RingProgressVertex>,
523 w: f32,
524 h: f32,
525 scale: Scale,
526 mut ring: DrawRingProgress,
527) {
528 ring.rect = scale_rect_to_physical(ring.rect, scale);
529 if ring.rect.w <= 0.0 || ring.rect.h <= 0.0 {
530 return;
531 }
532
533 let x0 = ring.rect.x;
534 let y0 = ring.rect.y;
535 let x1 = ring.rect.x + ring.rect.w;
536 let y1 = ring.rect.y + ring.rect.h;
537
538 let p0 = to_ndc(w, h, x0, y0);
539 let p1 = to_ndc(w, h, x1, y0);
540 let p2 = to_ndc(w, h, x1, y1);
541 let p3 = to_ndc(w, h, x0, y1);
542
543 let uv0 = [0.0, 0.0];
544 let uv1 = [1.0, 0.0];
545 let uv2 = [1.0, 1.0];
546 let uv3 = [0.0, 1.0];
547
548 let size_px = [ring.rect.w.max(1.0), ring.rect.h.max(1.0)];
549 let thickness_px = (ring.thickness * scale.dpr)
550 .max(0.0)
551 .min(size_px[0].min(size_px[1]) * 0.5);
552 let fraction = ring.fraction.clamp(0.0, 1.0);
553 let track_color = ring.track_color.to_array();
554 let fill_color = ring.fill_color.to_array();
555 let start_angle = ring.start_angle;
556
557 out.push(RingProgressVertex {
558 pos: p0,
559 uv: uv0,
560 track_color,
561 fill_color,
562 size_px,
563 thickness_px,
564 fraction,
565 start_angle,
566 });
567 out.push(RingProgressVertex {
568 pos: p1,
569 uv: uv1,
570 track_color,
571 fill_color,
572 size_px,
573 thickness_px,
574 fraction,
575 start_angle,
576 });
577 out.push(RingProgressVertex {
578 pos: p2,
579 uv: uv2,
580 track_color,
581 fill_color,
582 size_px,
583 thickness_px,
584 fraction,
585 start_angle,
586 });
587 out.push(RingProgressVertex {
588 pos: p0,
589 uv: uv0,
590 track_color,
591 fill_color,
592 size_px,
593 thickness_px,
594 fraction,
595 start_angle,
596 });
597 out.push(RingProgressVertex {
598 pos: p2,
599 uv: uv2,
600 track_color,
601 fill_color,
602 size_px,
603 thickness_px,
604 fraction,
605 start_angle,
606 });
607 out.push(RingProgressVertex {
608 pos: p3,
609 uv: uv3,
610 track_color,
611 fill_color,
612 size_px,
613 thickness_px,
614 fraction,
615 start_angle,
616 });
617}
618
619#[allow(clippy::too_many_arguments)]
620pub fn push_text(
640 out: &mut Vec<TexVertex>,
641 text_draws: &mut Vec<(u8, std::ops::Range<u32>)>,
642 w: f32,
643 h: f32,
644 atlas: &mut TextAtlas,
645 device: &wgpu::Device,
646 queue: &wgpu::Queue,
647 bind_group_layout: &wgpu::BindGroupLayout,
648 face_blobs: &HashMap<u64, Arc<[u8]>>,
649 dt: &DrawText,
650) {
651 atlas.start_frame();
652 push_text_scaled(
653 out,
654 text_draws,
655 [w, h],
656 Scale::new(1.0),
657 atlas,
658 device,
659 queue,
660 bind_group_layout,
661 face_blobs,
662 dt,
663 );
664 atlas.finish_frame();
665}
666
667#[allow(clippy::too_many_arguments)]
668pub fn push_text_scaled(
690 out: &mut Vec<TexVertex>,
691 text_draws: &mut Vec<(u8, std::ops::Range<u32>)>,
692 surface: [f32; 2],
693 scale: Scale,
694 atlas: &mut TextAtlas,
695 device: &wgpu::Device,
696 queue: &wgpu::Queue,
697 bind_group_layout: &wgpu::BindGroupLayout,
698 face_blobs: &HashMap<u64, Arc<[u8]>>,
699 dt: &DrawText,
700) {
701 let [w, h] = surface;
702 let (origin_x, origin_y) = text_origin_from_baseline(dt);
703
704 let fallback_color = dt.color;
705 let scale_100 = (scale.dpr * 100.0).round().clamp(1.0, u16::MAX as f32) as u16;
706 let mut current_page: Option<u8> = None;
707 let mut current_start: u32 = out.len() as u32;
708 for gi in dt.layout.glyphs() {
709 let color = gi.color.unwrap_or(fallback_color).to_array();
710 let physical_px_size = ((gi.px_size as f32) * scale.dpr).round();
711 let key = GlyphKey {
712 face_id: gi.face_id,
713 font_index: gi.font_index,
714 px_size: physical_px_size.clamp(8.0, 128.0) as u16,
715 glyph_id: gi.glyph_id,
716 scale_100,
717 };
718 let Some(blob) = face_blobs.get(&gi.face_id) else {
719 atlas.record_missing_face();
720 continue;
721 };
722 let Some((page_idx, g)) =
723 atlas.get_or_rasterize_pinned(device, queue, bind_group_layout, key, blob.as_ref())
724 else {
725 continue;
726 };
727 if g.size_px[0] <= 0.0 || g.size_px[1] <= 0.0 {
728 continue;
729 }
730
731 if Some(page_idx) != current_page {
732 if let Some(p) = current_page {
733 let end = out.len() as u32;
734 if end > current_start {
735 text_draws.push((p, current_start..end));
736 }
737 }
738 current_page = Some(page_idx);
739 current_start = out.len() as u32;
740 }
741
742 let pen_x = (origin_x + gi.x) * scale.dpr;
743 let pen_y = (origin_y + gi.y) * scale.dpr;
744
745 let x0 = (pen_x + g.offset_px[0]).round();
746 let y0 = (pen_y + g.offset_px[1]).round();
747 let x1 = x0 + g.size_px[0];
748 let y1 = y0 + g.size_px[1];
749
750 let p0 = to_ndc(w, h, x0, y0);
751 let p1 = to_ndc(w, h, x1, y0);
752 let p2 = to_ndc(w, h, x1, y1);
753 let p3 = to_ndc(w, h, x0, y1);
754
755 let uv0 = g.uv_min;
756 let uv2 = g.uv_max;
757 let uv1 = [uv2[0], uv0[1]];
758 let uv3 = [uv0[0], uv2[1]];
759
760 out.push(TexVertex {
761 pos: p0,
762 uv: uv0,
763 tint: color,
764 });
765 out.push(TexVertex {
766 pos: p1,
767 uv: uv1,
768 tint: color,
769 });
770 out.push(TexVertex {
771 pos: p2,
772 uv: uv2,
773 tint: color,
774 });
775 out.push(TexVertex {
776 pos: p0,
777 uv: uv0,
778 tint: color,
779 });
780 out.push(TexVertex {
781 pos: p2,
782 uv: uv2,
783 tint: color,
784 });
785 out.push(TexVertex {
786 pos: p3,
787 uv: uv3,
788 tint: color,
789 });
790 }
791
792 if let Some(p) = current_page {
793 let end = out.len() as u32;
794 if end > current_start {
795 text_draws.push((p, current_start..end));
796 }
797 }
798}
799
800pub(crate) fn text_origin_from_baseline(dt: &DrawText) -> (f32, f32) {
813 let first_baseline = dt.layout.lines.first().map(|l| l.baseline_y).unwrap_or(0.0);
814 (dt.pos[0], dt.pos[1] - first_baseline)
815}
816
817pub fn set_scissor_rect(rpass: &mut wgpu::RenderPass<'_>, w: f32, h: f32, clip: Rect) {
829 set_scissor_rect_scaled(rpass, w, h, Scale::new(1.0), clip);
830}
831
832pub fn set_scissor_rect_scaled(
845 rpass: &mut wgpu::RenderPass<'_>,
846 w_px: f32,
847 h_px: f32,
848 scale: Scale,
849 clip_logical: Rect,
850) {
851 let (x0, y0, w, h) = match scissor_rect_u32(w_px as u32, h_px as u32, scale, clip_logical) {
852 Some(v) => v,
853 None => return,
854 };
855 rpass.set_scissor_rect(x0, y0, w, h);
856}
857
858pub fn apply_layer_scissor(
872 rpass: &mut wgpu::RenderPass<'_>,
873 w_px: f32,
874 h_px: f32,
875 scale: Scale,
876 clip_logical: Option<Rect>,
877) {
878 let surface_w = w_px as u32;
879 let surface_h = h_px as u32;
880 if let Some(clip) = clip_logical {
881 if let Some((x0, y0, w, h)) = scissor_rect_u32(surface_w, surface_h, scale, clip) {
882 rpass.set_scissor_rect(x0, y0, w, h);
883 return;
884 }
885 }
886 rpass.set_scissor_rect(0, 0, surface_w.max(1), surface_h.max(1));
887}
888
889fn scissor_rect_u32(
891 surface_w_px: u32,
892 surface_h_px: u32,
893 scale: Scale,
894 clip_logical: Rect,
895) -> Option<(u32, u32, u32, u32)> {
896 let pr = snap_rect_to_physical(clip_logical, scale);
897
898 let max_w = surface_w_px as i32;
899 let max_h = surface_h_px as i32;
900 let x0 = pr.x.clamp(0, max_w) as u32;
901 let y0 = pr.y.clamp(0, max_h) as u32;
902 let x1 = (pr.x + pr.w).clamp(0, max_w) as u32;
903 let y1 = (pr.y + pr.h).clamp(0, max_h) as u32;
904 let w = x1.saturating_sub(x0);
905 let h = y1.saturating_sub(y0);
906 if w == 0 || h == 0 {
907 None
908 } else {
909 Some((x0, y0, w, h))
910 }
911}
912
913#[cfg(test)]
914mod tests {
916 use super::*;
917
918 use ailloli_ui_core::{style::Color, FontId, TextStyle};
919 use ailloli_ui_runtime::DrawText as RuntimeDrawText;
920 use ailloli_ui_text::{TextLayoutParams, TextSystem, WrapMode};
921
922 #[test]
923 fn draw_text_pos_is_baseline_contract() {
924 let mut ts = TextSystem::new();
925 let style = TextStyle::new(FontId::Ui, 16, Color::new(1.0, 1.0, 1.0, 1.0));
926 let prep = ts.layout_cached(TextLayoutParams {
927 text: "A",
928 style,
929 max_width: None,
930 wrap_mode: WrapMode::NoWrap,
931 });
932 let baseline = prep.lines.first().map(|l| l.baseline_y).unwrap_or(0.0);
933
934 let dt = RuntimeDrawText {
935 pos: [10.0, 20.0 + baseline],
936 color: style.color,
937 decoration: ailloli_ui_core::TextDecoration::None,
938 layout: std::sync::Arc::clone(&prep),
939 };
940
941 let (_x, y) = text_origin_from_baseline(&dt);
942 assert!((y - 20.0).abs() <= f32::EPSILON);
943 }
944
945 #[test]
946 fn scissor_uses_core_snap_rect_to_physical_with_dpr_2() {
947 let scale = Scale::new(2.0);
948 let clip = Rect::new(1.0, 2.0, 3.0, 4.0);
950 let (x0, y0, w, h) = scissor_rect_u32(100, 100, scale, clip).expect("non-empty");
951 assert_eq!((x0, y0, w, h), (2, 4, 6, 8));
952 }
953
954 #[test]
955 fn rect_geometry_is_scaled_to_physical_before_ndc() {
956 let mut vertices = Vec::new();
957 push_rect_scaled(
958 &mut vertices,
959 200.0,
960 100.0,
961 Scale::new(2.0),
962 Rect::new(0.0, 0.0, 100.0, 50.0),
963 Color::new(1.0, 0.0, 0.0, 1.0),
964 );
965
966 assert_eq!(vertices.len(), 6);
967 assert_eq!(vertices[0].pos, [-1.0, 1.0]);
968 assert_eq!(vertices[1].pos, [1.0, 1.0]);
969 assert_eq!(vertices[2].pos, [1.0, -1.0]);
970 }
971
972 #[test]
973 fn rrect_size_and_radius_are_scaled_to_physical() {
974 let mut vertices = Vec::new();
975 push_rrect_scaled(
976 &mut vertices,
977 200.0,
978 100.0,
979 Scale::new(2.0),
980 ailloli_ui_runtime::DrawRRect {
981 rect: Rect::new(0.0, 0.0, 100.0, 50.0),
982 radius: 4.0,
983 color: Color::new(1.0, 0.0, 0.0, 1.0),
984 },
985 );
986
987 assert_eq!(vertices[0].size_px, [200.0, 100.0]);
988 assert_eq!(vertices[0].radius_px, 8.0);
989 }
990}