1use std::collections::{HashMap, HashSet};
4
5use crate::engine::css::values::{CssValue, Unit};
6use crate::engine::layouter::shift::shift_box;
7use crate::engine::layouter::types::{
8 AnchorSide, AnchorSpec, ContainerStyle, Direction, InfoNode, NodeKind, WritingMode,
9};
10use ui_layout::{LayoutBox, LayoutChild, LayoutNode, LengthOrAuto, Rect};
11
12pub fn parse_anchor_spec(value: &CssValue) -> Option<AnchorSpec> {
14 let CssValue::Function(name, args_groups) = value else {
15 return None;
16 };
17 if !name.eq_ignore_ascii_case("anchor") {
18 return None;
19 }
20
21 let mut groups: Vec<Vec<CssValue>> = args_groups.clone();
22 if groups.is_empty() {
23 return None;
24 }
25 let first_group = groups.remove(0);
26 let second_group = groups.first().cloned();
27
28 let mut element: Option<String> = None;
29 let mut side: Option<AnchorSide> = None;
30 let mut inline_fallback: Option<LengthOrAuto> = None;
31
32 for v in &first_group {
33 match v {
34 CssValue::Keyword(k) => {
35 let lower = k.to_ascii_lowercase();
36 match lower.as_str() {
37 "top" => side = Some(AnchorSide::Top),
38 "right" => side = Some(AnchorSide::Right),
39 "bottom" => side = Some(AnchorSide::Bottom),
40 "left" => side = Some(AnchorSide::Left),
41 "center" => side = Some(AnchorSide::Center),
42 "start" => side = Some(AnchorSide::Start),
43 "end" => side = Some(AnchorSide::End),
44 "self-start" => side = Some(AnchorSide::SelfStart),
45 "self-end" => side = Some(AnchorSide::SelfEnd),
46 "inside" => side = Some(AnchorSide::Inside),
47 "outside" => side = Some(AnchorSide::Outside),
48 _ => {
49 if lower.starts_with("--") {
50 element = Some(k.to_string());
51 }
52 }
53 }
54 }
55 CssValue::Length(val, unit) => {
56 if *unit == Unit::Percent {
57 side = Some(AnchorSide::Percent(*val));
58 } else if let Some(len) = length_from_css_unit(*val, *unit) {
59 inline_fallback = Some(LengthOrAuto::Length(len));
60 }
61 }
62 CssValue::Number(n) if *n == 0.0 => {
63 inline_fallback = Some(LengthOrAuto::Length(ui_layout::Length::Px(0.0)));
64 }
65 _ => {}
66 }
67 }
68
69 let fallback = second_group
70 .as_ref()
71 .and_then(|g| parse_len_percentage(g))
72 .or(inline_fallback);
73
74 Some(AnchorSpec {
75 element,
76 side: side.unwrap_or(AnchorSide::Center),
77 fallback,
78 })
79}
80
81#[derive(Clone, Copy, Debug)]
82struct Point {
83 x: f32,
84 y: f32,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89enum Axis {
90 Vertical,
91 Horizontal,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101struct ScopeInstance {
102 node: usize,
103}
104
105#[derive(Clone, Debug)]
111struct AnchorEntry {
112 rect: Rect,
113 writing_mode: WritingMode,
114 direction: Direction,
115 scope: Option<ScopeInstance>,
116 node_id: usize,
117}
118
119fn length_from_css_unit(val: f32, unit: Unit) -> Option<ui_layout::Length> {
120 match unit {
121 Unit::Px => Some(ui_layout::Length::Px(val)),
122 Unit::Percent => Some(ui_layout::Length::Percent(val)),
123 Unit::Vw => Some(ui_layout::Length::Vw(val)),
124 Unit::Vh => Some(ui_layout::Length::Vh(val)),
125 _ => None,
126 }
127}
128
129fn parse_len_percentage(group: &[CssValue]) -> Option<LengthOrAuto> {
130 for v in group {
131 match v {
132 CssValue::Length(val, unit) => {
133 if let Some(len) = length_from_css_unit(*val, *unit) {
134 return Some(LengthOrAuto::Length(len));
135 }
136 }
137 CssValue::Number(n) if *n == 0.0 => {
138 return Some(LengthOrAuto::Length(ui_layout::Length::Px(0.0)));
139 }
140 CssValue::Keyword(k) if k.eq_ignore_ascii_case("auto") => {
141 return Some(LengthOrAuto::Auto);
142 }
143 _ => {}
144 }
145 }
146 None
147}
148fn box_border_rect(layout_box: &LayoutBox) -> Option<Rect> {
149 match layout_box {
150 LayoutBox::BlockBox(m) => Some(m.border_box),
151 LayoutBox::InlineBox(i) => Some(i.box_model.border_box),
152 LayoutBox::None => None,
153 }
154}
155
156fn box_offsets(layout_box: &LayoutBox) -> (Point, Point) {
161 match layout_box {
162 LayoutBox::BlockBox(m) => (
163 Point {
164 x: m.border_box.x,
165 y: m.border_box.y,
166 },
167 Point {
168 x: m.content_box.x,
169 y: m.content_box.y,
170 },
171 ),
172 LayoutBox::InlineBox(i) => (
173 Point {
174 x: i.box_model.border_box.x,
175 y: i.box_model.border_box.y,
176 },
177 Point {
178 x: i.box_model.content_box.x,
179 y: i.box_model.content_box.y,
180 },
181 ),
182 LayoutBox::None => (Point { x: 0.0, y: 0.0 }, Point { x: 0.0, y: 0.0 }),
183 }
184}
185
186fn custom_border_rect(child: &ui_layout::CustomChild) -> Option<Rect> {
187 child.result().map(|r| r.box_model.border_box)
188}
189
190fn anchor_side_coord(
192 rect: &Rect,
193 side: AnchorSide,
194 axis: Axis,
195 writing_mode: WritingMode,
196 direction: Direction,
197) -> f32 {
198 let low = match axis {
199 Axis::Vertical => rect.y,
200 Axis::Horizontal => rect.x,
201 };
202 let high = match axis {
203 Axis::Vertical => rect.bottom(),
204 Axis::Horizontal => rect.right(),
205 };
206 let mid = low + (high - low) * 0.5;
207 match side {
208 AnchorSide::Top | AnchorSide::Left => low,
209 AnchorSide::Bottom | AnchorSide::Right => high,
210 AnchorSide::Center => mid,
211 AnchorSide::Percent(p) => low + (high - low) * (p / 100.0),
212 AnchorSide::Start | AnchorSide::SelfStart => {
213 if logical_start_is_low(writing_mode, direction, axis) {
214 low
215 } else {
216 high
217 }
218 }
219 AnchorSide::End | AnchorSide::SelfEnd => {
220 if logical_start_is_low(writing_mode, direction, axis) {
221 high
222 } else {
223 low
224 }
225 }
226 AnchorSide::Inside | AnchorSide::Outside => low,
227 }
228}
229
230fn logical_start_is_low(writing_mode: WritingMode, direction: Direction, axis: Axis) -> bool {
233 match writing_mode {
234 WritingMode::HorizontalTb => match axis {
235 Axis::Vertical => true,
237 Axis::Horizontal => direction == Direction::Ltr,
239 },
240 WritingMode::VerticalRl => match axis {
241 Axis::Horizontal => false,
243 Axis::Vertical => true,
245 },
246 WritingMode::VerticalLr => match axis {
247 Axis::Horizontal => true,
249 Axis::Vertical => true,
251 },
252 }
253}
254
255fn style_of(info: &InfoNode) -> Option<&ContainerStyle> {
257 match &info.kind {
258 NodeKind::Container { style, .. } => Some(style),
259 NodeKind::Custom { style, .. } => Some(style),
260 NodeKind::Svg { style, .. } => Some(style),
261 _ => None,
262 }
263}
264
265fn push_scope(
267 node_id: usize,
268 style: Option<&ContainerStyle>,
269 scopes: &mut Vec<ScopeInstance>,
270) -> bool {
271 if let Some(cs) = style
272 && cs.anchor.anchor_scope.is_some()
273 {
274 scopes.push(ScopeInstance { node: node_id });
275 return true;
276 }
277 false
278}
279
280fn collect_anchors(
281 layout: &LayoutNode,
282 info: &InfoNode,
283 parent_origin: Point,
284 anchors: &mut HashMap<String, Vec<AnchorEntry>>,
285 scopes: &mut Vec<ScopeInstance>,
286) {
287 let (border_off, content_off) = box_offsets(&layout.layout_box);
288 let origin = Point {
289 x: parent_origin.x + content_off.x,
290 y: parent_origin.y + content_off.y,
291 };
292
293 let node_id = info as *const InfoNode as usize;
294 let style = style_of(info);
295 let pushed = push_scope(node_id, style, scopes);
296
297 if let Some(cs) = style
298 && let Some(b) = box_border_rect(&layout.layout_box)
299 {
300 let entry = AnchorEntry {
301 rect: Rect {
302 x: parent_origin.x + border_off.x,
303 y: parent_origin.y + border_off.y,
304 width: b.width,
305 height: b.height,
306 },
307 writing_mode: cs.writing_mode,
308 direction: cs.direction,
309 scope: scopes.last().copied(),
310 node_id,
311 };
312 for name in &cs.anchor.anchors {
313 anchors.entry(name.clone()).or_default().push(entry.clone());
314 }
315 }
316
317 for (child_idx, child) in layout.children.iter().enumerate() {
318 match child {
319 LayoutChild::Node(n) => {
320 if let Some(info_child) = info.children.get(child_idx) {
321 collect_anchors(n, info_child, origin, anchors, scopes);
322 }
323 }
324 LayoutChild::Custom(c) => {
325 if let Some(info_child) = info.children.get(child_idx)
326 && let Some(cs) = style_of(info_child)
327 && let Some(b) = custom_border_rect(c)
328 {
329 let entry = AnchorEntry {
330 rect: Rect {
331 x: origin.x + b.x,
332 y: origin.y + b.y,
333 width: b.width,
334 height: b.height,
335 },
336 writing_mode: cs.writing_mode,
337 direction: cs.direction,
338 scope: scopes.last().copied(),
339 node_id: info_child as *const InfoNode as usize,
340 };
341 for name in &cs.anchor.anchors {
342 anchors.entry(name.clone()).or_default().push(entry.clone());
343 }
344 }
345 }
346 LayoutChild::Fragment(_) => {}
347 }
348 }
349
350 if pushed {
351 scopes.pop();
352 }
353}
354
355fn scope_visible(scope: Option<ScopeInstance>, active: &[ScopeInstance]) -> bool {
358 match scope {
359 None => true,
360 Some(instance) => active.contains(&instance),
361 }
362}
363
364fn scope_rank(scope: Option<ScopeInstance>, active: &[ScopeInstance]) -> Option<usize> {
366 scope.and_then(|instance| active.iter().rposition(|a| *a == instance))
367}
368
369fn nearest_ancestor<'a>(
372 entries: &[&'a AnchorEntry],
373 target_ancestors: &[usize],
374) -> Option<&'a AnchorEntry> {
375 let mut best: Option<(&'a AnchorEntry, usize)> = None;
376 for entry in entries {
377 if let Some(pos) = target_ancestors.iter().rposition(|&id| id == entry.node_id)
378 && best.is_none_or(|(_, best_pos)| pos > best_pos)
379 {
380 best = Some((entry, pos));
381 }
382 }
383 best.map(|(entry, _)| entry)
384}
385
386#[derive(Default)]
399struct AnchorContext {
400 containing_block: HashMap<usize, Option<usize>>,
403 order: HashMap<usize, usize>,
405 out_of_flow: HashSet<usize>,
407}
408
409impl AnchorContext {
410 fn build(layout: &LayoutNode, info: &InfoNode) -> Self {
411 fn walk(
412 layout: &LayoutNode,
413 info: &InfoNode,
414 parent: Option<usize>,
415 positioned: &mut Vec<usize>,
416 order: &mut usize,
417 ctx: &mut AnchorContext,
418 ) {
419 let id = info as *const InfoNode as usize;
420 let kind = layout.style.position.kind;
421 let containing_block = if kind.is_out_of_flow() {
422 positioned.last().copied()
423 } else {
424 parent
425 };
426 ctx.containing_block.insert(id, containing_block);
427 ctx.order.insert(id, *order);
428 *order += 1;
429 if kind.is_out_of_flow() {
430 ctx.out_of_flow.insert(id);
431 }
432 let establishes_cb = kind != ui_layout::Position::Static;
433 if establishes_cb {
434 positioned.push(id);
435 }
436 for (idx, child) in layout.children.iter().enumerate() {
437 if let LayoutChild::Node(n) = child
438 && let Some(child_info) = info.children.get(idx)
439 {
440 walk(n, child_info, Some(id), positioned, order, ctx);
441 }
442 }
443 if establishes_cb {
444 positioned.pop();
445 }
446 }
447
448 let mut ctx = AnchorContext::default();
449 let mut order = 0usize;
450 walk(layout, info, None, &mut Vec::new(), &mut order, &mut ctx);
451 ctx
452 }
453
454 fn acceptable(&self, anchor: &AnchorEntry, target: usize, target_ancestors: &[usize]) -> bool {
455 if target_ancestors.contains(&anchor.node_id) {
457 return true;
458 }
459 let target_cb = self.containing_block.get(&target).copied().flatten();
460 let mut current = Some(anchor.node_id);
461 while let Some(node) = current {
462 let node_cb = self.containing_block.get(&node).copied().flatten();
463 if node_cb == target_cb {
464 return !self.out_of_flow.contains(&node)
465 || self.order.get(&node) < self.order.get(&target);
466 }
467 current = node_cb;
468 }
469 false
470 }
471}
472
473fn find_anchor_entry<'a>(
474 spec: &AnchorSpec,
475 container_style: &ContainerStyle,
476 anchors: &'a HashMap<String, Vec<AnchorEntry>>,
477 active: &[ScopeInstance],
478 target: usize,
479 target_ancestors: &[usize],
480 ctx: &AnchorContext,
481) -> Option<&'a AnchorEntry> {
482 let name = spec
483 .element
484 .as_ref()
485 .or(container_style.anchor.position_anchor.as_ref())?;
486 let candidates = anchors.get(name)?;
487 let visible: Vec<&AnchorEntry> = candidates
488 .iter()
489 .filter(|entry| scope_visible(entry.scope, active))
490 .filter(|entry| ctx.acceptable(entry, target, target_ancestors))
491 .collect();
492 if visible.is_empty() {
493 return None;
494 }
495 if let Some(entry) = nearest_ancestor(&visible, target_ancestors) {
499 return Some(entry);
500 }
501 let mut best: Option<&AnchorEntry> = None;
504 let mut best_rank: isize = -1;
505 for entry in visible {
506 let rank = scope_rank(entry.scope, active).map_or(0, |p| p as isize + 1);
507 if rank >= best_rank {
508 best_rank = rank;
509 best = Some(entry);
510 }
511 }
512 best
513}
514
515#[derive(Clone, Copy)]
516enum Edge {
517 Low,
518 High,
519}
520
521fn margin_px(value: &ui_layout::LengthOrAuto) -> f32 {
523 match value {
524 ui_layout::LengthOrAuto::Length(ui_layout::Length::Px(v)) => *v,
525 _ => 0.0,
526 }
527}
528
529fn resolve_node(
530 layout: &mut LayoutNode,
531 info: &InfoNode,
532 parent_origin: Point,
533 anchors: &HashMap<String, Vec<AnchorEntry>>,
534 scopes: &mut Vec<ScopeInstance>,
535 node_path: &mut Vec<usize>,
536 ctx: &AnchorContext,
537) -> bool {
538 let (border_off, content_off) = box_offsets(&layout.layout_box);
539 let border_origin = Point {
540 x: parent_origin.x + border_off.x,
541 y: parent_origin.y + border_off.y,
542 };
543 let origin = Point {
544 x: parent_origin.x + content_off.x,
545 y: parent_origin.y + content_off.y,
546 };
547 let border = box_border_rect(&layout.layout_box);
548
549 let node_id = info as *const InfoNode as usize;
550 let container_style = style_of(info).cloned();
551 let mut dx: f32 = 0.0;
552 let mut dy: f32 = 0.0;
553
554 let active: Vec<ScopeInstance> = scopes.clone();
557
558 if let Some(cs) = &container_style {
559 let margin_top = margin_px(&layout.style.spacing.margin_top);
562 let margin_bottom = margin_px(&layout.style.spacing.margin_bottom);
563 let margin_left = margin_px(&layout.style.spacing.margin_left);
564 let margin_right = margin_px(&layout.style.spacing.margin_right);
565
566 let mut apply = |spec: &AnchorSpec, axis: Axis, edge: Edge| {
567 let Some(entry) =
568 find_anchor_entry(spec, cs, anchors, &active, node_id, node_path, ctx)
569 else {
570 return;
571 };
572 let (wm, dir) = match spec.side {
573 AnchorSide::SelfStart | AnchorSide::SelfEnd => (cs.writing_mode, cs.direction),
574 _ => (entry.writing_mode, entry.direction),
575 };
576 let resolved_side = match spec.side {
577 AnchorSide::Inside => match (axis, edge) {
578 (Axis::Vertical, Edge::Low) => AnchorSide::Top,
579 (Axis::Vertical, Edge::High) => AnchorSide::Bottom,
580 (Axis::Horizontal, Edge::Low) => AnchorSide::Left,
581 (Axis::Horizontal, Edge::High) => AnchorSide::Right,
582 },
583 AnchorSide::Outside => match (axis, edge) {
584 (Axis::Vertical, Edge::Low) => AnchorSide::Bottom,
585 (Axis::Vertical, Edge::High) => AnchorSide::Top,
586 (Axis::Horizontal, Edge::Low) => AnchorSide::Right,
587 (Axis::Horizontal, Edge::High) => AnchorSide::Left,
588 },
589 other => other,
590 };
591 let target = anchor_side_coord(&entry.rect, resolved_side, axis, wm, dir);
592 if let Some(b) = border {
593 let current = match edge {
594 Edge::Low => match axis {
595 Axis::Vertical => border_origin.y - margin_top,
596 Axis::Horizontal => border_origin.x - margin_left,
597 },
598 Edge::High => match axis {
599 Axis::Vertical => border_origin.y + b.height + margin_bottom,
600 Axis::Horizontal => border_origin.x + b.width + margin_right,
601 },
602 };
603 match axis {
604 Axis::Vertical => dy += target - current,
605 Axis::Horizontal => dx += target - current,
606 }
607 }
608 };
609
610 match (&cs.anchor.top, &cs.anchor.bottom) {
613 (Some(spec), _) => apply(spec, Axis::Vertical, Edge::Low),
614 (None, Some(spec)) => apply(spec, Axis::Vertical, Edge::High),
615 (None, None) => {}
616 }
617 match (&cs.anchor.left, &cs.anchor.right) {
618 (Some(spec), _) => apply(spec, Axis::Horizontal, Edge::Low),
619 (None, Some(spec)) => apply(spec, Axis::Horizontal, Edge::High),
620 (None, None) => {}
621 }
622 }
623
624 let shifted = dx.abs() > 0.001 || dy.abs() > 0.001;
625 if shifted {
626 shift_box(&mut layout.layout_box, dx, dy);
627 }
628
629 let origin = Point {
632 x: origin.x + dx,
633 y: origin.y + dy,
634 };
635
636 let pushed = push_scope(node_id, container_style.as_ref(), scopes);
637
638 node_path.push(node_id);
641
642 let mut child_shifted = false;
643 for (child_idx, child) in layout.children.iter_mut().enumerate() {
644 if let LayoutChild::Node(n) = child
645 && let Some(info_child) = info.children.get(child_idx)
646 {
647 child_shifted |= resolve_node(n, info_child, origin, anchors, scopes, node_path, ctx);
648 }
649 }
650
651 node_path.pop();
652
653 if pushed {
654 scopes.pop();
655 }
656
657 shifted || child_shifted
658}
659
660pub fn resolve_anchor_positioning(layout: &mut LayoutNode, info: &InfoNode) {
665 let origin = Point { x: 0.0, y: 0.0 };
666 let ctx = AnchorContext::build(layout, info);
667 const MAX_PASSES: usize = 4;
671 for _ in 0..MAX_PASSES {
672 let mut anchors: HashMap<String, Vec<AnchorEntry>> = HashMap::new();
673 let mut collect_scopes: Vec<ScopeInstance> = Vec::new();
674 collect_anchors(layout, info, origin, &mut anchors, &mut collect_scopes);
675 let mut resolve_scopes: Vec<ScopeInstance> = Vec::new();
676 let mut node_path: Vec<usize> = Vec::new();
677 if !resolve_node(
678 layout,
679 info,
680 origin,
681 &anchors,
682 &mut resolve_scopes,
683 &mut node_path,
684 &ctx,
685 ) {
686 break;
687 }
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 fn rect() -> Rect {
696 Rect {
697 x: 10.0,
698 y: 20.0,
699 width: 100.0,
700 height: 40.0,
701 }
702 }
703
704 #[test]
705 fn logical_start_low_horizontal_ltr() {
706 assert!(logical_start_is_low(
707 WritingMode::HorizontalTb,
708 Direction::Ltr,
709 Axis::Vertical
710 ));
711 assert!(logical_start_is_low(
712 WritingMode::HorizontalTb,
713 Direction::Ltr,
714 Axis::Horizontal
715 ));
716 }
717
718 #[test]
719 fn logical_start_low_horizontal_rtl() {
720 assert!(logical_start_is_low(
721 WritingMode::HorizontalTb,
722 Direction::Rtl,
723 Axis::Vertical
724 ));
725 assert!(!logical_start_is_low(
726 WritingMode::HorizontalTb,
727 Direction::Rtl,
728 Axis::Horizontal
729 ));
730 }
731
732 #[test]
733 fn logical_start_low_vertical_rl() {
734 assert!(!logical_start_is_low(
735 WritingMode::VerticalRl,
736 Direction::Ltr,
737 Axis::Horizontal
738 ));
739 assert!(logical_start_is_low(
740 WritingMode::VerticalRl,
741 Direction::Ltr,
742 Axis::Vertical
743 ));
744 }
745
746 #[test]
747 fn anchor_side_coord_logical_inline_rtl() {
748 let r = rect();
749 assert_eq!(
751 anchor_side_coord(
752 &r,
753 AnchorSide::Start,
754 Axis::Horizontal,
755 WritingMode::HorizontalTb,
756 Direction::Rtl
757 ),
758 r.right()
759 );
760 assert_eq!(
761 anchor_side_coord(
762 &r,
763 AnchorSide::End,
764 Axis::Horizontal,
765 WritingMode::HorizontalTb,
766 Direction::Rtl
767 ),
768 r.x
769 );
770 }
771
772 #[test]
773 fn anchor_side_coord_physical_and_percent() {
774 let r = rect();
775 assert_eq!(
776 anchor_side_coord(
777 &r,
778 AnchorSide::Top,
779 Axis::Vertical,
780 WritingMode::HorizontalTb,
781 Direction::Ltr
782 ),
783 r.y
784 );
785 assert_eq!(
786 anchor_side_coord(
787 &r,
788 AnchorSide::Bottom,
789 Axis::Vertical,
790 WritingMode::HorizontalTb,
791 Direction::Ltr
792 ),
793 r.bottom()
794 );
795 assert_eq!(
796 anchor_side_coord(
797 &r,
798 AnchorSide::Percent(50.0),
799 Axis::Vertical,
800 WritingMode::HorizontalTb,
801 Direction::Ltr
802 ),
803 r.y + r.height * 0.5
804 );
805 }
806
807 use crate::engine::layouter::types::{ContainerRole, InfoNode, NodeKind};
808 use ui_layout::BoxModel;
809
810 fn model(x: f32, y: f32, w: f32, h: f32) -> BoxModel {
811 let r = Rect {
812 x,
813 y,
814 width: w,
815 height: h,
816 };
817 BoxModel {
818 sticky_edges: None,
819 border_box: r,
820 padding_box: r,
821 content_box: r,
822 children_box: r,
823 }
824 }
825
826 fn container(style: ContainerStyle, children: Vec<InfoNode>) -> InfoNode {
827 InfoNode {
828 kind: NodeKind::Container {
829 scroll_x: false,
830 scroll_y: false,
831 scroll_offset_x: 0.0,
832 scroll_offset_y: 0.0,
833 style,
834 role: ContainerRole::Normal,
835 },
836 children,
837 dom_id: None,
838 }
839 }
840
841 fn block(children: Vec<LayoutChild>, m: BoxModel) -> LayoutNode {
842 let mut n = LayoutNode::new(ui_layout::Style::default());
843 n.children = children;
844 n.layout_box = LayoutBox::BlockBox(m);
845 n
846 }
847
848 fn top_spec() -> AnchorSpec {
849 AnchorSpec {
850 element: None,
851 side: AnchorSide::Top,
852 fallback: None,
853 }
854 }
855
856 fn bottom_spec() -> AnchorSpec {
857 AnchorSpec {
858 element: None,
859 side: AnchorSide::Bottom,
860 fallback: None,
861 }
862 }
863
864 #[test]
865 fn resolve_shifts_target_top_to_anchor_top_across_padding() {
866 let mut anchor_style = ContainerStyle::default();
867 anchor_style.anchor.anchors = vec!["--a".to_string()];
868 let mut target_style = ContainerStyle::default();
869 target_style.anchor.position_anchor = Some("--a".to_string());
870 target_style.anchor.top = Some(top_spec());
871
872 let anchor_node = block(vec![], model(10.0, 20.0, 100.0, 40.0));
873 let target_node = block(vec![], model(10.0, 200.0, 50.0, 30.0));
874 let mut root = block(
875 vec![
876 LayoutChild::Node(Box::new(anchor_node)),
877 LayoutChild::Node(Box::new(target_node)),
878 ],
879 BoxModel {
880 sticky_edges: None,
881 border_box: Rect {
882 x: 0.0,
883 y: 0.0,
884 width: 500.0,
885 height: 500.0,
886 },
887 padding_box: Rect {
888 x: 0.0,
889 y: 0.0,
890 width: 500.0,
891 height: 500.0,
892 },
893 content_box: Rect {
896 x: 5.0,
897 y: 7.0,
898 width: 490.0,
899 height: 486.0,
900 },
901 children_box: Rect {
902 x: 5.0,
903 y: 7.0,
904 width: 490.0,
905 height: 486.0,
906 },
907 },
908 );
909
910 let info = container(
911 ContainerStyle::default(),
912 vec![
913 container(anchor_style, vec![]),
914 container(target_style, vec![]),
915 ],
916 );
917
918 resolve_anchor_positioning(&mut root, &info);
919
920 let target = root.children[1].node().unwrap();
922 let target_box = target.layout_box.iter().next().unwrap();
923 assert!((target_box.border_box.y - 20.0).abs() < 0.01);
925 }
926
927 #[test]
928 fn resolve_shifts_target_bottom_to_anchor_bottom() {
929 let mut anchor_style = ContainerStyle::default();
930 anchor_style.anchor.anchors = vec!["--a".to_string()];
931 let mut target_style = ContainerStyle::default();
932 target_style.anchor.position_anchor = Some("--a".to_string());
933 target_style.anchor.bottom = Some(bottom_spec());
934
935 let anchor_node = block(vec![], model(10.0, 20.0, 100.0, 40.0));
936 let target_node = block(vec![], model(10.0, 200.0, 50.0, 30.0));
937 let mut root = block(
938 vec![
939 LayoutChild::Node(Box::new(anchor_node)),
940 LayoutChild::Node(Box::new(target_node)),
941 ],
942 model(0.0, 0.0, 500.0, 500.0),
943 );
944
945 let info = container(
946 ContainerStyle::default(),
947 vec![
948 container(anchor_style, vec![]),
949 container(target_style, vec![]),
950 ],
951 );
952
953 resolve_anchor_positioning(&mut root, &info);
954
955 let target = root.children[1].node().unwrap();
958 let target_box = target.layout_box.iter().next().unwrap();
959 assert!((target_box.border_box.y - 30.0).abs() < 0.01);
960 }
961
962 #[test]
963 fn anchor_scope_hides_anchor_from_outside_subtree() {
964 let mut scoped_anchor_style = ContainerStyle::default();
965 scoped_anchor_style.anchor.anchors = vec!["--a".to_string()];
966 let mut scope_holder_style = ContainerStyle::default();
967 scope_holder_style.anchor.anchor_scope = Some("--a".to_string());
968 let mut target_style = ContainerStyle::default();
969 target_style.anchor.position_anchor = Some("--a".to_string());
970 target_style.anchor.top = Some(top_spec());
971
972 let scope_holder = block(
974 vec![LayoutChild::Node(Box::new(block(
975 vec![],
976 model(0.0, 0.0, 10.0, 10.0),
977 )))],
978 model(0.0, 0.0, 10.0, 10.0),
979 );
980 let target_node = block(vec![], model(0.0, 100.0, 10.0, 10.0));
981 let mut root = block(
982 vec![
983 LayoutChild::Node(Box::new(scope_holder)),
984 LayoutChild::Node(Box::new(target_node)),
985 ],
986 model(0.0, 0.0, 500.0, 500.0),
987 );
988
989 let info = container(
990 ContainerStyle::default(),
991 vec![
992 container(
993 scope_holder_style,
994 vec![container(scoped_anchor_style, vec![])],
995 ),
996 container(target_style, vec![]),
997 ],
998 );
999
1000 resolve_anchor_positioning(&mut root, &info);
1001
1002 let target = root.children[1].node().unwrap();
1005 let target_box = target.layout_box.iter().next().unwrap();
1006 assert!((target_box.border_box.y - 100.0).abs() < 0.01);
1007 }
1008
1009 #[test]
1010 fn anchor_top_applies_margin_offset() {
1011 let mut anchor_style = ContainerStyle::default();
1012 anchor_style.anchor.anchors = vec!["--a".to_string()];
1013 let mut target_style = ContainerStyle::default();
1014 target_style.anchor.position_anchor = Some("--a".to_string());
1015 target_style.anchor.top = Some(top_spec());
1016
1017 let anchor_node = block(vec![], model(10.0, 20.0, 100.0, 40.0));
1018 let mut target_node = block(vec![], model(10.0, 200.0, 50.0, 30.0));
1019 target_node.style.spacing.margin_top =
1020 ui_layout::LengthOrAuto::Length(ui_layout::Length::Px(5.0));
1021 let mut root = block(
1022 vec![
1023 LayoutChild::Node(Box::new(anchor_node)),
1024 LayoutChild::Node(Box::new(target_node)),
1025 ],
1026 model(0.0, 0.0, 500.0, 500.0),
1027 );
1028
1029 let info = container(
1030 ContainerStyle::default(),
1031 vec![
1032 container(anchor_style, vec![]),
1033 container(target_style, vec![]),
1034 ],
1035 );
1036
1037 resolve_anchor_positioning(&mut root, &info);
1038
1039 let target = root.children[1].node().unwrap();
1041 let target_box = target.layout_box.iter().next().unwrap();
1042 assert!((target_box.border_box.y - 5.0 - 20.0).abs() < 0.01);
1043 }
1044
1045 #[test]
1046 fn anchor_top_wins_when_both_vertical_edges_present() {
1047 let mut anchor_style = ContainerStyle::default();
1048 anchor_style.anchor.anchors = vec!["--a".to_string()];
1049 let mut target_style = ContainerStyle::default();
1050 target_style.anchor.position_anchor = Some("--a".to_string());
1051 target_style.anchor.top = Some(top_spec());
1052 target_style.anchor.bottom = Some(bottom_spec());
1053
1054 let anchor_node = block(vec![], model(10.0, 20.0, 100.0, 40.0));
1055 let target_node = block(vec![], model(10.0, 200.0, 50.0, 30.0));
1056 let mut root = block(
1057 vec![
1058 LayoutChild::Node(Box::new(anchor_node)),
1059 LayoutChild::Node(Box::new(target_node)),
1060 ],
1061 model(0.0, 0.0, 500.0, 500.0),
1062 );
1063
1064 let info = container(
1065 ContainerStyle::default(),
1066 vec![
1067 container(anchor_style, vec![]),
1068 container(target_style, vec![]),
1069 ],
1070 );
1071
1072 resolve_anchor_positioning(&mut root, &info);
1073
1074 let target = root.children[1].node().unwrap();
1077 let target_box = target.layout_box.iter().next().unwrap();
1078 assert!((target_box.border_box.y - 20.0).abs() < 0.01);
1079 }
1080
1081 fn entry(scope: Option<ScopeInstance>, node_id: usize) -> AnchorEntry {
1082 AnchorEntry {
1083 rect: Rect {
1084 x: 0.0,
1085 y: 0.0,
1086 width: 10.0,
1087 height: 10.0,
1088 },
1089 writing_mode: WritingMode::HorizontalTb,
1090 direction: Direction::Ltr,
1091 scope,
1092 node_id,
1093 }
1094 }
1095
1096 fn name_spec() -> AnchorSpec {
1097 AnchorSpec {
1098 element: Some("--a".to_string()),
1099 side: AnchorSide::Bottom,
1100 fallback: None,
1101 }
1102 }
1103
1104 #[test]
1105 fn find_anchor_entry_prefers_nearest_ancestor_over_later_sibling() {
1106 let mut anchors: HashMap<String, Vec<AnchorEntry>> = HashMap::new();
1107 anchors.insert("--a".to_string(), vec![entry(None, 1), entry(None, 2)]);
1108 let cs = ContainerStyle::default();
1109 let ctx = AnchorContext::default();
1110
1111 let found = find_anchor_entry(&name_spec(), &cs, &anchors, &[], 99, &[0, 1, 7], &ctx);
1114 assert_eq!(found.unwrap().node_id, 1);
1115
1116 let found = find_anchor_entry(&name_spec(), &cs, &anchors, &[], 99, &[0, 5, 7], &ctx);
1118 assert_eq!(found.unwrap().node_id, 2);
1119 }
1120
1121 #[test]
1122 fn find_anchor_entry_prefers_deepest_ancestor() {
1123 let mut anchors: HashMap<String, Vec<AnchorEntry>> = HashMap::new();
1124 anchors.insert("--a".to_string(), vec![entry(None, 3), entry(None, 1)]);
1125 let cs = ContainerStyle::default();
1126 let ctx = AnchorContext::default();
1127
1128 let found = find_anchor_entry(&name_spec(), &cs, &anchors, &[], 99, &[0, 3, 1], &ctx);
1131 assert_eq!(found.unwrap().node_id, 1);
1132 }
1133
1134 #[test]
1135 fn find_anchor_entry_scope_instance_isolates_same_name_scopes() {
1136 let mut anchors: HashMap<String, Vec<AnchorEntry>> = HashMap::new();
1140 anchors.insert(
1141 "--a".to_string(),
1142 vec![
1143 entry(Some(ScopeInstance { node: 10 }), 10),
1144 entry(Some(ScopeInstance { node: 20 }), 20),
1145 ],
1146 );
1147 let cs = ContainerStyle::default();
1148 let ctx = AnchorContext::default();
1149
1150 let active = [ScopeInstance { node: 20 }];
1151 let found =
1152 find_anchor_entry(&name_spec(), &cs, &anchors, &active, 99, &[20], &ctx).unwrap();
1153 assert_eq!(found.node_id, 20);
1154
1155 assert!(find_anchor_entry(&name_spec(), &cs, &anchors, &[], 99, &[], &ctx).is_none());
1157 }
1158
1159 #[test]
1160 fn anchor_context_rejects_anchor_in_other_containing_block() {
1161 let ctx = AnchorContext {
1165 containing_block: HashMap::from([
1166 (1, Some(10)), (2, Some(20)), (10, Some(0)), (20, Some(0)), (0, None), (3, Some(10)), ]),
1173 order: HashMap::from([(0, 0), (10, 1), (1, 2), (3, 3), (20, 4), (2, 5)]),
1174 out_of_flow: HashSet::new(),
1175 };
1176
1177 let own = entry(None, 1);
1178 let other = entry(None, 2);
1179 assert!(ctx.acceptable(&own, 3, &[0, 10]));
1180 assert!(!ctx.acceptable(&other, 3, &[0, 10]));
1181 }
1182
1183 #[test]
1184 fn anchor_context_rejects_out_of_flow_anchor_after_target() {
1185 let ctx = AnchorContext {
1189 containing_block: HashMap::from([(0, None), (1, Some(0)), (5, Some(0)), (2, Some(0))]),
1190 order: HashMap::from([(0, 0), (1, 1), (5, 2), (2, 3)]),
1191 out_of_flow: HashSet::from([1, 2]),
1192 };
1193
1194 let before = entry(None, 1);
1195 let after = entry(None, 2);
1196 assert!(ctx.acceptable(&before, 5, &[0]));
1197 assert!(!ctx.acceptable(&after, 5, &[0]));
1198 }
1199}