1use ui_layout::{
5 BoxModel, EdgeOption, InnerDisplay, LayoutChild, LayoutNode, OuterDisplay, Position, Rect,
6};
7
8use crate::engine::layouter::text_layouter::TextFlowLayouter;
9use crate::engine::layouter::types::{
10 Background, BackgroundDimension, BackgroundImage, BackgroundOffset, BackgroundPositionAxis,
11 BackgroundRepeat, BackgroundSize, BorderRadius, ClipPath, Color, ContainerStyle, CornerRadius,
12 InfoNode, NodeKind, TextDecoration, TextFlowStyle, TextStyle, Visibility,
13};
14use crate::engine::renderer_model::draw_command::{Brush, DrawCommand, FillRule, Paint};
15use crate::engine::renderer_model::geom::AffineTransform;
16use crate::engine::renderer_model::path::{
17 Path, append_quarter_ellipse, clamp_radii, ellipse_path, offset_path, polygon_path, rect_path,
18 rounded_rect_path,
19};
20use crate::engine::svg::emit_commands;
21use crate::engine::ui::ContentSize;
22
23#[derive(Default, Clone, Copy)]
25struct BoxPushState {
26 border: bool,
27 transform: bool,
29 clip_path: bool,
30 overflow_clip: bool,
31 content: bool,
32 scroll: bool,
33 cull: bool,
36}
37
38fn push_transform(cmd_buf: &mut Vec<DrawCommand>, dx: f32, dy: f32) -> bool {
43 if dx != 0.0 || dy != 0.0 {
44 cmd_buf.push(DrawCommand::PushTransform {
45 transform: AffineTransform::translate(dx, dy),
46 });
47 true
48 } else {
49 false
50 }
51}
52
53#[derive(Default, Clone, Copy)]
58pub struct StickyViewport {
59 pub top_left: (f32, f32),
65 pub size: (f32, f32),
67}
68
69pub fn is_scrollport(kind: &NodeKind) -> bool {
73 match kind {
74 NodeKind::Container {
75 scroll_x, scroll_y, ..
76 }
77 | NodeKind::Custom {
78 scroll_x, scroll_y, ..
79 }
80 | NodeKind::Svg {
81 scroll_x, scroll_y, ..
82 } => *scroll_x || *scroll_y,
83 _ => false,
84 }
85}
86
87pub fn sticky_offset(
96 edges: &EdgeOption,
97 box_rect: &Rect,
98 viewport: StickyViewport,
99 containing: (f32, f32),
100) -> (f32, f32) {
101 let x = box_rect.x;
102 let y = box_rect.y;
103 let w = box_rect.width;
104 let h = box_rect.height;
105
106 let mut dy: f32 = 0.0;
107 let port_top = viewport.top_left.1;
108 let port_bottom = viewport.top_left.1 + viewport.size.1;
109 if let Some(top) = edges.top {
110 dy = dy.max((port_top + top) - y);
111 }
112 if let Some(bottom) = edges.bottom {
113 let has_room = match edges.top {
117 Some(top) => (port_bottom - bottom) - (port_top + top) >= h,
118 None => true,
119 };
120 if has_room {
121 dy = dy.min((port_bottom - bottom) - (y + h));
122 }
123 }
124 let dy_lo = (-y).min(0.0);
128 let dy_hi = (containing.1 - y - h).max(0.0);
129 dy = dy.max(dy_lo).min(dy_hi);
130
131 let mut dx: f32 = 0.0;
132 let port_left = viewport.top_left.0;
133 let port_right = viewport.top_left.0 + viewport.size.0;
134 if let Some(left) = edges.left {
135 dx = dx.max((port_left + left) - x);
136 }
137 if let Some(right) = edges.right {
138 let has_room = match edges.left {
139 Some(left) => (port_right - right) - (port_left + left) >= w,
140 None => true,
141 };
142 if has_room {
143 dx = dx.min((port_right - right) - (x + w));
144 }
145 }
146 let dx_lo = (-x).min(0.0);
147 let dx_hi = (containing.0 - x - w).max(0.0);
148 dx = dx.max(dx_lo).min(dx_hi);
149
150 (dx, dy)
151}
152
153fn resolve_outer_radii(radius: &BorderRadius, box_w: f32, box_h: f32) -> [(f32, f32); 4] {
158 let resolve = |c: &CornerRadius| -> (f32, f32) {
159 (
160 c.x.resolve_with(Some(box_w), 0.0, 0.0)
161 .unwrap_or(0.0)
162 .max(0.0),
163 c.y.resolve_with(Some(box_h), 0.0, 0.0)
164 .unwrap_or(0.0)
165 .max(0.0),
166 )
167 };
168 [
169 resolve(&radius.top_left),
170 resolve(&radius.top_right),
171 resolve(&radius.bottom_right),
172 resolve(&radius.bottom_left),
173 ]
174}
175
176fn inner_radii(outer: [(f32, f32); 4], bl: f32, bt: f32, br: f32, bb: f32) -> [(f32, f32); 4] {
179 [
180 (outer[0].0 - bl, outer[0].1 - bt),
181 (outer[1].0 - br, outer[1].1 - bt),
182 (outer[2].0 - br, outer[2].1 - bb),
183 (outer[3].0 - bl, outer[3].1 - bb),
184 ]
185 .map(|(x, y)| (x.max(0.0), y.max(0.0)))
186}
187
188fn top_border_strip(
194 w: f32,
195 bl: f32,
196 bt: f32,
197 br: f32,
198 outer: [(f32, f32); 4],
199 inner: [(f32, f32); 4],
200) -> Path {
201 let (rtl_x, rtl_y) = outer[0];
202 let (rtr_x, rtr_y) = outer[1];
203 let (itl_x, itl_y) = inner[0];
204 let (itr_x, itr_y) = inner[1];
205 let mut path = Path::new();
206 path.move_to(0.0, rtl_y);
207 append_quarter_ellipse(
208 &mut path,
209 rtl_x,
210 rtl_y,
211 rtl_x,
212 rtl_y,
213 (0.0, rtl_y),
214 (rtl_x, 0.0),
215 );
216 path.line_to(w - rtr_x, 0.0);
217 append_quarter_ellipse(
218 &mut path,
219 w - rtr_x,
220 rtr_y,
221 rtr_x,
222 rtr_y,
223 (w - rtr_x, 0.0),
224 (w, rtr_y),
225 );
226 path.line_to(w - br, rtr_y);
229 append_quarter_ellipse(
233 &mut path,
234 w - rtr_x - br,
235 rtr_y - bt,
236 itr_x,
237 itr_y,
238 (w - br, rtr_y),
239 (w - rtr_x - br, bt),
240 );
241 path.line_to(itl_x, bt);
242 append_quarter_ellipse(
243 &mut path,
244 rtl_x - bl,
245 rtl_y - bt,
246 itl_x,
247 itl_y,
248 (itl_x, bt),
249 (bl, rtl_y),
250 );
251 path.close();
252 path
253}
254
255fn bottom_border_strip(
258 w: f32,
259 h: f32,
260 bl: f32,
261 bb: f32,
262 br: f32,
263 outer: [(f32, f32); 4],
264 inner: [(f32, f32); 4],
265) -> Path {
266 let (rbl_x, rbl_y) = outer[3];
267 let (rbr_x, rbr_y) = outer[2];
268 let (ibl_x, ibl_y) = inner[3];
269 let (ibr_x, ibr_y) = inner[2];
270 let mut path = Path::new();
271 path.move_to(w, h - rbr_y);
272 append_quarter_ellipse(
273 &mut path,
274 w - rbr_x,
275 h - rbr_y,
276 rbr_x,
277 rbr_y,
278 (w, h - rbr_y),
279 (w - rbr_x, h),
280 );
281 path.line_to(rbl_x, h);
282 append_quarter_ellipse(
283 &mut path,
284 rbl_x,
285 h - rbl_y,
286 rbl_x,
287 rbl_y,
288 (rbl_x, h),
289 (0.0, h - rbl_y),
290 );
291 path.line_to(bl, h - rbl_y);
292 append_quarter_ellipse(
293 &mut path,
294 rbl_x,
295 h - rbl_y,
296 ibl_x,
297 ibl_y,
298 (bl, h - rbl_y),
299 (rbl_x, h - bb),
300 );
301 path.line_to(w - rbr_x, h - bb);
302 append_quarter_ellipse(
303 &mut path,
304 w - rbr_x,
305 h - rbr_y,
306 ibr_x,
307 ibr_y,
308 (w - rbr_x, h - bb),
309 (w - br, h - rbr_y),
310 );
311 path.close();
312 path
313}
314
315fn draw_border(
320 cmd_buf: &mut Vec<DrawCommand>,
321 border_box: &ui_layout::Rect,
322 padding_box: &ui_layout::Rect,
323 style: &ContainerStyle,
324 ox: f32,
325 oy: f32,
326 opacity: f32,
327) {
328 let bw_top = (padding_box.y - border_box.y).max(0.0);
329 let bw_bottom =
330 (border_box.y + border_box.height - (padding_box.y + padding_box.height)).max(0.0);
331 let bw_left = (padding_box.x - border_box.x).max(0.0);
332 let bw_right = (border_box.x + border_box.width - (padding_box.x + padding_box.width)).max(0.0);
333
334 let w = border_box.width;
335 let h = border_box.height;
336 let mut outer = resolve_outer_radii(&style.border_radius, w, h);
337 outer = clamp_radii(outer, w, h);
338 let mut inner = inner_radii(outer, bw_left, bw_top, bw_right, bw_bottom);
339 inner = clamp_radii(
340 inner,
341 padding_box.width.max(0.0),
342 padding_box.height.max(0.0),
343 );
344
345 let bc = &style.border_color;
346 let bs = &style.border_style;
347 let push_fill = |cmd_buf: &mut Vec<DrawCommand>, path: Path, color: Color| {
348 cmd_buf.push(DrawCommand::Fill {
349 path,
350 rule: FillRule::NonZero,
351 paint: Paint {
352 brush: Brush::Solid(color),
353 opacity,
354 },
355 });
356 };
357
358 let has_radius = outer.iter().any(|(rx, ry)| *rx > 0.0 || *ry > 0.0);
359 if !has_radius {
360 if bw_top > 0.0 && bs.top != crate::engine::layouter::types::BorderStyle::None {
361 push_fill(cmd_buf, rect_path(ox, oy, w, bw_top), bc.top);
362 }
363 if bw_bottom > 0.0 && bs.bottom != crate::engine::layouter::types::BorderStyle::None {
364 push_fill(
365 cmd_buf,
366 rect_path(ox, oy + h - bw_bottom, w, bw_bottom),
367 bc.bottom,
368 );
369 }
370 if bw_left > 0.0 && bs.left != crate::engine::layouter::types::BorderStyle::None {
371 push_fill(
372 cmd_buf,
373 rect_path(ox, oy + bw_top, bw_left, h - bw_top - bw_bottom),
374 bc.left,
375 );
376 }
377 if bw_right > 0.0 && bs.right != crate::engine::layouter::types::BorderStyle::None {
378 push_fill(
379 cmd_buf,
380 rect_path(
381 ox + w - bw_right,
382 oy + bw_top,
383 bw_right,
384 h - bw_top - bw_bottom,
385 ),
386 bc.right,
387 );
388 }
389 return;
390 }
391
392 if bw_top > 0.0 && bs.top != crate::engine::layouter::types::BorderStyle::None {
393 push_fill(
394 cmd_buf,
395 offset_path(
396 &top_border_strip(w, bw_left, bw_top, bw_right, outer, inner),
397 ox,
398 oy,
399 ),
400 bc.top,
401 );
402 }
403 if bw_bottom > 0.0 && bs.bottom != crate::engine::layouter::types::BorderStyle::None {
404 push_fill(
405 cmd_buf,
406 offset_path(
407 &bottom_border_strip(w, h, bw_left, bw_bottom, bw_right, outer, inner),
408 ox,
409 oy,
410 ),
411 bc.bottom,
412 );
413 }
414 if bw_left > 0.0 && bs.left != crate::engine::layouter::types::BorderStyle::None {
415 push_fill(
416 cmd_buf,
417 rect_path(ox, oy + outer[0].1, bw_left, h - outer[0].1 - outer[3].1),
418 bc.left,
419 );
420 }
421 if bw_right > 0.0 && bs.right != crate::engine::layouter::types::BorderStyle::None {
422 push_fill(
423 cmd_buf,
424 rect_path(
425 ox + w - bw_right,
426 oy + outer[1].1,
427 bw_right,
428 h - outer[1].1 - outer[2].1,
429 ),
430 bc.right,
431 );
432 }
433}
434
435fn draw_background(
440 cmd_buf: &mut Vec<DrawCommand>,
441 border_box: &ui_layout::Rect,
442 padding_box: &ui_layout::Rect,
443 style: &ContainerStyle,
444 ox: f32,
445 oy: f32,
446 opacity: f32,
447) {
448 let x = padding_box.x - border_box.x + ox;
449 let y = padding_box.y - border_box.y + oy;
450 let bw_top = (padding_box.y - border_box.y).max(0.0);
451 let bw_bottom =
452 (border_box.y + border_box.height - (padding_box.y + padding_box.height)).max(0.0);
453 let bw_left = (padding_box.x - border_box.x).max(0.0);
454 let bw_right = (border_box.x + border_box.width - (padding_box.x + padding_box.width)).max(0.0);
455
456 let mut outer = resolve_outer_radii(&style.border_radius, border_box.width, border_box.height);
457 outer = clamp_radii(outer, border_box.width, border_box.height);
458 let mut inner = inner_radii(outer, bw_left, bw_top, bw_right, bw_bottom);
459 inner = clamp_radii(
460 inner,
461 padding_box.width.max(0.0),
462 padding_box.height.max(0.0),
463 );
464 let path = rounded_rect_path(
466 x,
467 y,
468 padding_box.width,
469 padding_box.height,
470 inner[0],
471 inner[1],
472 inner[2],
473 inner[3],
474 );
475 if let Some(color) = style.background.color
477 && color.3 > 0
478 {
479 cmd_buf.push(DrawCommand::Fill {
480 path: path.clone(),
481 rule: FillRule::NonZero,
482 paint: Paint {
483 brush: Brush::Solid(color),
484 opacity,
485 },
486 });
487 }
488
489 for layer in style.background.layers.iter().rev() {
492 let layer_repeat = layer.repeat;
493 let layer_size = layer.size;
494 let layer_position = layer.position;
495
496 match &layer.image {
497 Some(BackgroundImage::Gradient(g)) => {
498 cmd_buf.push(DrawCommand::Fill {
499 path: path.clone(),
500 rule: FillRule::NonZero,
501 paint: Paint {
502 brush: Brush::Gradient(g.clone()),
503 opacity,
504 },
505 });
506 }
507 Some(BackgroundImage::Image {
508 source: _,
509 image: Some(image),
510 }) => {
511 let (image_width, image_height) = resolve_background_image_size(
512 image.width() as f32,
513 image.height() as f32,
514 padding_box.width,
515 padding_box.height,
516 layer_size,
517 );
518 if image_width > 0.0 && image_height > 0.0 {
519 let image_x =
520 resolve_background_axis(padding_box.width, image_width, layer_position.x);
521 let image_y =
522 resolve_background_axis(padding_box.height, image_height, layer_position.y);
523 let repeat_x = matches!(
524 layer_repeat,
525 BackgroundRepeat::Repeat | BackgroundRepeat::RepeatX
526 );
527 let repeat_y = matches!(
528 layer_repeat,
529 BackgroundRepeat::Repeat | BackgroundRepeat::RepeatY
530 );
531 let xs = background_tile_positions(
532 image_x,
533 image_width,
534 padding_box.width,
535 repeat_x,
536 );
537 let ys = background_tile_positions(
538 image_y,
539 image_height,
540 padding_box.height,
541 repeat_y,
542 );
543 cmd_buf.push(DrawCommand::PushClip {
544 path: path.clone(),
545 rule: FillRule::NonZero,
546 });
547 for tile_y in ys {
548 for tile_x in &xs {
549 cmd_buf.push(DrawCommand::Fill {
550 path: rect_path(x + *tile_x, y + tile_y, image_width, image_height),
551 rule: FillRule::NonZero,
552 paint: Paint {
553 brush: Brush::Image(image.clone()),
554 opacity,
555 },
556 });
557 }
558 }
559 cmd_buf.push(DrawCommand::PopClip);
560 }
561 }
562 Some(BackgroundImage::Image { image: None, .. }) => {}
563 None => {}
564 }
565 }
566}
567
568fn resolve_background_dimension(dimension: BackgroundDimension, area: f32) -> Option<f32> {
569 match dimension {
570 BackgroundDimension::Auto => None,
571 BackgroundDimension::Length(value) => Some(value.max(0.0)),
572 BackgroundDimension::Percent(value) => Some((area * value).max(0.0)),
573 }
574}
575
576fn resolve_background_image_size(
577 intrinsic_width: f32,
578 intrinsic_height: f32,
579 area_width: f32,
580 area_height: f32,
581 size: BackgroundSize,
582) -> (f32, f32) {
583 if intrinsic_width <= 0.0 || intrinsic_height <= 0.0 {
584 return (0.0, 0.0);
585 }
586 let ratio = intrinsic_width / intrinsic_height;
587 match size {
588 BackgroundSize::Auto => (intrinsic_width, intrinsic_height),
589 BackgroundSize::Contain | BackgroundSize::Cover => {
590 let width_scale = area_width / intrinsic_width;
591 let height_scale = area_height / intrinsic_height;
592 let scale = if matches!(size, BackgroundSize::Contain) {
593 width_scale.min(height_scale)
594 } else {
595 width_scale.max(height_scale)
596 };
597 (intrinsic_width * scale, intrinsic_height * scale)
598 }
599 BackgroundSize::Explicit { width, height } => {
600 let width = resolve_background_dimension(width, area_width);
601 let height = resolve_background_dimension(height, area_height);
602 match (width, height) {
603 (Some(width), Some(height)) => (width, height),
604 (Some(width), None) => (width, width / ratio),
605 (None, Some(height)) => (height * ratio, height),
606 (None, None) => (intrinsic_width, intrinsic_height),
607 }
608 }
609 }
610}
611
612fn resolve_background_axis(area: f32, image: f32, position: BackgroundPositionAxis) -> f32 {
613 let available = area - image;
614 let length_offset = |offset| match offset {
615 BackgroundOffset::Zero => 0.0,
616 BackgroundOffset::Length(value) => value,
617 BackgroundOffset::Percent(value) => area * value,
618 };
619 match position {
620 BackgroundPositionAxis::Start(BackgroundOffset::Percent(value)) => available * value,
621 BackgroundPositionAxis::Start(offset) => length_offset(offset),
622 BackgroundPositionAxis::Center(offset) => available * 0.5 + length_offset(offset),
623 BackgroundPositionAxis::End(offset) => available - length_offset(offset),
624 }
625}
626
627fn background_tile_positions(base: f32, tile: f32, area: f32, repeat: bool) -> Vec<f32> {
628 if !repeat || tile <= 0.0 {
629 return vec![base];
630 }
631 let mut start = base;
632 while start > 0.0 {
633 start -= tile;
634 }
635 while start + tile <= 0.0 {
636 start += tile;
637 }
638 let mut positions = Vec::new();
639 let mut position = start;
640 while position < area && positions.len() < 512 {
641 positions.push(position);
642 position += tile;
643 }
644 positions
645}
646
647fn clip_path_to_path(clip: &ClipPath, w: f32, h: f32) -> Path {
650 match clip {
651 ClipPath::None => rect_path(0.0, 0.0, w, h),
652 ClipPath::Circle {
653 radius,
654 center_x,
655 center_y,
656 } => {
657 let cx = center_x * w;
658 let cy = center_y * h;
659 let farthest = w.max(h);
661 let r = (radius * farthest).min(farthest);
662 ellipse_path(cx, cy, r, r)
663 }
664 ClipPath::Ellipse {
665 rx,
666 ry,
667 center_x,
668 center_y,
669 } => {
670 let cx = center_x * w;
671 let cy = center_y * h;
672 ellipse_path(cx, cy, rx * w, ry * h)
673 }
674 ClipPath::Inset {
675 top,
676 right,
677 bottom,
678 left,
679 } => {
680 let x = left * w;
681 let y = top * h;
682 let iw = (w - left * w - right * w).max(0.0);
683 let ih = (h - top * h - bottom * h).max(0.0);
684 rect_path(x, y, iw, ih)
685 }
686 ClipPath::Polygon { points } => {
687 let verts: Vec<(f32, f32)> = points.iter().map(|(px, py)| (px * w, py * h)).collect();
688 polygon_path(&verts)
689 }
690 }
691}
692
693fn clip_path_is_empty(clip: &ClipPath, w: f32, h: f32) -> bool {
696 match clip {
697 ClipPath::None => false,
698 ClipPath::Circle { radius, .. } => *radius <= 0.0 || w <= 0.0 || h <= 0.0,
699 ClipPath::Ellipse { rx, ry, .. } => *rx <= 0.0 || *ry <= 0.0 || w <= 0.0 || h <= 0.0,
700 ClipPath::Inset {
701 top,
702 right,
703 bottom,
704 left,
705 } => {
706 (w - left * w - right * w).max(0.0) <= 0.0 || (h - top * h - bottom * h).max(0.0) <= 0.0
707 }
708 ClipPath::Polygon { points } => points.len() < 3,
709 }
710}
711
712#[allow(clippy::too_many_arguments)]
714fn push_box_model(
715 cmd_buf: &mut Vec<DrawCommand>,
716 box_model: &ui_layout::BoxModel,
717 style: &crate::engine::layouter::types::ContainerStyle,
718 scroll_offset_x: f32,
719 scroll_offset_y: f32,
720 is_inline: bool,
721 clips_overflow: bool,
722 draw_bg: bool,
723 paint: bool,
724 opacity: f32,
725) -> BoxPushState {
726 let border_box = box_model.border_box;
727 let padding_box = box_model.padding_box;
728 let content_box = box_model.content_box;
729
730 let dx = content_box.x - border_box.x;
731 let dy = content_box.y - border_box.y;
732
733 let border = !is_inline && push_transform(cmd_buf, border_box.x, border_box.y);
738
739 let transform = !is_inline
744 && style
745 .transform
746 .is_some_and(|t| t != AffineTransform::identity())
747 && {
748 let center_x = border_box.width / 2.0;
749 let center_y = border_box.height / 2.0;
750 let t = style.transform.expect("checked above");
751 let centered = AffineTransform::translate(center_x, center_y)
752 .then(&t)
753 .then(&AffineTransform::translate(-center_x, -center_y));
754 cmd_buf.push(DrawCommand::PushTransform {
755 transform: centered,
756 });
757 true
758 };
759
760 let (ox, oy) = if is_inline {
764 (border_box.x, border_box.y)
765 } else {
766 (0.0, 0.0)
767 };
768
769 let clip_path_pushed = !is_inline
772 && !matches!(style.clip_path, ClipPath::None)
773 && border_box.width > 0.0
774 && border_box.height > 0.0;
775
776 let clip_culls = clip_path_pushed
779 && clip_path_is_empty(&style.clip_path, border_box.width, border_box.height);
780
781 let clip_path_pushed = clip_path_pushed && !clip_culls;
784 if clip_path_pushed {
785 let path = clip_path_to_path(&style.clip_path, border_box.width, border_box.height);
786 cmd_buf.push(DrawCommand::PushClip {
787 path,
788 rule: FillRule::NonZero,
789 });
790 }
791
792 if paint && !clip_culls {
796 draw_border(cmd_buf, &border_box, &padding_box, style, ox, oy, opacity);
797
798 if draw_bg {
799 draw_background(cmd_buf, &border_box, &padding_box, style, ox, oy, opacity);
800 }
801 }
802
803 let overflow_clip = !clip_culls
806 && !is_inline
807 && clips_overflow
808 && padding_box.width > 0.0
809 && padding_box.height > 0.0;
810 if overflow_clip {
811 cmd_buf.push(DrawCommand::PushClip {
812 path: rect_path(
813 padding_box.x - border_box.x,
814 padding_box.y - border_box.y,
815 padding_box.width,
816 padding_box.height,
817 ),
818 rule: FillRule::NonZero,
819 });
820 }
821
822 let content = !is_inline && push_transform(cmd_buf, dx, dy);
823 let scroll = !is_inline && push_transform(cmd_buf, -scroll_offset_x, -scroll_offset_y);
824
825 BoxPushState {
826 border,
827 transform,
828 clip_path: clip_path_pushed,
829 overflow_clip,
830 content,
831 scroll,
832 cull: clip_culls,
833 }
834}
835
836fn pop_box_model(cmd_buf: &mut Vec<DrawCommand>, state: BoxPushState) {
838 if state.scroll {
839 cmd_buf.push(DrawCommand::PopTransform);
840 }
841 if state.content {
842 cmd_buf.push(DrawCommand::PopTransform);
843 }
844 if state.overflow_clip {
845 cmd_buf.push(DrawCommand::PopClip);
846 }
847 if state.clip_path {
848 cmd_buf.push(DrawCommand::PopClip);
849 }
850 if state.transform {
851 cmd_buf.push(DrawCommand::PopTransform);
852 }
853 if state.border {
854 cmd_buf.push(DrawCommand::PopTransform);
855 }
856}
857
858fn draw_text(
860 cmd_buf: &mut Vec<DrawCommand>,
861 style: &TextStyle,
862 flow_style: TextFlowStyle,
863 text_id: usize,
864 offset: (f32, f32),
865 opacity: f32,
866) {
867 if let Some(result) = TextFlowLayouter::get_result(text_id) {
868 let mut style = style.clone();
871 style.color.3 = (style.color.3 as f32 * opacity).round() as u8;
872 for (i, line_text) in result.line_texts.iter().enumerate() {
873 let span = &result.spans[i];
874 let x = span.line_pos.0 + offset.0;
875 let y = span.line_pos.1 + offset.1;
876
877 cmd_buf.push(DrawCommand::DrawText {
878 x,
879 y,
880 text: line_text.as_str().into(),
881 style: style.clone(),
882 flow_style,
883 });
884
885 let font_size = flow_style.font_size;
886 let line_thickness = (font_size * 0.08).max(1.0);
887 let line_y_adj = if line_text.is_empty() {
888 y
889 } else {
890 y + font_size
891 };
892 let (line_y, draw) = match style.text_decoration {
893 TextDecoration::None => (0.0, false),
894 TextDecoration::Underline => (line_y_adj, true),
895 TextDecoration::LineThrough => (y + font_size * 0.5, true),
896 TextDecoration::Overline => (y, true),
897 };
898
899 if draw {
900 cmd_buf.push(DrawCommand::Fill {
901 path: rect_path(
902 x,
903 line_y,
904 span.x_range.end - span.x_range.start,
905 line_thickness,
906 ),
907 rule: FillRule::NonZero,
908 paint: Paint {
909 brush: Brush::Solid(style.text_decoration_color.unwrap_or(style.color)),
910 opacity,
911 },
912 });
913 }
914 }
915 }
916}
917
918fn emit_canvas_background(cmd_buf: &mut Vec<DrawCommand>, info: &InfoNode, viewport: (f32, f32)) {
930 fn has_visible_background(bg: &Background) -> bool {
933 bg.color.is_some_and(|c| c.3 > 0) || !bg.layers.is_empty()
934 }
935
936 fn resolve_canvas_background(info: &InfoNode) -> Option<&Background> {
940 let html = info.children.iter().find(|child| {
942 if let NodeKind::Container { style, .. } = &child.kind {
943 style.tag.as_deref() == Some("html")
944 } else {
945 false
946 }
947 });
948
949 match html {
950 Some(html_info) => {
951 let html_bg = html_info.kind.container_bg();
952 if html_bg.is_some_and(has_visible_background) {
953 html_bg
954 } else {
955 html_info.children.iter().find_map(|child| {
957 if let NodeKind::Container { style, .. } = &child.kind {
958 (style.tag.as_deref() == Some("body"))
959 .then_some(child.kind.container_bg())
960 .flatten()
961 } else {
962 None
963 }
964 })
965 }
966 }
967 None => {
968 let solid_color = |bg: Option<&Background>| match bg {
971 Some(bg) if bg.color.is_some_and(|c| c.3 > 0) => Some(bg.color.unwrap()),
972 _ => None,
973 };
974 let child_bg = || {
975 info.children
976 .iter()
977 .find_map(|child| solid_color(child.kind.container_bg()))
978 };
979 solid_color(info.kind.container_bg())
980 .or_else(child_bg)
981 .and_then(|_| {
982 info.kind
984 .container_bg()
985 .filter(|bg| has_visible_background(bg))
986 .or_else(|| {
987 info.children.iter().find_map(|child| {
988 child
989 .kind
990 .container_bg()
991 .filter(|bg| has_visible_background(bg))
992 })
993 })
994 })
995 }
996 }
997 }
998
999 let background = resolve_canvas_background(info);
1000 let Some(bg) = background else {
1001 let solid_color = |bg: Option<&Background>| match bg {
1004 Some(bg) if bg.color.is_some_and(|c| c.3 > 0) => Some(bg.color.unwrap()),
1005 _ => None,
1006 };
1007 let child_bg = || {
1008 info.children
1009 .iter()
1010 .find_map(|child| solid_color(child.kind.container_bg()))
1011 };
1012 let background = solid_color(info.kind.container_bg()).or_else(child_bg);
1013 let Some(color) = background else {
1014 return;
1015 };
1016 cmd_buf.push(DrawCommand::Fill {
1017 path: rect_path(0.0, 0.0, viewport.0, viewport.1),
1018 rule: FillRule::NonZero,
1019 paint: Paint {
1020 brush: Brush::Solid(color),
1021 opacity: 1.0,
1022 },
1023 });
1024 return;
1025 };
1026
1027 if let Some(color) = bg.color.filter(|c| c.3 > 0) {
1029 cmd_buf.push(DrawCommand::Fill {
1030 path: rect_path(0.0, 0.0, viewport.0, viewport.1),
1031 rule: FillRule::NonZero,
1032 paint: Paint {
1033 brush: Brush::Solid(color),
1034 opacity: 1.0,
1035 },
1036 });
1037 }
1038
1039 for layer in bg.layers.iter().rev() {
1041 match &layer.image {
1042 Some(BackgroundImage::Gradient(g)) => {
1043 cmd_buf.push(DrawCommand::Fill {
1044 path: rect_path(0.0, 0.0, viewport.0, viewport.1),
1045 rule: FillRule::NonZero,
1046 paint: Paint {
1047 brush: Brush::Gradient(g.clone()),
1048 opacity: 1.0,
1049 },
1050 });
1051 }
1052 Some(BackgroundImage::Image {
1053 image: Some(image), ..
1054 }) => {
1055 cmd_buf.push(DrawCommand::Fill {
1056 path: rect_path(0.0, 0.0, viewport.0, viewport.1),
1057 rule: FillRule::NonZero,
1058 paint: Paint {
1059 brush: Brush::Image(image.clone()),
1060 opacity: 1.0,
1061 },
1062 });
1063 }
1064 Some(BackgroundImage::Image { image: None, .. }) => {}
1065 None => {}
1066 }
1067 }
1068}
1069
1070pub fn generate_draw_commands(
1075 cmd_buf: &mut Vec<DrawCommand>,
1076 layout: &LayoutNode,
1077 info: &InfoNode,
1078 viewport: (f32, f32),
1079) {
1080 emit_canvas_background(cmd_buf, info, viewport);
1081 let (scroll_x, scroll_y) = info.kind.scroll_offsets();
1082 let root_viewport = StickyViewport {
1083 top_left: (-scroll_x, scroll_y),
1084 size: viewport,
1085 };
1086 let containing = layout
1087 .layout_box
1088 .iter()
1089 .next()
1090 .map_or((0.0, 0.0), |b| (b.content_box.width, b.content_box.height));
1091 let origin = layout
1092 .layout_box
1093 .iter()
1094 .next()
1095 .map_or((0.0, 0.0), |b| (b.content_box.x, b.content_box.y));
1096 let mut popups: Vec<(Vec<DrawCommand>, (f32, f32))> = Vec::new();
1097 generate_draw_commands_inner(
1098 cmd_buf,
1099 layout,
1100 info,
1101 (0.0, 0.0),
1102 root_viewport,
1103 containing,
1104 origin,
1105 origin,
1106 &mut popups,
1107 1.0,
1108 true,
1109 );
1110 for (commands, (tx, ty)) in popups {
1113 if tx != 0.0 || ty != 0.0 {
1114 cmd_buf.push(DrawCommand::PushTransform {
1115 transform: AffineTransform::translate(tx, ty),
1116 });
1117 }
1118 cmd_buf.extend(commands);
1119 if tx != 0.0 || ty != 0.0 {
1120 cmd_buf.push(DrawCommand::PopTransform);
1121 }
1122 }
1123}
1124
1125fn child_origin(child: &LayoutNode, parent_origin: (f32, f32)) -> (f32, f32) {
1130 child.layout_box.iter().next().map_or(parent_origin, |b| {
1131 (
1132 parent_origin.0 + b.content_box.x,
1133 parent_origin.1 + b.content_box.y,
1134 )
1135 })
1136}
1137
1138fn child_transform_origin(
1149 child: &LayoutNode,
1150 parent_origin: (f32, f32),
1151 inherited: (f32, f32),
1152) -> (f32, f32) {
1153 if matches!(child.layout_box, ui_layout::LayoutBox::InlineBox(_)) {
1154 inherited
1155 } else {
1156 child_origin(child, parent_origin)
1157 }
1158}
1159
1160#[allow(clippy::too_many_arguments)]
1180fn generate_draw_commands_inner(
1181 cmd_buf: &mut Vec<DrawCommand>,
1182 layout: &LayoutNode,
1183 info: &InfoNode,
1184 accumulated_scroll: (f32, f32),
1185 viewport: StickyViewport,
1186 containing: (f32, f32),
1187 origin: (f32, f32),
1188 transform_origin: (f32, f32),
1189 popups: &mut Vec<(Vec<DrawCommand>, (f32, f32))>,
1190 opacity: f32,
1191 is_root: bool,
1192) {
1193 let self_hidden = matches!(
1201 &info.kind,
1202 NodeKind::Container { style, .. }
1203 | NodeKind::Custom { style, .. }
1204 | NodeKind::Svg { style, .. }
1205 if matches!(style.visibility, Visibility::Hidden | Visibility::Collapse)
1206 );
1207
1208 let effective_opacity = {
1214 let own = match &info.kind {
1215 NodeKind::Container { style, .. }
1216 | NodeKind::Custom { style, .. }
1217 | NodeKind::Svg { style, .. } => style.opacity,
1218 _ => 1.0,
1219 };
1220 opacity * own
1221 };
1222
1223 let mut box_states: Vec<BoxPushState> = Vec::new();
1224
1225 let is_fixed = layout.style.position.kind == Position::Fixed;
1226 let is_sticky = layout.style.position.kind == Position::Sticky;
1227 let is_inline = matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_));
1228
1229 let establishes_inline_box_space = is_inline
1238 && layout.style.display.outer() == Some(OuterDisplay::Inline)
1239 && layout.style.display.inner() == Some(InnerDisplay::FlowRoot);
1240
1241 let flow_root_inline_dx = if establishes_inline_box_space {
1242 origin.0 - transform_origin.0
1243 } else {
1244 0.0
1245 };
1246
1247 let cancel_scroll = is_fixed && (accumulated_scroll.0 != 0.0 || accumulated_scroll.1 != 0.0);
1249 if cancel_scroll {
1250 cmd_buf.push(DrawCommand::PushTransform {
1251 transform: AffineTransform::translate(-accumulated_scroll.0, accumulated_scroll.1),
1252 });
1253 }
1254
1255 let sticky_pushed = if is_sticky && !is_fixed {
1259 match layout.layout_box.iter().next() {
1260 Some(bm) => {
1261 let (dx, dy) = bm
1262 .sticky_edges
1263 .map(|edges| sticky_offset(&edges, &bm.border_box, viewport, containing))
1264 .unwrap_or((0.0, 0.0));
1265 push_transform(cmd_buf, dx, dy)
1266 }
1267 None => false,
1268 }
1269 } else {
1270 false
1271 };
1272
1273 match &info.kind {
1274 NodeKind::Text { .. } | NodeKind::LineBreak => unreachable!(),
1275
1276 NodeKind::Container {
1277 scroll_x,
1278 scroll_y,
1279 scroll_offset_x,
1280 scroll_offset_y,
1281 style,
1282 ..
1283 } => {
1284 for box_model in &layout.layout_box {
1285 box_states.push(push_box_model(
1286 cmd_buf,
1287 &box_model,
1288 style,
1289 *scroll_offset_x,
1290 *scroll_offset_y,
1291 is_inline,
1292 *scroll_x || *scroll_y,
1293 true,
1294 !self_hidden,
1295 effective_opacity,
1296 ));
1297 }
1298 }
1299
1300 NodeKind::Svg {
1303 view_box,
1304 shapes,
1305 scroll_x,
1306 scroll_y,
1307 scroll_offset_x,
1308 scroll_offset_y,
1309 style,
1310 ..
1311 } => {
1312 for box_model in &layout.layout_box {
1313 box_states.push(push_box_model(
1314 cmd_buf,
1315 &box_model,
1316 style,
1317 *scroll_offset_x,
1318 *scroll_offset_y,
1319 is_inline,
1320 *scroll_x || *scroll_y,
1321 true,
1322 !self_hidden,
1323 effective_opacity,
1324 ));
1325 }
1326
1327 if !self_hidden && let Some(bm) = layout.layout_box.iter().next() {
1330 emit_commands(
1331 cmd_buf,
1332 (
1333 bm.content_box.x,
1334 bm.content_box.y,
1335 bm.content_box.width,
1336 bm.content_box.height,
1337 ),
1338 is_inline,
1339 *view_box,
1340 shapes,
1341 effective_opacity,
1342 );
1343 }
1344 }
1345
1346 NodeKind::Custom {
1347 scroll_x,
1348 scroll_y,
1349 scroll_offset_x,
1350 scroll_offset_y,
1351 style,
1352 layout_style,
1353 node,
1354 text_style,
1355 text_flow_style,
1356 ..
1357 } => 'custom: {
1358 for box_model in &layout.layout_box {
1359 box_states.push(push_box_model(
1360 cmd_buf,
1361 &box_model,
1362 style,
1363 *scroll_offset_x,
1364 *scroll_offset_y,
1365 false,
1366 *scroll_x || *scroll_y,
1367 true,
1368 !self_hidden,
1369 effective_opacity,
1370 ));
1371 }
1372
1373 let base_box = match &layout.layout_box {
1377 ui_layout::LayoutBox::BlockBox(bm) => Some(bm.clone()),
1378 ui_layout::LayoutBox::InlineBox(ib) => Some(ib.box_model.clone()),
1379 _ => None,
1380 };
1381 let Some(size) = base_box.as_ref().map(|bm| ContentSize {
1382 width: bm.content_box.width,
1383 height: bm.content_box.height,
1384 }) else {
1385 break 'custom;
1386 };
1387 let culled = box_states.iter().any(|s| s.cull);
1388 if !self_hidden && !culled {
1389 node.draw_sized(cmd_buf, text_style, text_flow_style, layout_style, size);
1390 }
1391 if let Some(popup) = node.popup(text_style, text_flow_style) {
1394 let own_scroll = info.kind.scroll_offsets();
1395 let inherited_scroll = if is_fixed {
1398 (0.0, 0.0)
1399 } else {
1400 accumulated_scroll
1401 };
1402 let (mut tx, mut ty) = (
1403 origin.0 - inherited_scroll.0 - own_scroll.0,
1404 origin.1 - inherited_scroll.1 - own_scroll.1,
1405 );
1406 if is_sticky
1409 && !is_fixed
1410 && let Some(bm) = layout.layout_box.iter().next()
1411 && let Some(edges) = bm.sticky_edges
1412 {
1413 let (dx, dy) = sticky_offset(&edges, &bm.border_box, viewport, containing);
1414 tx += dx;
1415 ty += dy;
1416 }
1417 popups.push((popup.commands, (tx, ty)));
1418 }
1419 }
1420 }
1421
1422 let subtree_culled = box_states.iter().any(|s| s.cull);
1425
1426 let own_scroll = info.kind.scroll_offsets();
1428 let child_scroll = if is_fixed {
1431 own_scroll
1432 } else {
1433 (
1434 accumulated_scroll.0 + own_scroll.0,
1435 accumulated_scroll.1 + own_scroll.1,
1436 )
1437 };
1438
1439 let child_viewport = if is_root || is_scrollport(&info.kind) {
1445 let bm = layout.layout_box.iter().next();
1446 let (scroll_x, scroll_y) = own_scroll;
1447 StickyViewport {
1448 top_left: bm.as_ref().map_or((0.0, 0.0), |b| {
1449 (
1450 b.padding_box.x - b.content_box.x - scroll_x,
1451 b.padding_box.y - b.content_box.y + scroll_y,
1452 )
1453 }),
1454 size: if is_root {
1455 viewport.size
1456 } else {
1457 bm.map_or(viewport.size, |b| {
1458 (b.padding_box.width, b.padding_box.height)
1459 })
1460 },
1461 }
1462 } else {
1463 let bm = layout.layout_box.iter().next();
1464 StickyViewport {
1465 top_left: bm.map_or(viewport.top_left, |b| {
1466 (
1467 viewport.top_left.0 - b.content_box.x,
1468 viewport.top_left.1 - b.content_box.y,
1469 )
1470 }),
1471 size: viewport.size,
1472 }
1473 };
1474 let child_containing = layout
1475 .layout_box
1476 .iter()
1477 .next()
1478 .map_or(containing, |b| (b.content_box.width, b.content_box.height));
1479
1480 let flow_root_inline_pushed =
1484 flow_root_inline_dx != 0.0 && push_transform(cmd_buf, flow_root_inline_dx, 0.0);
1485
1486 let mut layout_iter = layout.children.iter();
1487 let mut positive_stacking_children: Vec<(i32, usize, Vec<DrawCommand>)> = Vec::new();
1488
1489 for (child_order, child_info) in info.children.iter().enumerate() {
1490 match &child_info.kind {
1491 NodeKind::Text {
1492 text_id,
1493 style,
1494 flow_style,
1495 ..
1496 } => {
1497 if !self_hidden && !subtree_culled {
1499 draw_text(
1504 cmd_buf,
1505 style,
1506 *flow_style,
1507 *text_id,
1508 (0.0, 0.0),
1509 effective_opacity,
1510 );
1511 }
1512 layout_iter.next();
1513 }
1514 NodeKind::LineBreak => {
1515 layout_iter.next();
1516 }
1517 NodeKind::Container { .. } | NodeKind::Svg { .. } => {
1518 if subtree_culled {
1519 layout_iter.next();
1520 continue;
1521 }
1522 if let Some(LayoutChild::Node(node)) = layout_iter.next() {
1523 let child_origin = child_origin(node, origin);
1524 let inherited = if establishes_inline_box_space {
1530 origin
1531 } else {
1532 transform_origin
1533 };
1534 let child_transform_origin = child_transform_origin(node, origin, inherited);
1535 let z_index = child_info.kind.z_index();
1536 if z_index > 0 {
1537 let mut child_commands = Vec::new();
1538 generate_draw_commands_inner(
1539 &mut child_commands,
1540 node,
1541 child_info,
1542 child_scroll,
1543 child_viewport,
1544 child_containing,
1545 child_origin,
1546 child_transform_origin,
1547 popups,
1548 effective_opacity,
1549 false,
1550 );
1551 positive_stacking_children.push((z_index, child_order, child_commands));
1552 } else {
1553 generate_draw_commands_inner(
1554 cmd_buf,
1555 node,
1556 child_info,
1557 child_scroll,
1558 child_viewport,
1559 child_containing,
1560 child_origin,
1561 child_transform_origin,
1562 popups,
1563 effective_opacity,
1564 false,
1565 );
1566 }
1567 }
1568 }
1569 NodeKind::Custom {
1570 node,
1571 text_style,
1572 text_flow_style,
1573 style,
1574 layout_style,
1575 ..
1576 } => {
1577 if subtree_culled {
1578 layout_iter.next();
1579 continue;
1580 }
1581 match layout_iter.next() {
1582 Some(LayoutChild::Node(node_layout)) => {
1584 let child_origin = child_origin(node_layout, origin);
1585 generate_draw_commands_inner(
1588 cmd_buf,
1589 node_layout,
1590 child_info,
1591 child_scroll,
1592 child_viewport,
1593 child_containing,
1594 child_origin,
1595 child_origin,
1596 popups,
1597 effective_opacity,
1598 false,
1599 );
1600 }
1601 Some(LayoutChild::Custom(custom_child)) => {
1604 if let Some(result) = custom_child.result() {
1605 let bm = &result.box_model;
1606 let rect = BoxModel {
1607 sticky_edges: bm.sticky_edges,
1608 border_box: bm.border_box,
1609 padding_box: bm.padding_box,
1610 content_box: bm.content_box,
1611 children_box: bm.children_box,
1612 };
1613 let size = layout
1614 .layout_box
1615 .iter()
1616 .next()
1617 .map(|box_model| ContentSize {
1618 width: box_model.content_box.width,
1619 height: box_model.content_box.height,
1620 })
1621 .unwrap_or(ContentSize {
1622 width: rect.content_box.width,
1623 height: rect.content_box.height,
1624 });
1625 let state = push_box_model(
1628 cmd_buf,
1629 &rect,
1630 style,
1631 0.0,
1632 0.0,
1633 false,
1634 false,
1635 false,
1636 false,
1637 effective_opacity,
1638 );
1639 node.draw_sized(
1640 cmd_buf,
1641 text_style,
1642 text_flow_style,
1643 layout_style,
1644 size,
1645 );
1646 pop_box_model(cmd_buf, state);
1647 }
1648 }
1649 _ => {}
1650 }
1651 }
1652 }
1653 }
1654
1655 positive_stacking_children.sort_by_key(|(z_index, child_order, _)| (*z_index, *child_order));
1656 for (_, _, commands) in positive_stacking_children {
1657 cmd_buf.extend(commands);
1658 }
1659
1660 if matches!(
1661 info.kind,
1662 NodeKind::Container { .. } | NodeKind::Custom { .. } | NodeKind::Svg { .. }
1663 ) {
1664 for state in box_states.iter().rev() {
1665 pop_box_model(cmd_buf, *state);
1666 }
1667 }
1668
1669 if sticky_pushed {
1670 cmd_buf.push(DrawCommand::PopTransform);
1671 }
1672
1673 if cancel_scroll {
1674 cmd_buf.push(DrawCommand::PopTransform);
1675 }
1676
1677 if flow_root_inline_pushed {
1678 cmd_buf.push(DrawCommand::PopTransform);
1679 }
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684 use super::*;
1685 use crate::engine::layouter::types::{BackgroundLayer, ContainerRole};
1686 use crate::engine::renderer_model::geom::AffineTransform;
1687 use crate::engine::ui::custom_node::{CustomNode, Popup};
1688 use std::sync::Arc;
1689 use ui_layout::Style;
1690
1691 fn count_balanced(commands: &[DrawCommand]) -> bool {
1692 let mut transform_depth = 0usize;
1693 let mut clip_depth = 0usize;
1694 for cmd in commands {
1695 match cmd {
1696 DrawCommand::PushTransform { .. } => transform_depth += 1,
1697 DrawCommand::PopTransform => transform_depth -= 1,
1698 DrawCommand::PushClip { .. } => clip_depth += 1,
1699 DrawCommand::PopClip => clip_depth -= 1,
1700 _ => {}
1701 }
1702 }
1703 transform_depth == 0 && clip_depth == 0
1704 }
1705
1706 #[test]
1707 fn test_push_pop_transform_balanced() {
1708 let mut buf = Vec::new();
1709 assert!(push_transform(&mut buf, 5.0, 5.0));
1710 assert!(matches!(buf.pop(), Some(DrawCommand::PushTransform { .. })));
1711 assert!(!push_transform(&mut buf, 0.0, 0.0));
1712 }
1713
1714 #[test]
1715 fn test_radii_resolution_and_clamp() {
1716 let outer = resolve_outer_radii(&BorderRadius::default(), 100.0, 50.0);
1717 assert_eq!(outer, [(0.0, 0.0); 4]);
1718 }
1719
1720 #[test]
1721 fn scratch_background_image_geometry_is_resolved() {
1722 let (width, height) = resolve_background_image_size(
1723 1200.0,
1724 600.0,
1725 800.0,
1726 400.0,
1727 BackgroundSize::Explicit {
1728 width: BackgroundDimension::Length(624.0),
1729 height: BackgroundDimension::Length(325.0),
1730 },
1731 );
1732 assert_eq!((width, height), (624.0, 325.0));
1733 assert_eq!(
1734 resolve_background_axis(
1735 800.0,
1736 width,
1737 BackgroundPositionAxis::End(BackgroundOffset::Zero),
1738 ),
1739 176.0
1740 );
1741 assert_eq!(
1742 resolve_background_axis(
1743 400.0,
1744 height,
1745 BackgroundPositionAxis::Center(BackgroundOffset::Zero),
1746 ),
1747 37.5
1748 );
1749 }
1750
1751 #[test]
1752 fn background_contain_and_cover_preserve_aspect_ratio() {
1753 assert_eq!(
1754 resolve_background_image_size(200.0, 100.0, 300.0, 300.0, BackgroundSize::Contain,),
1755 (300.0, 150.0)
1756 );
1757 assert_eq!(
1758 resolve_background_image_size(200.0, 100.0, 300.0, 300.0, BackgroundSize::Cover),
1759 (600.0, 300.0)
1760 );
1761 }
1762
1763 #[test]
1764 fn background_color_is_painted_below_gradient_layers() {
1765 use crate::engine::layouter::types::{Gradient, GradientKind, GradientSpread};
1766
1767 let gradient = Gradient {
1768 kind: GradientKind::Linear { angle: 0.0 },
1769 stops: Vec::new(),
1770 spread: GradientSpread::Pad,
1771 };
1772 let style = ContainerStyle {
1773 background: Background {
1774 color: Some(Color(255, 0, 0, 255)),
1775 layers: vec![BackgroundLayer {
1776 image: Some(crate::engine::layouter::types::BackgroundImage::Gradient(
1777 gradient,
1778 )),
1779 position: Default::default(),
1780 size: Default::default(),
1781 repeat: Default::default(),
1782 }],
1783 },
1784 ..ContainerStyle::default()
1785 };
1786 let rect = ui_rect(0.0, 0.0, 100.0, 50.0);
1787 let mut cmd_buf = Vec::new();
1788 draw_background(&mut cmd_buf, &rect, &rect, &style, 0.0, 0.0, 1.0);
1789
1790 let mut brushes = cmd_buf.iter().filter_map(|cmd| match cmd {
1791 DrawCommand::Fill { paint, .. } => Some(paint.brush.clone()),
1792 _ => None,
1793 });
1794 assert!(matches!(
1795 brushes.next(),
1796 Some(Brush::Solid(Color(255, 0, 0, 255)))
1797 ));
1798 assert!(matches!(brushes.next(), Some(Brush::Gradient(_))));
1799 }
1800
1801 #[test]
1802 fn background_geometry_uses_the_original_layer_index() {
1803 use crate::engine::renderer_model::draw_command::Image;
1804 use crate::engine::renderer_model::path::PathCommand;
1805
1806 let path_size = |path: &Path| {
1807 let mut max = (0.0f32, 0.0f32);
1808 for command in &path.commands {
1809 match command {
1810 PathCommand::MoveTo { x, y } | PathCommand::LineTo { x, y } => {
1811 max.0 = max.0.max(*x);
1812 max.1 = max.1.max(*y);
1813 }
1814 _ => {}
1815 }
1816 }
1817 max
1818 };
1819
1820 let top = Image::from_rgba(1, 1, vec![0, 0, 0, 0]).expect("top image");
1821 let bottom = Image::from_rgba(2, 1, vec![0; 8]).expect("bottom image");
1822 let top_id = top.id();
1823 let bottom_id = bottom.id();
1824 let style = ContainerStyle {
1825 background: Background {
1826 color: None,
1827 layers: vec![
1828 BackgroundLayer {
1829 image: Some(crate::engine::layouter::types::BackgroundImage::Image {
1830 source: "top".into(),
1831 image: Some(top),
1832 }),
1833 position: Default::default(),
1834 size: BackgroundSize::Explicit {
1835 width: BackgroundDimension::Length(10.0),
1836 height: BackgroundDimension::Length(10.0),
1837 },
1838 repeat: BackgroundRepeat::NoRepeat,
1839 },
1840 BackgroundLayer {
1841 image: Some(crate::engine::layouter::types::BackgroundImage::Image {
1842 source: "bottom".into(),
1843 image: Some(bottom),
1844 }),
1845 position: Default::default(),
1846 size: BackgroundSize::Explicit {
1847 width: BackgroundDimension::Length(20.0),
1848 height: BackgroundDimension::Length(20.0),
1849 },
1850 repeat: BackgroundRepeat::NoRepeat,
1851 },
1852 ],
1853 },
1854 ..ContainerStyle::default()
1855 };
1856 let rect = ui_rect(0.0, 0.0, 100.0, 100.0);
1857 let mut cmd_buf = Vec::new();
1858 draw_background(&mut cmd_buf, &rect, &rect, &style, 0.0, 0.0, 1.0);
1859
1860 let fills: Vec<(u64, (f32, f32))> = cmd_buf
1861 .iter()
1862 .filter_map(|cmd| match cmd {
1863 DrawCommand::Fill {
1864 path,
1865 paint:
1866 Paint {
1867 brush: Brush::Image(image),
1868 ..
1869 },
1870 ..
1871 } => Some((image.id(), path_size(path))),
1872 _ => None,
1873 })
1874 .collect();
1875 assert_eq!(
1876 fills,
1877 vec![(bottom_id, (20.0, 20.0)), (top_id, (10.0, 10.0))]
1878 );
1879 }
1880
1881 fn ui_rect(x: f32, y: f32, w: f32, h: f32) -> ui_layout::Rect {
1882 ui_layout::Rect {
1883 x,
1884 y,
1885 width: w,
1886 height: h,
1887 }
1888 }
1889
1890 #[derive(Debug)]
1891 struct DrawingNode;
1892
1893 impl CustomNode for DrawingNode {
1894 fn draw_sized(
1895 &self,
1896 cmd_buf: &mut Vec<DrawCommand>,
1897 _text_style: &TextStyle,
1898 _text_flow_style: &TextFlowStyle,
1899 _style: &Style,
1900 size: ContentSize,
1901 ) {
1902 cmd_buf.push(DrawCommand::Fill {
1903 path: rect_path(0.0, 0.0, size.width, size.height),
1904 rule: FillRule::NonZero,
1905 paint: Paint {
1906 brush: Brush::Solid(Color(255, 0, 0, 255)),
1907 opacity: 1.0,
1908 },
1909 });
1910 }
1911
1912 fn intrinsic_size(&self) -> ContentSize {
1913 ContentSize {
1914 width: 10.0,
1915 height: 10.0,
1916 }
1917 }
1918 }
1919
1920 #[test]
1921 fn inline_custom_content_uses_its_flow_root_position_once() {
1922 let mut root = LayoutNode::new(Style::default());
1923 root.layout_box = ui_layout::LayoutBox::BlockBox(ui_layout::BoxModel {
1924 sticky_edges: None,
1925 border_box: ui_rect(0.0, 0.0, 200.0, 200.0),
1926 padding_box: ui_rect(0.0, 0.0, 200.0, 200.0),
1927 content_box: ui_rect(0.0, 0.0, 200.0, 200.0),
1928 children_box: ui_rect(0.0, 0.0, 200.0, 200.0),
1929 });
1930
1931 let inline_style = Style {
1932 display: ui_layout::Display::parse("inline-block").unwrap(),
1933 ..Style::default()
1934 };
1935 let mut custom = LayoutNode::new(inline_style.clone());
1936 custom.layout_box = ui_layout::LayoutBox::InlineBox(ui_layout::InlineBox {
1937 box_model: ui_layout::BoxModel {
1938 sticky_edges: None,
1939 border_box: ui_rect(3.0, 10.0, 20.0, 20.0),
1940 padding_box: ui_rect(4.0, 11.0, 18.0, 18.0),
1941 content_box: ui_rect(8.0, 15.0, 10.0, 10.0),
1942 children_box: ui_rect(8.0, 15.0, 10.0, 10.0),
1943 },
1944 line_spans: vec![ui_layout::LineSpan {
1945 x_range: 0.0..10.0,
1946 line_pos: (3.0, 10.0),
1947 line_index: 0,
1948 }],
1949 });
1950 root.children = vec![LayoutChild::Node(Box::new(custom))];
1951
1952 let root_info = mk_info_node(
1953 NodeKind::Container {
1954 scroll_x: false,
1955 scroll_y: false,
1956 scroll_offset_x: 0.0,
1957 scroll_offset_y: 0.0,
1958 style: ContainerStyle::default(),
1959 role: ContainerRole::Normal,
1960 },
1961 vec![mk_info_node(
1962 NodeKind::Custom {
1963 node: Arc::new(DrawingNode),
1964 scroll_x: false,
1965 scroll_y: false,
1966 scroll_offset_x: 0.0,
1967 scroll_offset_y: 0.0,
1968 style: ContainerStyle::default(),
1969 layout_style: inline_style,
1970 text_style: TextStyle::default(),
1971 text_flow_style: TextFlowStyle::default(),
1972 },
1973 Vec::new(),
1974 )],
1975 );
1976
1977 let mut commands = Vec::new();
1978 generate_draw_commands(&mut commands, &root, &root_info, (200.0, 200.0));
1979 assert!(count_balanced(&commands));
1980
1981 let mut translations = Vec::new();
1982 for command in commands {
1983 match command {
1984 DrawCommand::PushTransform { transform } => {
1985 translations.push(transform.apply(0.0, 0.0));
1986 }
1987 DrawCommand::Fill { paint, .. } => {
1988 if !matches!(paint.brush, Brush::Solid(Color(255, 0, 0, 255))) {
1989 translations.clear();
1990 continue;
1991 }
1992 assert_eq!(translations, [(3.0, 10.0), (5.0, 5.0)]);
1993 assert_eq!(
1994 translations
1995 .iter()
1996 .fold((0.0, 0.0), |(x, y), (dx, dy)| { (x + dx, y + dy) }),
1997 (8.0, 15.0)
1998 );
1999 return;
2000 }
2001 _ => {}
2002 }
2003 }
2004 panic!("custom content fill was not emitted");
2005 }
2006
2007 #[test]
2008 fn test_single_box_model_is_balanced() {
2009 let box_model = ui_layout::BoxModel {
2010 sticky_edges: None,
2011 border_box: ui_rect(10.0, 20.0, 120.0, 60.0),
2012 padding_box: ui_rect(12.0, 22.0, 116.0, 56.0),
2013 content_box: ui_rect(12.0, 22.0, 116.0, 56.0),
2014 children_box: ui_rect(12.0, 22.0, 116.0, 56.0),
2015 };
2016 let style = ContainerStyle::default();
2017 let mut buf = Vec::new();
2018 let state = push_box_model(
2019 &mut buf, &box_model, &style, 0.0, 0.0, false, true, true, true, 1.0,
2020 );
2021 assert!(buf.len() >= 2);
2024 assert!(!count_balanced(&buf));
2026 pop_box_model(&mut buf, state);
2027 assert!(count_balanced(&buf));
2028 }
2029
2030 #[test]
2031 fn transform_origin_is_element_center() {
2032 let box_model = ui_layout::BoxModel {
2033 sticky_edges: None,
2034 border_box: ui_rect(10.0, 20.0, 100.0, 50.0),
2035 padding_box: ui_rect(12.0, 22.0, 96.0, 46.0),
2036 content_box: ui_rect(12.0, 22.0, 96.0, 46.0),
2037 children_box: ui_rect(12.0, 22.0, 96.0, 46.0),
2038 };
2039 let style = ContainerStyle {
2040 transform: Some(AffineTransform::rotate(std::f32::consts::FRAC_PI_2)),
2041 ..Default::default()
2042 };
2043 let mut buf = Vec::new();
2044 let state = push_box_model(
2045 &mut buf, &box_model, &style, 0.0, 0.0, false, true, true, true, 1.0,
2046 );
2047 pop_box_model(&mut buf, state);
2048
2049 let transforms: Vec<&AffineTransform> = buf
2051 .iter()
2052 .filter_map(|cmd| match cmd {
2053 DrawCommand::PushTransform { transform } => Some(transform),
2054 _ => None,
2055 })
2056 .collect();
2057 assert!(transforms.len() >= 2);
2058 let centered = transforms[1];
2059
2060 let expected = AffineTransform::translate(50.0, 25.0)
2062 .then(&AffineTransform::rotate(std::f32::consts::FRAC_PI_2))
2063 .then(&AffineTransform::translate(-50.0, -25.0));
2064 assert!((centered.m11 - expected.m11).abs() < 1e-5);
2065 assert!((centered.m12 - expected.m12).abs() < 1e-5);
2066 assert!((centered.m21 - expected.m21).abs() < 1e-5);
2067 assert!((centered.m22 - expected.m22).abs() < 1e-5);
2068 assert!((centered.dx - expected.dx).abs() < 1e-5);
2069 assert!((centered.dy - expected.dy).abs() < 1e-5);
2070
2071 let (cx, cy) = centered.apply(50.0, 25.0);
2073 assert!((cx - 50.0).abs() < 1e-5, "center x = {cx}");
2074 assert!((cy - 25.0).abs() < 1e-5, "center y = {cy}");
2075 }
2076
2077 #[test]
2078 fn visible_overflow_does_not_clip_block_contents() {
2079 let box_model = ui_layout::BoxModel {
2080 sticky_edges: None,
2081 border_box: ui_rect(0.0, 0.0, 100.0, 50.0),
2082 padding_box: ui_rect(0.0, 0.0, 100.0, 50.0),
2083 content_box: ui_rect(0.0, 0.0, 100.0, 50.0),
2084 children_box: ui_rect(0.0, 0.0, 120.0, 60.0),
2085 };
2086 let mut commands = Vec::new();
2087 let state = push_box_model(
2088 &mut commands,
2089 &box_model,
2090 &ContainerStyle::default(),
2091 0.0,
2092 0.0,
2093 false,
2094 false,
2095 true,
2096 true,
2097 1.0,
2098 );
2099 assert!(
2100 !commands
2101 .iter()
2102 .any(|command| matches!(command, DrawCommand::PushClip { .. }))
2103 );
2104 pop_box_model(&mut commands, state);
2105 assert!(count_balanced(&commands));
2106 }
2107
2108 #[test]
2109 fn test_nested_box_models_balanced() {
2110 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2111 sticky_edges: None,
2112 border_box: ui_rect(x, y, w, h),
2113 padding_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
2114 content_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
2115 children_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
2116 };
2117 let style = ContainerStyle::default();
2118 let mut buf = Vec::new();
2119 let outer = push_box_model(
2120 &mut buf,
2121 &mk_box(0.0, 0.0, 100.0, 100.0),
2122 &style,
2123 3.0,
2124 4.0,
2125 false,
2126 true,
2127 true,
2128 true,
2129 1.0,
2130 );
2131 let inner = push_box_model(
2132 &mut buf,
2133 &mk_box(10.0, 10.0, 50.0, 50.0),
2134 &style,
2135 0.0,
2136 0.0,
2137 false,
2138 true,
2139 true,
2140 true,
2141 1.0,
2142 );
2143 assert!(!buf.is_empty());
2145 pop_box_model(&mut buf, inner);
2146 pop_box_model(&mut buf, outer);
2147 assert!(count_balanced(&buf));
2148 }
2149
2150 #[test]
2151 fn test_affine_transform_reexport() {
2152 let t = AffineTransform::translate(1.0, 2.0);
2153 assert_eq!(t.apply(0.0, 0.0), (1.0, 2.0));
2154 }
2155
2156 fn mk_info_node(kind: NodeKind, children: Vec<InfoNode>) -> InfoNode {
2159 InfoNode {
2160 kind,
2161 children,
2162 dom_id: None,
2163 }
2164 }
2165
2166 fn hidden_container_info(children: Vec<InfoNode>) -> InfoNode {
2167 let style = ContainerStyle {
2168 visibility: Visibility::Hidden,
2169 ..ContainerStyle::default()
2170 };
2171 mk_info_node(
2172 NodeKind::Container {
2173 scroll_x: false,
2174 scroll_y: false,
2175 scroll_offset_x: 0.0,
2176 scroll_offset_y: 0.0,
2177 style,
2178 role: ContainerRole::Normal,
2179 },
2180 children,
2181 )
2182 }
2183
2184 fn visible_container_info(children: Vec<InfoNode>) -> InfoNode {
2185 let style = ContainerStyle {
2186 background: Background {
2187 color: Some(Color(255, 0, 0, 255)),
2188 layers: Vec::new(),
2189 },
2190 ..ContainerStyle::default()
2191 };
2192 mk_info_node(
2193 NodeKind::Container {
2194 scroll_x: false,
2195 scroll_y: false,
2196 scroll_offset_x: 0.0,
2197 scroll_offset_y: 0.0,
2198 style,
2199 role: ContainerRole::Normal,
2200 },
2201 children,
2202 )
2203 }
2204
2205 #[test]
2212 fn hidden_ancestor_does_not_prune_visible_descendants() {
2213 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
2214 x,
2215 y,
2216 width: w,
2217 height: h,
2218 };
2219 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2220 sticky_edges: None,
2221 border_box: ui_rect(x, y, w, h),
2222 padding_box: ui_rect(x, y, w, h),
2223 content_box: ui_rect(x, y, w, h),
2224 children_box: ui_rect(x, y, w, h),
2225 };
2226
2227 let mut hidden_root = LayoutNode::new(Style::default());
2229 hidden_root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 100.0, 100.0));
2230 let mut visible_child = LayoutNode::new(Style::default());
2231 visible_child.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 50.0, 50.0));
2232 hidden_root.children = vec![LayoutChild::Node(Box::new(visible_child))];
2233
2234 let hidden_info = hidden_container_info(vec![visible_container_info(Vec::new())]);
2235
2236 let mut commands = Vec::new();
2237 generate_draw_commands(&mut commands, &hidden_root, &hidden_info, (100.0, 100.0));
2238
2239 assert!(
2241 !commands.is_empty(),
2242 "visible descendants of a hidden ancestor must still draw"
2243 );
2244 assert!(count_balanced(&commands));
2245 }
2246
2247 #[test]
2250 fn fully_hidden_subtree_paints_nothing() {
2251 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
2252 x,
2253 y,
2254 width: w,
2255 height: h,
2256 };
2257 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2258 sticky_edges: None,
2259 border_box: ui_rect(x, y, w, h),
2260 padding_box: ui_rect(x, y, w, h),
2261 content_box: ui_rect(x, y, w, h),
2262 children_box: ui_rect(x, y, w, h),
2263 };
2264
2265 let mut hidden_root = LayoutNode::new(Style::default());
2266 hidden_root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 100.0, 100.0));
2267 let mut hidden_child = LayoutNode::new(Style::default());
2268 hidden_child.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 50.0, 50.0));
2269 hidden_root.children = vec![LayoutChild::Node(Box::new(hidden_child))];
2270
2271 let hidden_info = hidden_container_info(vec![hidden_container_info(Vec::new())]);
2272
2273 let mut commands = Vec::new();
2274 generate_draw_commands(&mut commands, &hidden_root, &hidden_info, (100.0, 100.0));
2275
2276 assert!(
2277 commands.is_empty(),
2278 "a fully hidden subtree must not paint, got {commands:?}"
2279 );
2280 }
2281
2282 fn scroll_translates(commands: &[DrawCommand]) -> Vec<(f32, f32)> {
2285 let mut tx: Vec<f32> = Vec::new();
2286 let mut ty: Vec<f32> = Vec::new();
2287 let mut out = Vec::new();
2288 for cmd in commands {
2289 match cmd {
2290 DrawCommand::PushTransform { transform } => {
2291 tx.push(transform.apply(0.0, 0.0).0);
2292 ty.push(transform.apply(0.0, 0.0).1);
2293 out.push((transform.apply(0.0, 0.0).0, transform.apply(0.0, 0.0).1));
2294 }
2295 DrawCommand::PopTransform => {
2296 if let (Some(x), Some(y)) = (tx.pop(), ty.pop()) {
2297 out.push((-x, -y));
2298 }
2299 }
2300 _ => {}
2301 }
2302 }
2303 out
2304 }
2305
2306 #[test]
2307 fn fixed_node_cancels_inherited_scroll_offset() {
2308 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
2309 x,
2310 y,
2311 width: w,
2312 height: h,
2313 };
2314 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2315 sticky_edges: None,
2316 border_box: ui_rect(x, y, w, h),
2317 padding_box: ui_rect(x, y, w, h),
2318 content_box: ui_rect(x, y, w, h),
2319 children_box: ui_rect(x, y, w, h),
2320 };
2321
2322 let scroller_style = Style::default();
2324 let mut scroller = LayoutNode::new(scroller_style);
2325 scroller.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 100.0, 100.0));
2326
2327 let mut fixed_style = Style::default();
2328 fixed_style.position.kind = Position::Fixed;
2329 let fixed = LayoutNode::new(fixed_style);
2330 scroller.children = vec![LayoutChild::Node(Box::new(fixed))];
2331
2332 let scroller_info = mk_info_node(
2333 NodeKind::Container {
2334 scroll_x: true,
2335 scroll_y: true,
2336 scroll_offset_x: 0.0,
2337 scroll_offset_y: 50.0,
2338 style: ContainerStyle::default(),
2339 role: ContainerRole::Normal,
2340 },
2341 vec![mk_info_node(
2342 NodeKind::Container {
2343 scroll_x: false,
2344 scroll_y: false,
2345 scroll_offset_x: 0.0,
2346 scroll_offset_y: 0.0,
2347 style: ContainerStyle::default(),
2348 role: ContainerRole::Normal,
2349 },
2350 Vec::new(),
2351 )],
2352 );
2353
2354 let mut commands = Vec::new();
2355 generate_draw_commands(&mut commands, &scroller, &scroller_info, (100.0, 100.0));
2356 assert!(count_balanced(&commands));
2357
2358 let translates = scroll_translates(&commands);
2361 assert!(
2362 translates.contains(&(0.0, 50.0)),
2363 "expected a scroll-cancelling transform, got {translates:?}"
2364 );
2365 }
2366
2367 #[test]
2368 fn positive_z_index_child_paints_after_later_normal_sibling() {
2369 let box_model = |x: f32| ui_layout::BoxModel {
2370 sticky_edges: None,
2371 border_box: ui_rect(x, 0.0, 50.0, 50.0),
2372 padding_box: ui_rect(x, 0.0, 50.0, 50.0),
2373 content_box: ui_rect(x, 0.0, 50.0, 50.0),
2374 children_box: ui_rect(x, 0.0, 50.0, 50.0),
2375 };
2376 let mut root = LayoutNode::new(Style::default());
2377 root.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
2378 let mut front = LayoutNode::new(Style::default());
2379 front.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
2380 let mut normal = LayoutNode::new(Style::default());
2381 normal.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
2382 root.children = vec![
2383 LayoutChild::Node(Box::new(front)),
2384 LayoutChild::Node(Box::new(normal)),
2385 ];
2386
2387 let child_info = |color, z_index| {
2388 mk_info_node(
2389 NodeKind::Container {
2390 scroll_x: false,
2391 scroll_y: false,
2392 scroll_offset_x: 0.0,
2393 scroll_offset_y: 0.0,
2394 style: ContainerStyle {
2395 background: Background {
2396 color: Some(color),
2397 layers: Vec::new(),
2398 },
2399 z_index,
2400 ..ContainerStyle::default()
2401 },
2402 role: ContainerRole::Normal,
2403 },
2404 Vec::new(),
2405 )
2406 };
2407 let root_info = mk_info_node(
2408 NodeKind::Container {
2409 scroll_x: false,
2410 scroll_y: false,
2411 scroll_offset_x: 0.0,
2412 scroll_offset_y: 0.0,
2413 style: ContainerStyle::default(),
2414 role: ContainerRole::Normal,
2415 },
2416 vec![
2417 child_info(Color(255, 0, 0, 255), Some(10)),
2418 child_info(Color(0, 0, 255, 255), None),
2419 ],
2420 );
2421
2422 let mut commands = Vec::new();
2423 generate_draw_commands(&mut commands, &root, &root_info, (100.0, 100.0));
2424 let colors: Vec<_> = commands
2425 .iter()
2426 .filter_map(|command| match command {
2427 DrawCommand::Fill {
2428 paint:
2429 Paint {
2430 brush: Brush::Solid(color),
2431 ..
2432 },
2433 ..
2434 } => Some(*color),
2435 _ => None,
2436 })
2437 .collect();
2438 assert_eq!(
2439 colors,
2440 vec![
2441 Color(255, 0, 0, 255),
2442 Color(0, 0, 255, 255),
2443 Color(255, 0, 0, 255)
2444 ]
2445 );
2446 assert!(count_balanced(&commands));
2447 }
2448
2449 fn sticky_viewport(scroll: (f32, f32)) -> StickyViewport {
2450 StickyViewport {
2451 top_left: (-scroll.0, scroll.1),
2452 size: (800.0, 500.0),
2453 }
2454 }
2455
2456 fn sticky_edges(top: Option<f32>, bottom: Option<f32>) -> EdgeOption {
2457 EdgeOption {
2458 left: None,
2459 top,
2460 right: None,
2461 bottom,
2462 }
2463 }
2464
2465 #[test]
2466 fn test_sticky_offset_top_sticks_to_viewport_top() {
2467 let (dx, dy) = sticky_offset(
2470 &sticky_edges(Some(10.0), None),
2471 &ui_rect(0.0, 600.0, 100.0, 50.0),
2472 sticky_viewport((0.0, 600.0)),
2473 (800.0, 2000.0),
2474 );
2475 assert_eq!((dx, dy), (0.0, 10.0));
2476 }
2477
2478 #[test]
2479 fn test_sticky_offset_bottom_pushes_up() {
2480 let (dx, dy) = sticky_offset(
2483 &sticky_edges(None, Some(10.0)),
2484 &ui_rect(0.0, 900.0, 100.0, 100.0),
2485 sticky_viewport((0.0, 0.0)),
2486 (800.0, 2000.0),
2487 );
2488 assert_eq!((dx, dy), (0.0, -510.0));
2489 }
2490
2491 #[test]
2492 fn test_sticky_offset_no_movement_when_in_view() {
2493 let (dx, dy) = sticky_offset(
2494 &sticky_edges(Some(10.0), None),
2495 &ui_rect(0.0, 50.0, 100.0, 50.0),
2496 sticky_viewport((0.0, 0.0)),
2497 (800.0, 2000.0),
2498 );
2499 assert_eq!((dx, dy), (0.0, 0.0));
2500 }
2501
2502 #[test]
2503 fn test_sticky_offset_containing_block_hi_clamp() {
2504 let (dx, dy) = sticky_offset(
2508 &sticky_edges(Some(10.0), None),
2509 &ui_rect(0.0, 900.0, 100.0, 400.0),
2510 sticky_viewport((0.0, 900.0)),
2511 (800.0, 1300.0),
2512 );
2513 assert_eq!((dx, dy), (0.0, 0.0));
2514 }
2515
2516 #[test]
2517 fn test_sticky_offset_inset_fit_ignores_end_edge_without_room() {
2518 let (dx, dy) = sticky_offset(
2521 &sticky_edges(Some(10.0), Some(10.0)),
2522 &ui_rect(0.0, 100.0, 100.0, 500.0),
2523 sticky_viewport((0.0, 0.0)),
2524 (800.0, 2000.0),
2525 );
2526 assert_eq!((dx, dy), (0.0, 0.0));
2527 }
2528
2529 #[test]
2530 fn test_sticky_offset_horizontal_right_pushes_left() {
2531 let edges = EdgeOption {
2534 left: None,
2535 top: None,
2536 right: Some(10.0),
2537 bottom: None,
2538 };
2539 let (dx, dy) = sticky_offset(
2540 &edges,
2541 &ui_rect(850.0, 0.0, 100.0, 50.0),
2542 sticky_viewport((0.0, 0.0)),
2543 (2000.0, 2000.0),
2544 );
2545 assert_eq!((dx, dy), (-160.0, 0.0));
2546 }
2547
2548 #[test]
2549 fn sticky_node_pushes_sticky_offset_transform() {
2550 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
2553 x,
2554 y,
2555 width: w,
2556 height: h,
2557 };
2558 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2559 sticky_edges: None,
2560 border_box: ui_rect(x, y, w, h),
2561 padding_box: ui_rect(x, y, w, h),
2562 content_box: ui_rect(x, y, w, h),
2563 children_box: ui_rect(x, y, w, h),
2564 };
2565
2566 let root_style = Style::default();
2567 let mut root = LayoutNode::new(root_style);
2568 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 800.0, 2000.0));
2569
2570 let mut sticky_style = Style::default();
2571 sticky_style.position.kind = Position::Sticky;
2572 let mut sticky = LayoutNode::new(sticky_style);
2573 let mut sticky_bm = mk_box(0.0, 600.0, 800.0, 100.0);
2574 sticky_bm.sticky_edges = Some(sticky_edges(Some(10.0), None));
2575 sticky.layout_box = ui_layout::LayoutBox::BlockBox(sticky_bm);
2576 root.children = vec![LayoutChild::Node(Box::new(sticky))];
2577
2578 let root_info = mk_info_node(
2579 NodeKind::Container {
2580 scroll_x: false,
2581 scroll_y: false,
2582 scroll_offset_x: 0.0,
2583 scroll_offset_y: 600.0,
2584 style: ContainerStyle::default(),
2585 role: ContainerRole::Normal,
2586 },
2587 vec![mk_info_node(
2588 NodeKind::Container {
2589 scroll_x: false,
2590 scroll_y: false,
2591 scroll_offset_x: 0.0,
2592 scroll_offset_y: 0.0,
2593 style: ContainerStyle::default(),
2594 role: ContainerRole::Normal,
2595 },
2596 Vec::new(),
2597 )],
2598 );
2599
2600 let mut commands = Vec::new();
2601 generate_draw_commands(&mut commands, &root, &root_info, (800.0, 500.0));
2602 assert!(count_balanced(&commands));
2603
2604 let translates = scroll_translates(&commands);
2605 assert!(
2606 translates.contains(&(0.0, 10.0)),
2607 "expected a sticky top offset, got {translates:?}"
2608 );
2609 }
2610
2611 #[test]
2612 fn sticky_node_bottom_pushes_up() {
2613 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
2615 x,
2616 y,
2617 width: w,
2618 height: h,
2619 };
2620 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2621 sticky_edges: None,
2622 border_box: ui_rect(x, y, w, h),
2623 padding_box: ui_rect(x, y, w, h),
2624 content_box: ui_rect(x, y, w, h),
2625 children_box: ui_rect(x, y, w, h),
2626 };
2627
2628 let root_style = Style::default();
2629 let mut root = LayoutNode::new(root_style);
2630 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 800.0, 2000.0));
2631
2632 let mut sticky_style = Style::default();
2633 sticky_style.position.kind = Position::Sticky;
2634 let mut sticky = LayoutNode::new(sticky_style);
2635 let mut sticky_bm = mk_box(0.0, 900.0, 800.0, 100.0);
2636 sticky_bm.sticky_edges = Some(sticky_edges(None, Some(10.0)));
2637 sticky.layout_box = ui_layout::LayoutBox::BlockBox(sticky_bm);
2638 root.children = vec![LayoutChild::Node(Box::new(sticky))];
2639
2640 let root_info = mk_info_node(
2641 NodeKind::Container {
2642 scroll_x: false,
2643 scroll_y: false,
2644 scroll_offset_x: 0.0,
2645 scroll_offset_y: 0.0,
2646 style: ContainerStyle::default(),
2647 role: ContainerRole::Normal,
2648 },
2649 vec![mk_info_node(
2650 NodeKind::Container {
2651 scroll_x: false,
2652 scroll_y: false,
2653 scroll_offset_x: 0.0,
2654 scroll_offset_y: 0.0,
2655 style: ContainerStyle::default(),
2656 role: ContainerRole::Normal,
2657 },
2658 Vec::new(),
2659 )],
2660 );
2661
2662 let mut commands = Vec::new();
2663 generate_draw_commands(&mut commands, &root, &root_info, (800.0, 500.0));
2664 assert!(count_balanced(&commands));
2665
2666 let translates = scroll_translates(&commands);
2667 assert!(
2668 translates.contains(&(0.0, -510.0)),
2669 "expected a sticky bottom offset, got {translates:?}"
2670 );
2671 }
2672
2673 #[derive(Debug)]
2675 struct PopupNode {
2676 open: bool,
2677 box_height: f32,
2678 popup_height: f32,
2679 }
2680
2681 impl CustomNode for PopupNode {
2682 fn draw_sized(
2683 &self,
2684 _cmd_buf: &mut Vec<DrawCommand>,
2685 _text_style: &TextStyle,
2686 _text_flow_style: &TextFlowStyle,
2687 _style: &Style,
2688 _size: ContentSize,
2689 ) {
2690 }
2691
2692 fn intrinsic_size(&self) -> ContentSize {
2693 ContentSize {
2694 width: 120.0,
2695 height: self.box_height,
2696 }
2697 }
2698
2699 fn popup(
2700 &self,
2701 _text_style: &TextStyle,
2702 _text_flow_style: &TextFlowStyle,
2703 ) -> Option<Popup> {
2704 self.open.then(|| Popup {
2705 rect: crate::engine::renderer_model::Rect {
2706 x: 0.0,
2707 y: self.box_height,
2708 width: 120.0,
2709 height: self.popup_height,
2710 },
2711 commands: vec![DrawCommand::Fill {
2712 path: rect_path(0.0, self.box_height, 120.0, self.popup_height),
2713 rule: FillRule::NonZero,
2714 paint: Paint {
2715 brush: Brush::Solid(Color(255, 0, 0, 255)),
2716 opacity: 1.0,
2717 },
2718 }],
2719 })
2720 }
2721 }
2722
2723 #[test]
2724 fn popup_is_emitted_as_top_layer_at_node_position() {
2725 let node: Arc<dyn CustomNode> = Arc::new(PopupNode {
2729 open: true,
2730 box_height: 28.0,
2731 popup_height: 84.0,
2732 });
2733
2734 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2735 sticky_edges: None,
2736 border_box: ui_rect(x, y, w, h),
2737 padding_box: ui_rect(x, y, w, h),
2738 content_box: ui_rect(x, y, w, h),
2739 children_box: ui_rect(x, y, w, h),
2740 };
2741
2742 let mut root = LayoutNode::new(Style::default());
2743 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 200.0, 200.0));
2744
2745 let mut scroller = LayoutNode::new(Style::default());
2746 scroller.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(10.0, 20.0, 160.0, 100.0));
2747
2748 let mut custom = LayoutNode::new(Style::default());
2749 custom.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(5.0, 0.0, 120.0, 28.0));
2750 scroller.children = vec![LayoutChild::Node(Box::new(custom))];
2751 root.children = vec![LayoutChild::Node(Box::new(scroller))];
2752
2753 let root_info = mk_info_node(
2754 NodeKind::Container {
2755 scroll_x: false,
2756 scroll_y: false,
2757 scroll_offset_x: 0.0,
2758 scroll_offset_y: 0.0,
2759 style: ContainerStyle::default(),
2760 role: ContainerRole::Normal,
2761 },
2762 vec![mk_info_node(
2763 NodeKind::Container {
2764 scroll_x: false,
2765 scroll_y: true,
2766 scroll_offset_x: 0.0,
2767 scroll_offset_y: 50.0,
2768 style: ContainerStyle::default(),
2769 role: ContainerRole::Normal,
2770 },
2771 vec![mk_info_node(
2772 NodeKind::Custom {
2773 node,
2774 scroll_x: false,
2775 scroll_y: false,
2776 scroll_offset_x: 0.0,
2777 scroll_offset_y: 0.0,
2778 style: ContainerStyle::default(),
2779 layout_style: Style::default(),
2780 text_style: TextStyle::default(),
2781 text_flow_style: TextFlowStyle::default(),
2782 },
2783 Vec::new(),
2784 )],
2785 )],
2786 );
2787
2788 let mut commands = Vec::new();
2789 generate_draw_commands(&mut commands, &root, &root_info, (200.0, 200.0));
2790 assert!(count_balanced(&commands));
2791
2792 let fills = commands
2795 .iter()
2796 .filter(|cmd| matches!(cmd, DrawCommand::Fill { .. }))
2797 .count();
2798 assert_eq!(fills, 1, "popup must be the only painted box");
2799
2800 match &commands[commands.len() - 3..] {
2801 [
2802 DrawCommand::PushTransform { transform },
2803 DrawCommand::Fill { .. },
2804 DrawCommand::PopTransform,
2805 ] => {
2806 assert_eq!(transform.apply(0.0, 0.0), (15.0, -30.0));
2807 }
2808 _ => panic!("expected trailing popup PushTransform/Fill/PopTransform"),
2809 }
2810 }
2811
2812 #[test]
2813 fn closed_popup_is_not_emitted() {
2814 let node: Arc<dyn CustomNode> = Arc::new(PopupNode {
2815 open: false,
2816 box_height: 28.0,
2817 popup_height: 84.0,
2818 });
2819
2820 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
2821 sticky_edges: None,
2822 border_box: ui_rect(x, y, w, h),
2823 padding_box: ui_rect(x, y, w, h),
2824 content_box: ui_rect(x, y, w, h),
2825 children_box: ui_rect(x, y, w, h),
2826 };
2827
2828 let mut root = LayoutNode::new(Style::default());
2829 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 200.0, 200.0));
2830 let mut custom = LayoutNode::new(Style::default());
2831 custom.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 120.0, 28.0));
2832 root.children = vec![LayoutChild::Node(Box::new(custom))];
2833
2834 let root_info = mk_info_node(
2835 NodeKind::Container {
2836 scroll_x: false,
2837 scroll_y: false,
2838 scroll_offset_x: 0.0,
2839 scroll_offset_y: 0.0,
2840 style: ContainerStyle::default(),
2841 role: ContainerRole::Normal,
2842 },
2843 vec![mk_info_node(
2844 NodeKind::Custom {
2845 node,
2846 scroll_x: false,
2847 scroll_y: false,
2848 scroll_offset_x: 0.0,
2849 scroll_offset_y: 0.0,
2850 style: ContainerStyle::default(),
2851 layout_style: Style::default(),
2852 text_style: TextStyle::default(),
2853 text_flow_style: TextFlowStyle::default(),
2854 },
2855 Vec::new(),
2856 )],
2857 );
2858
2859 let mut commands = Vec::new();
2860 generate_draw_commands(&mut commands, &root, &root_info, (200.0, 200.0));
2861 assert!(count_balanced(&commands));
2862 assert_eq!(
2863 commands
2864 .iter()
2865 .filter(|cmd| matches!(cmd, DrawCommand::Fill { .. }))
2866 .count(),
2867 0
2868 );
2869 }
2870
2871 #[test]
2872 fn canvas_background_inherits_from_body_when_html_has_none() {
2873 let body_info = mk_info_node(
2875 NodeKind::Container {
2876 scroll_x: false,
2877 scroll_y: false,
2878 scroll_offset_x: 0.0,
2879 scroll_offset_y: 0.0,
2880 style: ContainerStyle {
2881 background: Background {
2882 color: Some(Color(255, 0, 0, 255)),
2883 layers: Vec::new(),
2884 },
2885 tag: Some("body".to_string()),
2886 ..ContainerStyle::default()
2887 },
2888 role: ContainerRole::Normal,
2889 },
2890 Vec::new(),
2891 );
2892 let html_info = mk_info_node(
2893 NodeKind::Container {
2894 scroll_x: false,
2895 scroll_y: false,
2896 scroll_offset_x: 0.0,
2897 scroll_offset_y: 0.0,
2898 style: ContainerStyle {
2899 background: Background::default(),
2900 tag: Some("html".to_string()),
2901 ..ContainerStyle::default()
2902 },
2903 role: ContainerRole::Normal,
2904 },
2905 vec![body_info],
2906 );
2907 let root_info = mk_info_node(
2908 NodeKind::Container {
2909 scroll_x: false,
2910 scroll_y: false,
2911 scroll_offset_x: 0.0,
2912 scroll_offset_y: 0.0,
2913 style: ContainerStyle::default(),
2914 role: ContainerRole::Normal,
2915 },
2916 vec![html_info],
2917 );
2918
2919 let mut commands = Vec::new();
2920 generate_draw_commands(
2921 &mut commands,
2922 &LayoutNode::new(Style::default()),
2923 &root_info,
2924 (100.0, 100.0),
2925 );
2926
2927 let fills: Vec<_> = commands
2928 .iter()
2929 .filter_map(|cmd| match cmd {
2930 DrawCommand::Fill {
2931 paint:
2932 Paint {
2933 brush: Brush::Solid(color),
2934 ..
2935 },
2936 ..
2937 } => Some(*color),
2938 _ => None,
2939 })
2940 .collect();
2941
2942 assert!(
2944 fills.contains(&Color(255, 0, 0, 255)),
2945 "canvas should paint body's red background, got {fills:?}"
2946 );
2947 }
2948
2949 #[test]
2950 fn canvas_background_uses_html_when_both_specified() {
2951 let body_info = mk_info_node(
2953 NodeKind::Container {
2954 scroll_x: false,
2955 scroll_y: false,
2956 scroll_offset_x: 0.0,
2957 scroll_offset_y: 0.0,
2958 style: ContainerStyle {
2959 background: Background {
2960 color: Some(Color(255, 0, 0, 255)),
2961 layers: Vec::new(),
2962 },
2963 tag: Some("body".to_string()),
2964 ..ContainerStyle::default()
2965 },
2966 role: ContainerRole::Normal,
2967 },
2968 Vec::new(),
2969 );
2970 let html_info = mk_info_node(
2971 NodeKind::Container {
2972 scroll_x: false,
2973 scroll_y: false,
2974 scroll_offset_x: 0.0,
2975 scroll_offset_y: 0.0,
2976 style: ContainerStyle {
2977 background: Background {
2978 color: Some(Color(0, 255, 0, 255)),
2979 layers: Vec::new(),
2980 },
2981 tag: Some("html".to_string()),
2982 ..ContainerStyle::default()
2983 },
2984 role: ContainerRole::Normal,
2985 },
2986 vec![body_info],
2987 );
2988 let root_info = mk_info_node(
2989 NodeKind::Container {
2990 scroll_x: false,
2991 scroll_y: false,
2992 scroll_offset_x: 0.0,
2993 scroll_offset_y: 0.0,
2994 style: ContainerStyle::default(),
2995 role: ContainerRole::Normal,
2996 },
2997 vec![html_info],
2998 );
2999
3000 let mut commands = Vec::new();
3001 generate_draw_commands(
3002 &mut commands,
3003 &LayoutNode::new(Style::default()),
3004 &root_info,
3005 (100.0, 100.0),
3006 );
3007
3008 let fills: Vec<_> = commands
3009 .iter()
3010 .filter_map(|cmd| match cmd {
3011 DrawCommand::Fill {
3012 paint:
3013 Paint {
3014 brush: Brush::Solid(color),
3015 ..
3016 },
3017 ..
3018 } => Some(*color),
3019 _ => None,
3020 })
3021 .collect();
3022
3023 assert!(
3024 fills.contains(&Color(0, 255, 0, 255)),
3025 "canvas should paint html's green background, got {fills:?}"
3026 );
3027 assert!(
3028 fills[0] == Color(0, 255, 0, 255),
3029 "first canvas fill should be html's green background"
3030 );
3031 }
3032
3033 #[test]
3034 fn canvas_background_transparent_when_neither_html_nor_body_has_background() {
3035 let body_info = mk_info_node(
3036 NodeKind::Container {
3037 scroll_x: false,
3038 scroll_y: false,
3039 scroll_offset_x: 0.0,
3040 scroll_offset_y: 0.0,
3041 style: ContainerStyle {
3042 tag: Some("body".to_string()),
3043 ..ContainerStyle::default()
3044 },
3045 role: ContainerRole::Normal,
3046 },
3047 Vec::new(),
3048 );
3049 let html_info = mk_info_node(
3050 NodeKind::Container {
3051 scroll_x: false,
3052 scroll_y: false,
3053 scroll_offset_x: 0.0,
3054 scroll_offset_y: 0.0,
3055 style: ContainerStyle {
3056 tag: Some("html".to_string()),
3057 ..ContainerStyle::default()
3058 },
3059 role: ContainerRole::Normal,
3060 },
3061 vec![body_info],
3062 );
3063 let root_info = mk_info_node(
3064 NodeKind::Container {
3065 scroll_x: false,
3066 scroll_y: false,
3067 scroll_offset_x: 0.0,
3068 scroll_offset_y: 0.0,
3069 style: ContainerStyle::default(),
3070 role: ContainerRole::Normal,
3071 },
3072 vec![html_info],
3073 );
3074
3075 let mut commands = Vec::new();
3076 generate_draw_commands(
3077 &mut commands,
3078 &LayoutNode::new(Style::default()),
3079 &root_info,
3080 (100.0, 100.0),
3081 );
3082
3083 let fill_count = commands
3084 .iter()
3085 .filter(|cmd| matches!(cmd, DrawCommand::Fill { .. }))
3086 .count();
3087 assert_eq!(
3088 fill_count, 0,
3089 "no canvas background fill when neither html nor body has background"
3090 );
3091 }
3092
3093 #[test]
3094 fn canvas_background_inherits_body_gradient_when_html_has_none() {
3095 use crate::engine::layouter::types::{Gradient, GradientKind, GradientSpread};
3096
3097 let gradient = Gradient {
3098 kind: GradientKind::Linear { angle: 0.0 },
3099 stops: Vec::new(),
3100 spread: GradientSpread::Pad,
3101 };
3102 let body_info = mk_info_node(
3103 NodeKind::Container {
3104 scroll_x: false,
3105 scroll_y: false,
3106 scroll_offset_x: 0.0,
3107 scroll_offset_y: 0.0,
3108 style: ContainerStyle {
3109 background: Background {
3110 color: None,
3111 layers: vec![crate::engine::layouter::types::BackgroundLayer {
3112 image: Some(crate::engine::layouter::types::BackgroundImage::Gradient(
3113 gradient,
3114 )),
3115 position: Default::default(),
3116 size: Default::default(),
3117 repeat: Default::default(),
3118 }],
3119 },
3120 tag: Some("body".to_string()),
3121 ..ContainerStyle::default()
3122 },
3123 role: ContainerRole::Normal,
3124 },
3125 Vec::new(),
3126 );
3127 let html_info = mk_info_node(
3128 NodeKind::Container {
3129 scroll_x: false,
3130 scroll_y: false,
3131 scroll_offset_x: 0.0,
3132 scroll_offset_y: 0.0,
3133 style: ContainerStyle {
3134 background: Background::default(),
3135 tag: Some("html".to_string()),
3136 ..ContainerStyle::default()
3137 },
3138 role: ContainerRole::Normal,
3139 },
3140 vec![body_info],
3141 );
3142 let root_info = mk_info_node(
3143 NodeKind::Container {
3144 scroll_x: false,
3145 scroll_y: false,
3146 scroll_offset_x: 0.0,
3147 scroll_offset_y: 0.0,
3148 style: ContainerStyle::default(),
3149 role: ContainerRole::Normal,
3150 },
3151 vec![html_info],
3152 );
3153
3154 let mut commands = Vec::new();
3155 generate_draw_commands(
3156 &mut commands,
3157 &LayoutNode::new(Style::default()),
3158 &root_info,
3159 (100.0, 100.0),
3160 );
3161
3162 let gradient_count = commands
3163 .iter()
3164 .filter(|cmd| {
3165 matches!(
3166 cmd,
3167 DrawCommand::Fill {
3168 paint: Paint {
3169 brush: Brush::Gradient(_),
3170 ..
3171 },
3172 ..
3173 }
3174 )
3175 })
3176 .count();
3177
3178 assert!(
3179 gradient_count > 0,
3180 "canvas should paint body's gradient layer"
3181 );
3182 }
3183}