1use ui_layout::{BoxModel, EdgeOption, LayoutChild, LayoutNode, Position, Rect};
5
6use crate::engine::layouter::text_layouter::TextFlowLayouter;
7use crate::engine::layouter::types::{
8 Background, BackgroundDimension, BackgroundOffset, BackgroundPositionAxis, BackgroundRepeat,
9 BackgroundSize, BorderRadius, ClipPath, Color, ContainerStyle, CornerRadius, InfoNode,
10 NodeKind, TextDecoration, TextFlowStyle, TextStyle, Visibility,
11};
12use crate::engine::renderer_model::draw_command::{Brush, DrawCommand, FillRule, Paint};
13use crate::engine::renderer_model::geom::AffineTransform;
14use crate::engine::renderer_model::path::{
15 Path, append_quarter_ellipse, clamp_radii, ellipse_path, offset_path, polygon_path, rect_path,
16 rounded_rect_path,
17};
18use crate::engine::ui::ContentSize;
19
20#[derive(Default, Clone, Copy)]
22struct BoxPushState {
23 border: bool,
24 clip_path: bool,
25 overflow_clip: bool,
26 content: bool,
27 scroll: bool,
28}
29
30fn push_transform(cmd_buf: &mut Vec<DrawCommand>, dx: f32, dy: f32) -> bool {
35 if dx != 0.0 || dy != 0.0 {
36 cmd_buf.push(DrawCommand::PushTransform {
37 transform: AffineTransform::translate(dx, dy),
38 });
39 true
40 } else {
41 false
42 }
43}
44
45#[derive(Default, Clone, Copy)]
50struct StickyViewport {
51 top_left: (f32, f32),
57 size: (f32, f32),
59}
60
61fn is_scrollport(kind: &NodeKind) -> bool {
65 match kind {
66 NodeKind::Container {
67 scroll_x, scroll_y, ..
68 }
69 | NodeKind::Custom {
70 scroll_x, scroll_y, ..
71 } => *scroll_x || *scroll_y,
72 _ => false,
73 }
74}
75
76fn sticky_offset(
85 edges: &EdgeOption,
86 box_rect: &Rect,
87 viewport: StickyViewport,
88 containing: (f32, f32),
89) -> (f32, f32) {
90 let x = box_rect.x;
91 let y = box_rect.y;
92 let w = box_rect.width;
93 let h = box_rect.height;
94
95 let mut dy: f32 = 0.0;
96 let port_top = viewport.top_left.1;
97 let port_bottom = viewport.top_left.1 + viewport.size.1;
98 if let Some(top) = edges.top {
99 dy = dy.max((port_top + top) - y);
100 }
101 if let Some(bottom) = edges.bottom {
102 let has_room = match edges.top {
106 Some(top) => (port_bottom - bottom) - (port_top + top) >= h,
107 None => true,
108 };
109 if has_room {
110 dy = dy.min((port_bottom - bottom) - (y + h));
111 }
112 }
113 let dy_lo = (-y).min(0.0);
117 let dy_hi = (containing.1 - y - h).max(0.0);
118 dy = dy.max(dy_lo).min(dy_hi);
119
120 let mut dx: f32 = 0.0;
121 let port_left = viewport.top_left.0;
122 let port_right = viewport.top_left.0 + viewport.size.0;
123 if let Some(left) = edges.left {
124 dx = dx.max((port_left + left) - x);
125 }
126 if let Some(right) = edges.right {
127 let has_room = match edges.left {
128 Some(left) => (port_right - right) - (port_left + left) >= w,
129 None => true,
130 };
131 if has_room {
132 dx = dx.min((port_right - right) - (x + w));
133 }
134 }
135 let dx_lo = (-x).min(0.0);
136 let dx_hi = (containing.0 - x - w).max(0.0);
137 dx = dx.max(dx_lo).min(dx_hi);
138
139 (dx, dy)
140}
141
142fn resolve_outer_radii(radius: &BorderRadius, box_w: f32, box_h: f32) -> [(f32, f32); 4] {
147 let resolve = |c: &CornerRadius| -> (f32, f32) {
148 (
149 c.x.resolve_with(Some(box_w), 0.0, 0.0)
150 .unwrap_or(0.0)
151 .max(0.0),
152 c.y.resolve_with(Some(box_h), 0.0, 0.0)
153 .unwrap_or(0.0)
154 .max(0.0),
155 )
156 };
157 [
158 resolve(&radius.top_left),
159 resolve(&radius.top_right),
160 resolve(&radius.bottom_right),
161 resolve(&radius.bottom_left),
162 ]
163}
164
165fn inner_radii(outer: [(f32, f32); 4], bl: f32, bt: f32, br: f32, bb: f32) -> [(f32, f32); 4] {
168 [
169 (outer[0].0 - bl, outer[0].1 - bt),
170 (outer[1].0 - br, outer[1].1 - bt),
171 (outer[2].0 - br, outer[2].1 - bb),
172 (outer[3].0 - bl, outer[3].1 - bb),
173 ]
174 .map(|(x, y)| (x.max(0.0), y.max(0.0)))
175}
176
177fn top_border_strip(
183 w: f32,
184 bl: f32,
185 bt: f32,
186 br: f32,
187 outer: [(f32, f32); 4],
188 inner: [(f32, f32); 4],
189) -> Path {
190 let (rtl_x, rtl_y) = outer[0];
191 let (rtr_x, rtr_y) = outer[1];
192 let (itl_x, itl_y) = inner[0];
193 let (itr_x, itr_y) = inner[1];
194 let mut path = Path::new();
195 path.move_to(0.0, rtl_y);
196 append_quarter_ellipse(
197 &mut path,
198 rtl_x,
199 rtl_y,
200 rtl_x,
201 rtl_y,
202 (0.0, rtl_y),
203 (rtl_x, 0.0),
204 );
205 path.line_to(w - rtr_x, 0.0);
206 append_quarter_ellipse(
207 &mut path,
208 w - rtr_x,
209 rtr_y,
210 rtr_x,
211 rtr_y,
212 (w - rtr_x, 0.0),
213 (w, rtr_y),
214 );
215 path.line_to(w - br, rtr_y);
218 append_quarter_ellipse(
222 &mut path,
223 w - rtr_x - br,
224 rtr_y - bt,
225 itr_x,
226 itr_y,
227 (w - br, rtr_y),
228 (w - rtr_x - br, bt),
229 );
230 path.line_to(itl_x, bt);
231 append_quarter_ellipse(
232 &mut path,
233 rtl_x - bl,
234 rtl_y - bt,
235 itl_x,
236 itl_y,
237 (itl_x, bt),
238 (bl, rtl_y),
239 );
240 path.close();
241 path
242}
243
244fn bottom_border_strip(
247 w: f32,
248 h: f32,
249 bl: f32,
250 bb: f32,
251 br: f32,
252 outer: [(f32, f32); 4],
253 inner: [(f32, f32); 4],
254) -> Path {
255 let (rbl_x, rbl_y) = outer[3];
256 let (rbr_x, rbr_y) = outer[2];
257 let (ibl_x, ibl_y) = inner[3];
258 let (ibr_x, ibr_y) = inner[2];
259 let mut path = Path::new();
260 path.move_to(w, h - rbr_y);
261 append_quarter_ellipse(
262 &mut path,
263 w - rbr_x,
264 h - rbr_y,
265 rbr_x,
266 rbr_y,
267 (w, h - rbr_y),
268 (w - rbr_x, h),
269 );
270 path.line_to(rbl_x, h);
271 append_quarter_ellipse(
272 &mut path,
273 rbl_x,
274 h - rbl_y,
275 rbl_x,
276 rbl_y,
277 (rbl_x, h),
278 (0.0, h - rbl_y),
279 );
280 path.line_to(bl, h - rbl_y);
281 append_quarter_ellipse(
282 &mut path,
283 rbl_x,
284 h - rbl_y,
285 ibl_x,
286 ibl_y,
287 (bl, h - rbl_y),
288 (rbl_x, h - bb),
289 );
290 path.line_to(w - rbr_x, h - bb);
291 append_quarter_ellipse(
292 &mut path,
293 w - rbr_x,
294 h - rbr_y,
295 ibr_x,
296 ibr_y,
297 (w - rbr_x, h - bb),
298 (w - br, h - rbr_y),
299 );
300 path.close();
301 path
302}
303
304fn draw_border(
309 cmd_buf: &mut Vec<DrawCommand>,
310 border_box: &ui_layout::Rect,
311 padding_box: &ui_layout::Rect,
312 style: &ContainerStyle,
313 ox: f32,
314 oy: f32,
315) {
316 let bw_top = (padding_box.y - border_box.y).max(0.0);
317 let bw_bottom =
318 (border_box.y + border_box.height - (padding_box.y + padding_box.height)).max(0.0);
319 let bw_left = (padding_box.x - border_box.x).max(0.0);
320 let bw_right = (border_box.x + border_box.width - (padding_box.x + padding_box.width)).max(0.0);
321
322 let w = border_box.width;
323 let h = border_box.height;
324 let mut outer = resolve_outer_radii(&style.border_radius, w, h);
325 outer = clamp_radii(outer, w, h);
326 let mut inner = inner_radii(outer, bw_left, bw_top, bw_right, bw_bottom);
327 inner = clamp_radii(
328 inner,
329 padding_box.width.max(0.0),
330 padding_box.height.max(0.0),
331 );
332
333 let bc = &style.border_color;
334 let push_fill = |cmd_buf: &mut Vec<DrawCommand>, path: Path, color: Color| {
335 cmd_buf.push(DrawCommand::Fill {
336 path,
337 rule: FillRule::NonZero,
338 paint: Paint {
339 brush: Brush::Solid(color),
340 opacity: 1.0,
341 },
342 });
343 };
344
345 let has_radius = outer.iter().any(|(rx, ry)| *rx > 0.0 || *ry > 0.0);
346 if !has_radius {
347 if bw_top > 0.0 {
348 push_fill(cmd_buf, rect_path(ox, oy, w, bw_top), bc.top);
349 }
350 if bw_bottom > 0.0 {
351 push_fill(
352 cmd_buf,
353 rect_path(ox, oy + h - bw_bottom, w, bw_bottom),
354 bc.bottom,
355 );
356 }
357 if bw_left > 0.0 {
358 push_fill(
359 cmd_buf,
360 rect_path(ox, oy + bw_top, bw_left, h - bw_top - bw_bottom),
361 bc.left,
362 );
363 }
364 if bw_right > 0.0 {
365 push_fill(
366 cmd_buf,
367 rect_path(
368 ox + w - bw_right,
369 oy + bw_top,
370 bw_right,
371 h - bw_top - bw_bottom,
372 ),
373 bc.right,
374 );
375 }
376 return;
377 }
378
379 if bw_top > 0.0 {
380 push_fill(
381 cmd_buf,
382 offset_path(
383 &top_border_strip(w, bw_left, bw_top, bw_right, outer, inner),
384 ox,
385 oy,
386 ),
387 bc.top,
388 );
389 }
390 if bw_bottom > 0.0 {
391 push_fill(
392 cmd_buf,
393 offset_path(
394 &bottom_border_strip(w, h, bw_left, bw_bottom, bw_right, outer, inner),
395 ox,
396 oy,
397 ),
398 bc.bottom,
399 );
400 }
401 if bw_left > 0.0 {
402 push_fill(
403 cmd_buf,
404 rect_path(ox, oy + outer[0].1, bw_left, h - outer[0].1 - outer[3].1),
405 bc.left,
406 );
407 }
408 if bw_right > 0.0 {
409 push_fill(
410 cmd_buf,
411 rect_path(
412 ox + w - bw_right,
413 oy + outer[1].1,
414 bw_right,
415 h - outer[1].1 - outer[2].1,
416 ),
417 bc.right,
418 );
419 }
420}
421
422fn draw_background(
427 cmd_buf: &mut Vec<DrawCommand>,
428 border_box: &ui_layout::Rect,
429 padding_box: &ui_layout::Rect,
430 style: &ContainerStyle,
431 ox: f32,
432 oy: f32,
433) {
434 let x = padding_box.x - border_box.x + ox;
435 let y = padding_box.y - border_box.y + oy;
436 let bw_top = (padding_box.y - border_box.y).max(0.0);
437 let bw_bottom =
438 (border_box.y + border_box.height - (padding_box.y + padding_box.height)).max(0.0);
439 let bw_left = (padding_box.x - border_box.x).max(0.0);
440 let bw_right = (border_box.x + border_box.width - (padding_box.x + padding_box.width)).max(0.0);
441
442 let mut outer = resolve_outer_radii(&style.border_radius, border_box.width, border_box.height);
443 outer = clamp_radii(outer, border_box.width, border_box.height);
444 let mut inner = inner_radii(outer, bw_left, bw_top, bw_right, bw_bottom);
445 inner = clamp_radii(
446 inner,
447 padding_box.width.max(0.0),
448 padding_box.height.max(0.0),
449 );
450 let path = rounded_rect_path(
452 x,
453 y,
454 padding_box.width,
455 padding_box.height,
456 inner[0],
457 inner[1],
458 inner[2],
459 inner[3],
460 );
461 match &style.background {
462 Background::Color(c) if c.3 > 0 => {
463 cmd_buf.push(DrawCommand::Fill {
464 path,
465 rule: FillRule::NonZero,
466 paint: Paint {
467 brush: Brush::Solid(*c),
468 opacity: 1.0,
469 },
470 });
471 }
472 Background::Gradient(g) => {
473 cmd_buf.push(DrawCommand::Fill {
474 path,
475 rule: FillRule::NonZero,
476 paint: Paint {
477 brush: Brush::Gradient(g.clone()),
478 opacity: 1.0,
479 },
480 });
481 }
482 Background::Image { image, color, .. } => {
483 if color.3 > 0 {
484 cmd_buf.push(DrawCommand::Fill {
485 path: path.clone(),
486 rule: FillRule::NonZero,
487 paint: Paint {
488 brush: Brush::Solid(*color),
489 opacity: 1.0,
490 },
491 });
492 }
493 if let Some(image) = image {
494 let (image_width, image_height) = resolve_background_image_size(
495 image.width() as f32,
496 image.height() as f32,
497 padding_box.width,
498 padding_box.height,
499 style.background_size,
500 );
501 if image_width > 0.0 && image_height > 0.0 {
502 let image_x = resolve_background_axis(
503 padding_box.width,
504 image_width,
505 style.background_position.x,
506 );
507 let image_y = resolve_background_axis(
508 padding_box.height,
509 image_height,
510 style.background_position.y,
511 );
512 let repeat_x = matches!(
513 style.background_repeat,
514 BackgroundRepeat::Repeat | BackgroundRepeat::RepeatX
515 );
516 let repeat_y = matches!(
517 style.background_repeat,
518 BackgroundRepeat::Repeat | BackgroundRepeat::RepeatY
519 );
520 let xs = background_tile_positions(
521 image_x,
522 image_width,
523 padding_box.width,
524 repeat_x,
525 );
526 let ys = background_tile_positions(
527 image_y,
528 image_height,
529 padding_box.height,
530 repeat_y,
531 );
532 cmd_buf.push(DrawCommand::PushClip {
533 path,
534 rule: FillRule::NonZero,
535 });
536 for tile_y in ys {
537 for tile_x in &xs {
538 cmd_buf.push(DrawCommand::Fill {
539 path: rect_path(x + *tile_x, y + tile_y, image_width, image_height),
540 rule: FillRule::NonZero,
541 paint: Paint {
542 brush: Brush::Image(image.clone()),
543 opacity: 1.0,
544 },
545 });
546 }
547 }
548 cmd_buf.push(DrawCommand::PopClip);
549 }
550 }
551 }
552 _ => {}
553 }
554}
555
556fn resolve_background_dimension(dimension: BackgroundDimension, area: f32) -> Option<f32> {
557 match dimension {
558 BackgroundDimension::Auto => None,
559 BackgroundDimension::Length(value) => Some(value.max(0.0)),
560 BackgroundDimension::Percent(value) => Some((area * value).max(0.0)),
561 }
562}
563
564fn resolve_background_image_size(
565 intrinsic_width: f32,
566 intrinsic_height: f32,
567 area_width: f32,
568 area_height: f32,
569 size: BackgroundSize,
570) -> (f32, f32) {
571 if intrinsic_width <= 0.0 || intrinsic_height <= 0.0 {
572 return (0.0, 0.0);
573 }
574 let ratio = intrinsic_width / intrinsic_height;
575 match size {
576 BackgroundSize::Auto => (intrinsic_width, intrinsic_height),
577 BackgroundSize::Contain | BackgroundSize::Cover => {
578 let width_scale = area_width / intrinsic_width;
579 let height_scale = area_height / intrinsic_height;
580 let scale = if matches!(size, BackgroundSize::Contain) {
581 width_scale.min(height_scale)
582 } else {
583 width_scale.max(height_scale)
584 };
585 (intrinsic_width * scale, intrinsic_height * scale)
586 }
587 BackgroundSize::Explicit { width, height } => {
588 let width = resolve_background_dimension(width, area_width);
589 let height = resolve_background_dimension(height, area_height);
590 match (width, height) {
591 (Some(width), Some(height)) => (width, height),
592 (Some(width), None) => (width, width / ratio),
593 (None, Some(height)) => (height * ratio, height),
594 (None, None) => (intrinsic_width, intrinsic_height),
595 }
596 }
597 }
598}
599
600fn resolve_background_axis(area: f32, image: f32, position: BackgroundPositionAxis) -> f32 {
601 let available = area - image;
602 let length_offset = |offset| match offset {
603 BackgroundOffset::Zero => 0.0,
604 BackgroundOffset::Length(value) => value,
605 BackgroundOffset::Percent(value) => area * value,
606 };
607 match position {
608 BackgroundPositionAxis::Start(BackgroundOffset::Percent(value)) => available * value,
609 BackgroundPositionAxis::Start(offset) => length_offset(offset),
610 BackgroundPositionAxis::Center(offset) => available * 0.5 + length_offset(offset),
611 BackgroundPositionAxis::End(offset) => available - length_offset(offset),
612 }
613}
614
615fn background_tile_positions(base: f32, tile: f32, area: f32, repeat: bool) -> Vec<f32> {
616 if !repeat || tile <= 0.0 {
617 return vec![base];
618 }
619 let mut start = base;
620 while start > 0.0 {
621 start -= tile;
622 }
623 while start + tile <= 0.0 {
624 start += tile;
625 }
626 let mut positions = Vec::new();
627 let mut position = start;
628 while position < area && positions.len() < 512 {
629 positions.push(position);
630 position += tile;
631 }
632 positions
633}
634
635fn clip_path_to_path(clip: &ClipPath, w: f32, h: f32) -> Path {
638 match clip {
639 ClipPath::None => rect_path(0.0, 0.0, w, h),
640 ClipPath::Circle {
641 radius,
642 center_x,
643 center_y,
644 } => {
645 let cx = center_x * w;
646 let cy = center_y * h;
647 let farthest = w.max(h);
649 let r = (radius * farthest).min(farthest);
650 ellipse_path(cx, cy, r, r)
651 }
652 ClipPath::Ellipse {
653 rx,
654 ry,
655 center_x,
656 center_y,
657 } => {
658 let cx = center_x * w;
659 let cy = center_y * h;
660 ellipse_path(cx, cy, rx * w, ry * h)
661 }
662 ClipPath::Inset {
663 top,
664 right,
665 bottom,
666 left,
667 } => {
668 let x = left * w;
669 let y = top * h;
670 let iw = (w - left * w - right * w).max(0.0);
671 let ih = (h - top * h - bottom * h).max(0.0);
672 rect_path(x, y, iw, ih)
673 }
674 ClipPath::Polygon { points } => {
675 let verts: Vec<(f32, f32)> = points.iter().map(|(px, py)| (px * w, py * h)).collect();
676 polygon_path(&verts)
677 }
678 }
679}
680
681#[allow(clippy::too_many_arguments)]
683fn push_box_model(
684 cmd_buf: &mut Vec<DrawCommand>,
685 box_model: &ui_layout::BoxModel,
686 style: &crate::engine::layouter::types::ContainerStyle,
687 scroll_offset_x: f32,
688 scroll_offset_y: f32,
689 is_inline: bool,
690 clips_overflow: bool,
691 draw_bg: bool,
692) -> BoxPushState {
693 let border_box = box_model.border_box;
694 let padding_box = box_model.padding_box;
695 let content_box = box_model.content_box;
696
697 let dx = content_box.x - border_box.x;
698 let dy = content_box.y - border_box.y;
699
700 let border = !is_inline && push_transform(cmd_buf, border_box.x, border_box.y);
705
706 let (ox, oy) = if is_inline {
710 (border_box.x, border_box.y)
711 } else {
712 (0.0, 0.0)
713 };
714
715 let clip_path_pushed = !is_inline
718 && !matches!(style.clip_path, ClipPath::None)
719 && border_box.width > 0.0
720 && border_box.height > 0.0;
721 if clip_path_pushed {
722 let path = clip_path_to_path(&style.clip_path, border_box.width, border_box.height);
723 cmd_buf.push(DrawCommand::PushClip {
724 path,
725 rule: FillRule::NonZero,
726 });
727 }
728
729 draw_border(cmd_buf, &border_box, &padding_box, style, ox, oy);
730
731 if draw_bg {
732 draw_background(cmd_buf, &border_box, &padding_box, style, ox, oy);
733 }
734
735 let overflow_clip =
736 !is_inline && clips_overflow && padding_box.width > 0.0 && padding_box.height > 0.0;
737 if overflow_clip {
738 cmd_buf.push(DrawCommand::PushClip {
739 path: rect_path(
740 padding_box.x - border_box.x,
741 padding_box.y - border_box.y,
742 padding_box.width,
743 padding_box.height,
744 ),
745 rule: FillRule::NonZero,
746 });
747 }
748
749 let content = !is_inline && push_transform(cmd_buf, dx, dy);
750 let scroll = !is_inline && push_transform(cmd_buf, -scroll_offset_x, -scroll_offset_y);
751
752 BoxPushState {
753 border,
754 clip_path: clip_path_pushed,
755 overflow_clip,
756 content,
757 scroll,
758 }
759}
760
761fn pop_box_model(cmd_buf: &mut Vec<DrawCommand>, state: BoxPushState) {
763 if state.scroll {
764 cmd_buf.push(DrawCommand::PopTransform);
765 }
766 if state.content {
767 cmd_buf.push(DrawCommand::PopTransform);
768 }
769 if state.overflow_clip {
770 cmd_buf.push(DrawCommand::PopClip);
771 }
772 if state.clip_path {
773 cmd_buf.push(DrawCommand::PopClip);
774 }
775 if state.border {
776 cmd_buf.push(DrawCommand::PopTransform);
777 }
778}
779
780fn draw_text(
782 cmd_buf: &mut Vec<DrawCommand>,
783 style: &TextStyle,
784 flow_style: TextFlowStyle,
785 text_id: usize,
786) {
787 if let Some(result) = TextFlowLayouter::get_result(text_id) {
788 for (i, line_text) in result.line_texts.iter().enumerate() {
789 let span = &result.spans[i];
790 let x = span.line_pos.0;
791 let y = span.line_pos.1;
792
793 cmd_buf.push(DrawCommand::DrawText {
794 x,
795 y,
796 text: line_text.as_str().into(),
797 style: style.clone(),
798 flow_style,
799 });
800
801 let font_size = flow_style.font_size;
802 let line_thickness = (font_size * 0.08).max(1.0);
803 let line_y_adj = if line_text.is_empty() {
804 y
805 } else {
806 y + font_size
807 };
808 let (line_y, draw) = match style.text_decoration {
809 TextDecoration::None => (0.0, false),
810 TextDecoration::Underline => (line_y_adj, true),
811 TextDecoration::LineThrough => (y + font_size * 0.5, true),
812 TextDecoration::Overline => (y, true),
813 };
814
815 if draw {
816 cmd_buf.push(DrawCommand::Fill {
817 path: rect_path(
818 x,
819 line_y,
820 span.x_range.end - span.x_range.start,
821 line_thickness,
822 ),
823 rule: FillRule::NonZero,
824 paint: Paint {
825 brush: Brush::Solid(style.text_decoration_color.unwrap_or(style.color)),
826 opacity: 1.0,
827 },
828 });
829 }
830 }
831 }
832}
833
834pub fn generate_draw_commands(
843 cmd_buf: &mut Vec<DrawCommand>,
844 layout: &LayoutNode,
845 info: &InfoNode,
846 viewport: (f32, f32),
847) {
848 let (scroll_x, scroll_y) = info.kind.scroll_offsets();
849 let root_viewport = StickyViewport {
850 top_left: (-scroll_x, scroll_y),
851 size: viewport,
852 };
853 let containing = layout
854 .layout_box
855 .iter()
856 .next()
857 .map_or((0.0, 0.0), |b| (b.content_box.width, b.content_box.height));
858 let origin = layout
859 .layout_box
860 .iter()
861 .next()
862 .map_or((0.0, 0.0), |b| (b.content_box.x, b.content_box.y));
863 let mut popups: Vec<(Vec<DrawCommand>, (f32, f32))> = Vec::new();
864 generate_draw_commands_inner(
865 cmd_buf,
866 layout,
867 info,
868 (0.0, 0.0),
869 root_viewport,
870 containing,
871 origin,
872 &mut popups,
873 true,
874 );
875 for (commands, (tx, ty)) in popups {
878 if tx != 0.0 || ty != 0.0 {
879 cmd_buf.push(DrawCommand::PushTransform {
880 transform: AffineTransform::translate(tx, ty),
881 });
882 }
883 cmd_buf.extend(commands);
884 if tx != 0.0 || ty != 0.0 {
885 cmd_buf.push(DrawCommand::PopTransform);
886 }
887 }
888}
889
890fn child_origin(child: &LayoutNode, parent_origin: (f32, f32)) -> (f32, f32) {
895 child.layout_box.iter().next().map_or(parent_origin, |b| {
896 (
897 parent_origin.0 + b.content_box.x,
898 parent_origin.1 + b.content_box.y,
899 )
900 })
901}
902
903#[allow(clippy::too_many_arguments)]
923fn generate_draw_commands_inner(
924 cmd_buf: &mut Vec<DrawCommand>,
925 layout: &LayoutNode,
926 info: &InfoNode,
927 accumulated_scroll: (f32, f32),
928 viewport: StickyViewport,
929 containing: (f32, f32),
930 origin: (f32, f32),
931 popups: &mut Vec<(Vec<DrawCommand>, (f32, f32))>,
932 is_root: bool,
933) {
934 match &info.kind {
938 NodeKind::Container { style, .. } | NodeKind::Custom { style, .. } => {
939 if matches!(style.visibility, Visibility::Hidden | Visibility::Collapse) {
940 return;
941 }
942 }
943 _ => {}
944 }
945
946 let mut box_states: Vec<BoxPushState> = Vec::new();
947
948 let is_fixed = layout.style.position.kind == Position::Fixed;
949 let is_sticky = layout.style.position.kind == Position::Sticky;
950 let is_inline = matches!(layout.layout_box, ui_layout::LayoutBox::InlineBox(_));
951
952 let cancel_scroll = is_fixed && (accumulated_scroll.0 != 0.0 || accumulated_scroll.1 != 0.0);
954 if cancel_scroll {
955 cmd_buf.push(DrawCommand::PushTransform {
956 transform: AffineTransform::translate(-accumulated_scroll.0, accumulated_scroll.1),
957 });
958 }
959
960 let sticky_pushed = if is_sticky && !is_fixed {
964 match layout.layout_box.iter().next() {
965 Some(bm) => {
966 let (dx, dy) = bm
967 .sticky_edges
968 .map(|edges| sticky_offset(&edges, &bm.border_box, viewport, containing))
969 .unwrap_or((0.0, 0.0));
970 push_transform(cmd_buf, dx, dy)
971 }
972 None => false,
973 }
974 } else {
975 false
976 };
977
978 match &info.kind {
979 NodeKind::Text { .. } | NodeKind::LineBreak => unreachable!(),
980
981 NodeKind::Container {
982 scroll_x,
983 scroll_y,
984 scroll_offset_x,
985 scroll_offset_y,
986 style,
987 ..
988 } => {
989 for box_model in &layout.layout_box {
990 box_states.push(push_box_model(
991 cmd_buf,
992 &box_model,
993 style,
994 *scroll_offset_x,
995 *scroll_offset_y,
996 is_inline,
997 *scroll_x || *scroll_y,
998 true,
999 ));
1000 }
1001 }
1002
1003 NodeKind::Custom {
1004 scroll_x,
1005 scroll_y,
1006 scroll_offset_x,
1007 scroll_offset_y,
1008 style,
1009 layout_style,
1010 node,
1011 text_style,
1012 text_flow_style,
1013 ..
1014 } => {
1015 for box_model in &layout.layout_box {
1016 box_states.push(push_box_model(
1017 cmd_buf,
1018 &box_model,
1019 style,
1020 *scroll_offset_x,
1021 *scroll_offset_y,
1022 false,
1023 *scroll_x || *scroll_y,
1024 false,
1025 ));
1026 }
1027
1028 let size = layout.layout_box.iter().next().map_or_else(
1029 || node.intrinsic_size(),
1030 |box_model| ContentSize {
1031 width: box_model.content_box.width,
1032 height: box_model.content_box.height,
1033 },
1034 );
1035 node.draw_sized(cmd_buf, text_style, text_flow_style, layout_style, size);
1036 if let Some(popup) = node.popup(text_style, text_flow_style) {
1039 let own_scroll = info.kind.scroll_offsets();
1040 let inherited_scroll = if is_fixed {
1043 (0.0, 0.0)
1044 } else {
1045 accumulated_scroll
1046 };
1047 let (mut tx, mut ty) = (
1048 origin.0 - inherited_scroll.0 - own_scroll.0,
1049 origin.1 - inherited_scroll.1 - own_scroll.1,
1050 );
1051 if is_sticky
1054 && !is_fixed
1055 && let Some(bm) = layout.layout_box.iter().next()
1056 && let Some(edges) = bm.sticky_edges
1057 {
1058 let (dx, dy) = sticky_offset(&edges, &bm.border_box, viewport, containing);
1059 tx += dx;
1060 ty += dy;
1061 }
1062 popups.push((popup.commands, (tx, ty)));
1063 }
1064 }
1065 }
1066
1067 let own_scroll = info.kind.scroll_offsets();
1069 let child_scroll = if is_fixed {
1072 own_scroll
1073 } else {
1074 (
1075 accumulated_scroll.0 + own_scroll.0,
1076 accumulated_scroll.1 + own_scroll.1,
1077 )
1078 };
1079
1080 let child_viewport = if is_root || is_scrollport(&info.kind) {
1086 let bm = layout.layout_box.iter().next();
1087 let (scroll_x, scroll_y) = own_scroll;
1088 StickyViewport {
1089 top_left: bm.as_ref().map_or((0.0, 0.0), |b| {
1090 (
1091 b.padding_box.x - b.content_box.x - scroll_x,
1092 b.padding_box.y - b.content_box.y + scroll_y,
1093 )
1094 }),
1095 size: if is_root {
1096 viewport.size
1097 } else {
1098 bm.map_or(viewport.size, |b| {
1099 (b.padding_box.width, b.padding_box.height)
1100 })
1101 },
1102 }
1103 } else {
1104 let bm = layout.layout_box.iter().next();
1105 StickyViewport {
1106 top_left: bm.map_or(viewport.top_left, |b| {
1107 (
1108 viewport.top_left.0 - b.content_box.x,
1109 viewport.top_left.1 - b.content_box.y,
1110 )
1111 }),
1112 size: viewport.size,
1113 }
1114 };
1115 let child_containing = layout
1116 .layout_box
1117 .iter()
1118 .next()
1119 .map_or(containing, |b| (b.content_box.width, b.content_box.height));
1120
1121 let mut layout_iter = layout.children.iter();
1122 let mut positive_stacking_children: Vec<(i32, usize, Vec<DrawCommand>)> = Vec::new();
1123
1124 for (child_order, child_info) in info.children.iter().enumerate() {
1125 match &child_info.kind {
1126 NodeKind::Text {
1127 text_id,
1128 style,
1129 flow_style,
1130 ..
1131 } => {
1132 draw_text(cmd_buf, style, *flow_style, *text_id);
1133 layout_iter.next();
1134 }
1135 NodeKind::LineBreak => {
1136 layout_iter.next();
1137 }
1138 NodeKind::Container { .. } => {
1139 if let Some(LayoutChild::Node(node)) = layout_iter.next() {
1140 let child_origin = child_origin(node, origin);
1141 let z_index = child_info.kind.z_index();
1142 if z_index > 0 {
1143 let mut child_commands = Vec::new();
1144 generate_draw_commands_inner(
1145 &mut child_commands,
1146 node,
1147 child_info,
1148 child_scroll,
1149 child_viewport,
1150 child_containing,
1151 child_origin,
1152 popups,
1153 false,
1154 );
1155 positive_stacking_children.push((z_index, child_order, child_commands));
1156 } else {
1157 generate_draw_commands_inner(
1158 cmd_buf,
1159 node,
1160 child_info,
1161 child_scroll,
1162 child_viewport,
1163 child_containing,
1164 child_origin,
1165 popups,
1166 false,
1167 );
1168 }
1169 }
1170 }
1171 NodeKind::Custom {
1172 node,
1173 text_style,
1174 text_flow_style,
1175 style,
1176 layout_style,
1177 ..
1178 } => {
1179 match layout_iter.next() {
1180 Some(LayoutChild::Node(node_layout)) => {
1182 let child_origin = child_origin(node_layout, origin);
1183 generate_draw_commands_inner(
1184 cmd_buf,
1185 node_layout,
1186 child_info,
1187 child_scroll,
1188 child_viewport,
1189 child_containing,
1190 child_origin,
1191 popups,
1192 false,
1193 );
1194 }
1195 Some(LayoutChild::Custom(custom_child)) => {
1198 if let Some(result) = custom_child.result() {
1199 let bm = &result.box_model;
1200 let rect = BoxModel {
1201 sticky_edges: bm.sticky_edges,
1202 border_box: bm.border_box,
1203 padding_box: bm.padding_box,
1204 content_box: bm.content_box,
1205 children_box: bm.children_box,
1206 };
1207 let state = push_box_model(
1208 cmd_buf, &rect, style, 0.0, 0.0, false, false, false,
1209 );
1210 node.draw_sized(
1211 cmd_buf,
1212 text_style,
1213 text_flow_style,
1214 layout_style,
1215 ContentSize {
1216 width: rect.content_box.width,
1217 height: rect.content_box.height,
1218 },
1219 );
1220 pop_box_model(cmd_buf, state);
1221 }
1222 }
1223 _ => {}
1224 }
1225 }
1226 }
1227 }
1228
1229 positive_stacking_children.sort_by_key(|(z_index, child_order, _)| (*z_index, *child_order));
1230 for (_, _, commands) in positive_stacking_children {
1231 cmd_buf.extend(commands);
1232 }
1233
1234 if matches!(
1235 info.kind,
1236 NodeKind::Container { .. } | NodeKind::Custom { .. }
1237 ) {
1238 for state in box_states.iter().rev() {
1239 pop_box_model(cmd_buf, *state);
1240 }
1241 }
1242
1243 if sticky_pushed {
1244 cmd_buf.push(DrawCommand::PopTransform);
1245 }
1246
1247 if cancel_scroll {
1248 cmd_buf.push(DrawCommand::PopTransform);
1249 }
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use crate::engine::layouter::types::ContainerRole;
1256 use crate::engine::renderer_model::geom::AffineTransform;
1257 use crate::engine::ui::custom_node::{CustomNode, Popup};
1258 use std::sync::Arc;
1259 use ui_layout::Style;
1260
1261 fn count_balanced(commands: &[DrawCommand]) -> bool {
1262 let mut transform_depth = 0usize;
1263 let mut clip_depth = 0usize;
1264 for cmd in commands {
1265 match cmd {
1266 DrawCommand::PushTransform { .. } => transform_depth += 1,
1267 DrawCommand::PopTransform => transform_depth -= 1,
1268 DrawCommand::PushClip { .. } => clip_depth += 1,
1269 DrawCommand::PopClip => clip_depth -= 1,
1270 _ => {}
1271 }
1272 }
1273 transform_depth == 0 && clip_depth == 0
1274 }
1275
1276 #[test]
1277 fn test_push_pop_transform_balanced() {
1278 let mut buf = Vec::new();
1279 assert!(push_transform(&mut buf, 5.0, 5.0));
1280 assert!(matches!(buf.pop(), Some(DrawCommand::PushTransform { .. })));
1281 assert!(!push_transform(&mut buf, 0.0, 0.0));
1282 }
1283
1284 #[test]
1285 fn test_radii_resolution_and_clamp() {
1286 let outer = resolve_outer_radii(&BorderRadius::default(), 100.0, 50.0);
1287 assert_eq!(outer, [(0.0, 0.0); 4]);
1288 }
1289
1290 #[test]
1291 fn scratch_background_image_geometry_is_resolved() {
1292 let (width, height) = resolve_background_image_size(
1293 1200.0,
1294 600.0,
1295 800.0,
1296 400.0,
1297 BackgroundSize::Explicit {
1298 width: BackgroundDimension::Length(624.0),
1299 height: BackgroundDimension::Length(325.0),
1300 },
1301 );
1302 assert_eq!((width, height), (624.0, 325.0));
1303 assert_eq!(
1304 resolve_background_axis(
1305 800.0,
1306 width,
1307 BackgroundPositionAxis::End(BackgroundOffset::Zero),
1308 ),
1309 176.0
1310 );
1311 assert_eq!(
1312 resolve_background_axis(
1313 400.0,
1314 height,
1315 BackgroundPositionAxis::Center(BackgroundOffset::Zero),
1316 ),
1317 37.5
1318 );
1319 }
1320
1321 #[test]
1322 fn background_contain_and_cover_preserve_aspect_ratio() {
1323 assert_eq!(
1324 resolve_background_image_size(200.0, 100.0, 300.0, 300.0, BackgroundSize::Contain,),
1325 (300.0, 150.0)
1326 );
1327 assert_eq!(
1328 resolve_background_image_size(200.0, 100.0, 300.0, 300.0, BackgroundSize::Cover),
1329 (600.0, 300.0)
1330 );
1331 }
1332
1333 fn ui_rect(x: f32, y: f32, w: f32, h: f32) -> ui_layout::Rect {
1334 ui_layout::Rect {
1335 x,
1336 y,
1337 width: w,
1338 height: h,
1339 }
1340 }
1341
1342 #[test]
1343 fn test_single_box_model_is_balanced() {
1344 let box_model = ui_layout::BoxModel {
1345 sticky_edges: None,
1346 border_box: ui_rect(10.0, 20.0, 120.0, 60.0),
1347 padding_box: ui_rect(12.0, 22.0, 116.0, 56.0),
1348 content_box: ui_rect(12.0, 22.0, 116.0, 56.0),
1349 children_box: ui_rect(12.0, 22.0, 116.0, 56.0),
1350 };
1351 let style = ContainerStyle::default();
1352 let mut buf = Vec::new();
1353 let state = push_box_model(&mut buf, &box_model, &style, 0.0, 0.0, false, true, true);
1354 assert!(buf.len() >= 2);
1357 assert!(!count_balanced(&buf));
1359 pop_box_model(&mut buf, state);
1360 assert!(count_balanced(&buf));
1361 }
1362
1363 #[test]
1364 fn visible_overflow_does_not_clip_block_contents() {
1365 let box_model = ui_layout::BoxModel {
1366 sticky_edges: None,
1367 border_box: ui_rect(0.0, 0.0, 100.0, 50.0),
1368 padding_box: ui_rect(0.0, 0.0, 100.0, 50.0),
1369 content_box: ui_rect(0.0, 0.0, 100.0, 50.0),
1370 children_box: ui_rect(0.0, 0.0, 120.0, 60.0),
1371 };
1372 let mut commands = Vec::new();
1373 let state = push_box_model(
1374 &mut commands,
1375 &box_model,
1376 &ContainerStyle::default(),
1377 0.0,
1378 0.0,
1379 false,
1380 false,
1381 true,
1382 );
1383 assert!(
1384 !commands
1385 .iter()
1386 .any(|command| matches!(command, DrawCommand::PushClip { .. }))
1387 );
1388 pop_box_model(&mut commands, state);
1389 assert!(count_balanced(&commands));
1390 }
1391
1392 #[test]
1393 fn test_nested_box_models_balanced() {
1394 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1395 sticky_edges: None,
1396 border_box: ui_rect(x, y, w, h),
1397 padding_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
1398 content_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
1399 children_box: ui_rect(x + 2.0, y + 2.0, w - 4.0, h - 4.0),
1400 };
1401 let style = ContainerStyle::default();
1402 let mut buf = Vec::new();
1403 let outer = push_box_model(
1404 &mut buf,
1405 &mk_box(0.0, 0.0, 100.0, 100.0),
1406 &style,
1407 3.0,
1408 4.0,
1409 false,
1410 true,
1411 true,
1412 );
1413 let inner = push_box_model(
1414 &mut buf,
1415 &mk_box(10.0, 10.0, 50.0, 50.0),
1416 &style,
1417 0.0,
1418 0.0,
1419 false,
1420 true,
1421 true,
1422 );
1423 assert!(!buf.is_empty());
1425 pop_box_model(&mut buf, inner);
1426 pop_box_model(&mut buf, outer);
1427 assert!(count_balanced(&buf));
1428 }
1429
1430 #[test]
1431 fn test_affine_transform_reexport() {
1432 let t = AffineTransform::translate(1.0, 2.0);
1433 assert_eq!(t.apply(0.0, 0.0), (1.0, 2.0));
1434 }
1435
1436 fn mk_info_node(kind: NodeKind, children: Vec<InfoNode>) -> InfoNode {
1439 InfoNode {
1440 kind,
1441 children,
1442 dom_id: None,
1443 }
1444 }
1445
1446 fn scroll_translates(commands: &[DrawCommand]) -> Vec<(f32, f32)> {
1449 let mut tx: Vec<f32> = Vec::new();
1450 let mut ty: Vec<f32> = Vec::new();
1451 let mut out = Vec::new();
1452 for cmd in commands {
1453 match cmd {
1454 DrawCommand::PushTransform { transform } => {
1455 tx.push(transform.apply(0.0, 0.0).0);
1456 ty.push(transform.apply(0.0, 0.0).1);
1457 out.push((transform.apply(0.0, 0.0).0, transform.apply(0.0, 0.0).1));
1458 }
1459 DrawCommand::PopTransform => {
1460 if let (Some(x), Some(y)) = (tx.pop(), ty.pop()) {
1461 out.push((-x, -y));
1462 }
1463 }
1464 _ => {}
1465 }
1466 }
1467 out
1468 }
1469
1470 #[test]
1471 fn fixed_node_cancels_inherited_scroll_offset() {
1472 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
1473 x,
1474 y,
1475 width: w,
1476 height: h,
1477 };
1478 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1479 sticky_edges: None,
1480 border_box: ui_rect(x, y, w, h),
1481 padding_box: ui_rect(x, y, w, h),
1482 content_box: ui_rect(x, y, w, h),
1483 children_box: ui_rect(x, y, w, h),
1484 };
1485
1486 let scroller_style = Style::default();
1488 let mut scroller = LayoutNode::new(scroller_style);
1489 scroller.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 100.0, 100.0));
1490
1491 let mut fixed_style = Style::default();
1492 fixed_style.position.kind = Position::Fixed;
1493 let fixed = LayoutNode::new(fixed_style);
1494 scroller.children = vec![LayoutChild::Node(Box::new(fixed))];
1495
1496 let scroller_info = mk_info_node(
1497 NodeKind::Container {
1498 scroll_x: true,
1499 scroll_y: true,
1500 scroll_offset_x: 0.0,
1501 scroll_offset_y: 50.0,
1502 style: ContainerStyle::default(),
1503 role: ContainerRole::Normal,
1504 },
1505 vec![mk_info_node(
1506 NodeKind::Container {
1507 scroll_x: false,
1508 scroll_y: false,
1509 scroll_offset_x: 0.0,
1510 scroll_offset_y: 0.0,
1511 style: ContainerStyle::default(),
1512 role: ContainerRole::Normal,
1513 },
1514 Vec::new(),
1515 )],
1516 );
1517
1518 let mut commands = Vec::new();
1519 generate_draw_commands(&mut commands, &scroller, &scroller_info, (100.0, 100.0));
1520 assert!(count_balanced(&commands));
1521
1522 let translates = scroll_translates(&commands);
1525 assert!(
1526 translates.contains(&(0.0, 50.0)),
1527 "expected a scroll-cancelling transform, got {translates:?}"
1528 );
1529 }
1530
1531 #[test]
1532 fn positive_z_index_child_paints_after_later_normal_sibling() {
1533 let box_model = |x: f32| ui_layout::BoxModel {
1534 sticky_edges: None,
1535 border_box: ui_rect(x, 0.0, 50.0, 50.0),
1536 padding_box: ui_rect(x, 0.0, 50.0, 50.0),
1537 content_box: ui_rect(x, 0.0, 50.0, 50.0),
1538 children_box: ui_rect(x, 0.0, 50.0, 50.0),
1539 };
1540 let mut root = LayoutNode::new(Style::default());
1541 root.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
1542 let mut front = LayoutNode::new(Style::default());
1543 front.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
1544 let mut normal = LayoutNode::new(Style::default());
1545 normal.layout_box = ui_layout::LayoutBox::BlockBox(box_model(0.0));
1546 root.children = vec![
1547 LayoutChild::Node(Box::new(front)),
1548 LayoutChild::Node(Box::new(normal)),
1549 ];
1550
1551 let child_info = |color, z_index| {
1552 mk_info_node(
1553 NodeKind::Container {
1554 scroll_x: false,
1555 scroll_y: false,
1556 scroll_offset_x: 0.0,
1557 scroll_offset_y: 0.0,
1558 style: ContainerStyle {
1559 background: Background::Color(color),
1560 z_index,
1561 ..ContainerStyle::default()
1562 },
1563 role: ContainerRole::Normal,
1564 },
1565 Vec::new(),
1566 )
1567 };
1568 let root_info = mk_info_node(
1569 NodeKind::Container {
1570 scroll_x: false,
1571 scroll_y: false,
1572 scroll_offset_x: 0.0,
1573 scroll_offset_y: 0.0,
1574 style: ContainerStyle::default(),
1575 role: ContainerRole::Normal,
1576 },
1577 vec![
1578 child_info(Color(255, 0, 0, 255), Some(10)),
1579 child_info(Color(0, 0, 255, 255), None),
1580 ],
1581 );
1582
1583 let mut commands = Vec::new();
1584 generate_draw_commands(&mut commands, &root, &root_info, (100.0, 100.0));
1585 let colors: Vec<_> = commands
1586 .iter()
1587 .filter_map(|command| match command {
1588 DrawCommand::Fill {
1589 paint:
1590 Paint {
1591 brush: Brush::Solid(color),
1592 ..
1593 },
1594 ..
1595 } => Some(*color),
1596 _ => None,
1597 })
1598 .collect();
1599 assert_eq!(colors, vec![Color(0, 0, 255, 255), Color(255, 0, 0, 255)]);
1600 assert!(count_balanced(&commands));
1601 }
1602
1603 fn sticky_viewport(scroll: (f32, f32)) -> StickyViewport {
1604 StickyViewport {
1605 top_left: (-scroll.0, scroll.1),
1606 size: (800.0, 500.0),
1607 }
1608 }
1609
1610 fn sticky_edges(top: Option<f32>, bottom: Option<f32>) -> EdgeOption {
1611 EdgeOption {
1612 left: None,
1613 top,
1614 right: None,
1615 bottom,
1616 }
1617 }
1618
1619 #[test]
1620 fn test_sticky_offset_top_sticks_to_viewport_top() {
1621 let (dx, dy) = sticky_offset(
1624 &sticky_edges(Some(10.0), None),
1625 &ui_rect(0.0, 600.0, 100.0, 50.0),
1626 sticky_viewport((0.0, 600.0)),
1627 (800.0, 2000.0),
1628 );
1629 assert_eq!((dx, dy), (0.0, 10.0));
1630 }
1631
1632 #[test]
1633 fn test_sticky_offset_bottom_pushes_up() {
1634 let (dx, dy) = sticky_offset(
1637 &sticky_edges(None, Some(10.0)),
1638 &ui_rect(0.0, 900.0, 100.0, 100.0),
1639 sticky_viewport((0.0, 0.0)),
1640 (800.0, 2000.0),
1641 );
1642 assert_eq!((dx, dy), (0.0, -510.0));
1643 }
1644
1645 #[test]
1646 fn test_sticky_offset_no_movement_when_in_view() {
1647 let (dx, dy) = sticky_offset(
1648 &sticky_edges(Some(10.0), None),
1649 &ui_rect(0.0, 50.0, 100.0, 50.0),
1650 sticky_viewport((0.0, 0.0)),
1651 (800.0, 2000.0),
1652 );
1653 assert_eq!((dx, dy), (0.0, 0.0));
1654 }
1655
1656 #[test]
1657 fn test_sticky_offset_containing_block_hi_clamp() {
1658 let (dx, dy) = sticky_offset(
1662 &sticky_edges(Some(10.0), None),
1663 &ui_rect(0.0, 900.0, 100.0, 400.0),
1664 sticky_viewport((0.0, 900.0)),
1665 (800.0, 1300.0),
1666 );
1667 assert_eq!((dx, dy), (0.0, 0.0));
1668 }
1669
1670 #[test]
1671 fn test_sticky_offset_inset_fit_ignores_end_edge_without_room() {
1672 let (dx, dy) = sticky_offset(
1675 &sticky_edges(Some(10.0), Some(10.0)),
1676 &ui_rect(0.0, 100.0, 100.0, 500.0),
1677 sticky_viewport((0.0, 0.0)),
1678 (800.0, 2000.0),
1679 );
1680 assert_eq!((dx, dy), (0.0, 0.0));
1681 }
1682
1683 #[test]
1684 fn test_sticky_offset_horizontal_right_pushes_left() {
1685 let edges = EdgeOption {
1688 left: None,
1689 top: None,
1690 right: Some(10.0),
1691 bottom: None,
1692 };
1693 let (dx, dy) = sticky_offset(
1694 &edges,
1695 &ui_rect(850.0, 0.0, 100.0, 50.0),
1696 sticky_viewport((0.0, 0.0)),
1697 (2000.0, 2000.0),
1698 );
1699 assert_eq!((dx, dy), (-160.0, 0.0));
1700 }
1701
1702 #[test]
1703 fn sticky_node_pushes_sticky_offset_transform() {
1704 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
1707 x,
1708 y,
1709 width: w,
1710 height: h,
1711 };
1712 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1713 sticky_edges: None,
1714 border_box: ui_rect(x, y, w, h),
1715 padding_box: ui_rect(x, y, w, h),
1716 content_box: ui_rect(x, y, w, h),
1717 children_box: ui_rect(x, y, w, h),
1718 };
1719
1720 let root_style = Style::default();
1721 let mut root = LayoutNode::new(root_style);
1722 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 800.0, 2000.0));
1723
1724 let mut sticky_style = Style::default();
1725 sticky_style.position.kind = Position::Sticky;
1726 let mut sticky = LayoutNode::new(sticky_style);
1727 let mut sticky_bm = mk_box(0.0, 600.0, 800.0, 100.0);
1728 sticky_bm.sticky_edges = Some(sticky_edges(Some(10.0), None));
1729 sticky.layout_box = ui_layout::LayoutBox::BlockBox(sticky_bm);
1730 root.children = vec![LayoutChild::Node(Box::new(sticky))];
1731
1732 let root_info = mk_info_node(
1733 NodeKind::Container {
1734 scroll_x: false,
1735 scroll_y: false,
1736 scroll_offset_x: 0.0,
1737 scroll_offset_y: 600.0,
1738 style: ContainerStyle::default(),
1739 role: ContainerRole::Normal,
1740 },
1741 vec![mk_info_node(
1742 NodeKind::Container {
1743 scroll_x: false,
1744 scroll_y: false,
1745 scroll_offset_x: 0.0,
1746 scroll_offset_y: 0.0,
1747 style: ContainerStyle::default(),
1748 role: ContainerRole::Normal,
1749 },
1750 Vec::new(),
1751 )],
1752 );
1753
1754 let mut commands = Vec::new();
1755 generate_draw_commands(&mut commands, &root, &root_info, (800.0, 500.0));
1756 assert!(count_balanced(&commands));
1757
1758 let translates = scroll_translates(&commands);
1759 assert!(
1760 translates.contains(&(0.0, 10.0)),
1761 "expected a sticky top offset, got {translates:?}"
1762 );
1763 }
1764
1765 #[test]
1766 fn sticky_node_bottom_pushes_up() {
1767 let ui_rect = |x: f32, y: f32, w: f32, h: f32| ui_layout::Rect {
1769 x,
1770 y,
1771 width: w,
1772 height: h,
1773 };
1774 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1775 sticky_edges: None,
1776 border_box: ui_rect(x, y, w, h),
1777 padding_box: ui_rect(x, y, w, h),
1778 content_box: ui_rect(x, y, w, h),
1779 children_box: ui_rect(x, y, w, h),
1780 };
1781
1782 let root_style = Style::default();
1783 let mut root = LayoutNode::new(root_style);
1784 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 800.0, 2000.0));
1785
1786 let mut sticky_style = Style::default();
1787 sticky_style.position.kind = Position::Sticky;
1788 let mut sticky = LayoutNode::new(sticky_style);
1789 let mut sticky_bm = mk_box(0.0, 900.0, 800.0, 100.0);
1790 sticky_bm.sticky_edges = Some(sticky_edges(None, Some(10.0)));
1791 sticky.layout_box = ui_layout::LayoutBox::BlockBox(sticky_bm);
1792 root.children = vec![LayoutChild::Node(Box::new(sticky))];
1793
1794 let root_info = mk_info_node(
1795 NodeKind::Container {
1796 scroll_x: false,
1797 scroll_y: false,
1798 scroll_offset_x: 0.0,
1799 scroll_offset_y: 0.0,
1800 style: ContainerStyle::default(),
1801 role: ContainerRole::Normal,
1802 },
1803 vec![mk_info_node(
1804 NodeKind::Container {
1805 scroll_x: false,
1806 scroll_y: false,
1807 scroll_offset_x: 0.0,
1808 scroll_offset_y: 0.0,
1809 style: ContainerStyle::default(),
1810 role: ContainerRole::Normal,
1811 },
1812 Vec::new(),
1813 )],
1814 );
1815
1816 let mut commands = Vec::new();
1817 generate_draw_commands(&mut commands, &root, &root_info, (800.0, 500.0));
1818 assert!(count_balanced(&commands));
1819
1820 let translates = scroll_translates(&commands);
1821 assert!(
1822 translates.contains(&(0.0, -510.0)),
1823 "expected a sticky bottom offset, got {translates:?}"
1824 );
1825 }
1826
1827 #[derive(Debug)]
1829 struct PopupNode {
1830 open: bool,
1831 box_height: f32,
1832 popup_height: f32,
1833 }
1834
1835 impl CustomNode for PopupNode {
1836 fn draw_sized(
1837 &self,
1838 _cmd_buf: &mut Vec<DrawCommand>,
1839 _text_style: &TextStyle,
1840 _text_flow_style: &TextFlowStyle,
1841 _style: &Style,
1842 _size: ContentSize,
1843 ) {
1844 }
1845
1846 fn intrinsic_size(&self) -> ContentSize {
1847 ContentSize {
1848 width: 120.0,
1849 height: self.box_height,
1850 }
1851 }
1852
1853 fn popup(
1854 &self,
1855 _text_style: &TextStyle,
1856 _text_flow_style: &TextFlowStyle,
1857 ) -> Option<Popup> {
1858 self.open.then(|| Popup {
1859 rect: crate::engine::renderer_model::Rect {
1860 x: 0.0,
1861 y: self.box_height,
1862 width: 120.0,
1863 height: self.popup_height,
1864 },
1865 commands: vec![DrawCommand::Fill {
1866 path: rect_path(0.0, self.box_height, 120.0, self.popup_height),
1867 rule: FillRule::NonZero,
1868 paint: Paint {
1869 brush: Brush::Solid(Color(255, 0, 0, 255)),
1870 opacity: 1.0,
1871 },
1872 }],
1873 })
1874 }
1875 }
1876
1877 #[test]
1878 fn popup_is_emitted_as_top_layer_at_node_position() {
1879 let node: Arc<dyn CustomNode> = Arc::new(PopupNode {
1883 open: true,
1884 box_height: 28.0,
1885 popup_height: 84.0,
1886 });
1887
1888 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1889 sticky_edges: None,
1890 border_box: ui_rect(x, y, w, h),
1891 padding_box: ui_rect(x, y, w, h),
1892 content_box: ui_rect(x, y, w, h),
1893 children_box: ui_rect(x, y, w, h),
1894 };
1895
1896 let mut root = LayoutNode::new(Style::default());
1897 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 200.0, 200.0));
1898
1899 let mut scroller = LayoutNode::new(Style::default());
1900 scroller.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(10.0, 20.0, 160.0, 100.0));
1901
1902 let mut custom = LayoutNode::new(Style::default());
1903 custom.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(5.0, 0.0, 120.0, 28.0));
1904 scroller.children = vec![LayoutChild::Node(Box::new(custom))];
1905 root.children = vec![LayoutChild::Node(Box::new(scroller))];
1906
1907 let root_info = mk_info_node(
1908 NodeKind::Container {
1909 scroll_x: false,
1910 scroll_y: false,
1911 scroll_offset_x: 0.0,
1912 scroll_offset_y: 0.0,
1913 style: ContainerStyle::default(),
1914 role: ContainerRole::Normal,
1915 },
1916 vec![mk_info_node(
1917 NodeKind::Container {
1918 scroll_x: false,
1919 scroll_y: true,
1920 scroll_offset_x: 0.0,
1921 scroll_offset_y: 50.0,
1922 style: ContainerStyle::default(),
1923 role: ContainerRole::Normal,
1924 },
1925 vec![mk_info_node(
1926 NodeKind::Custom {
1927 node,
1928 scroll_x: false,
1929 scroll_y: false,
1930 scroll_offset_x: 0.0,
1931 scroll_offset_y: 0.0,
1932 style: ContainerStyle::default(),
1933 layout_style: Style::default(),
1934 text_style: TextStyle::default(),
1935 text_flow_style: TextFlowStyle::default(),
1936 },
1937 Vec::new(),
1938 )],
1939 )],
1940 );
1941
1942 let mut commands = Vec::new();
1943 generate_draw_commands(&mut commands, &root, &root_info, (200.0, 200.0));
1944 assert!(count_balanced(&commands));
1945
1946 let fills = commands
1949 .iter()
1950 .filter(|cmd| matches!(cmd, DrawCommand::Fill { .. }))
1951 .count();
1952 assert_eq!(fills, 1, "popup must be the only painted box");
1953
1954 match &commands[commands.len() - 3..] {
1955 [
1956 DrawCommand::PushTransform { transform },
1957 DrawCommand::Fill { .. },
1958 DrawCommand::PopTransform,
1959 ] => {
1960 assert_eq!(transform.apply(0.0, 0.0), (15.0, -30.0));
1961 }
1962 _ => panic!("expected trailing popup PushTransform/Fill/PopTransform"),
1963 }
1964 }
1965
1966 #[test]
1967 fn closed_popup_is_not_emitted() {
1968 let node: Arc<dyn CustomNode> = Arc::new(PopupNode {
1969 open: false,
1970 box_height: 28.0,
1971 popup_height: 84.0,
1972 });
1973
1974 let mk_box = |x: f32, y: f32, w: f32, h: f32| ui_layout::BoxModel {
1975 sticky_edges: None,
1976 border_box: ui_rect(x, y, w, h),
1977 padding_box: ui_rect(x, y, w, h),
1978 content_box: ui_rect(x, y, w, h),
1979 children_box: ui_rect(x, y, w, h),
1980 };
1981
1982 let mut root = LayoutNode::new(Style::default());
1983 root.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 200.0, 200.0));
1984 let mut custom = LayoutNode::new(Style::default());
1985 custom.layout_box = ui_layout::LayoutBox::BlockBox(mk_box(0.0, 0.0, 120.0, 28.0));
1986 root.children = vec![LayoutChild::Node(Box::new(custom))];
1987
1988 let root_info = mk_info_node(
1989 NodeKind::Container {
1990 scroll_x: false,
1991 scroll_y: false,
1992 scroll_offset_x: 0.0,
1993 scroll_offset_y: 0.0,
1994 style: ContainerStyle::default(),
1995 role: ContainerRole::Normal,
1996 },
1997 vec![mk_info_node(
1998 NodeKind::Custom {
1999 node,
2000 scroll_x: false,
2001 scroll_y: false,
2002 scroll_offset_x: 0.0,
2003 scroll_offset_y: 0.0,
2004 style: ContainerStyle::default(),
2005 layout_style: Style::default(),
2006 text_style: TextStyle::default(),
2007 text_flow_style: TextFlowStyle::default(),
2008 },
2009 Vec::new(),
2010 )],
2011 );
2012
2013 let mut commands = Vec::new();
2014 generate_draw_commands(&mut commands, &root, &root_info, (200.0, 200.0));
2015 assert!(count_balanced(&commands));
2016 assert_eq!(
2017 commands
2018 .iter()
2019 .filter(|cmd| matches!(cmd, DrawCommand::Fill { .. }))
2020 .count(),
2021 0
2022 );
2023 }
2024}