Skip to main content

orinium_browser/engine/layouter/builder/
anchor.rs

1//! CSS Anchor Positioning resolution.
2
3use 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
12/// Parse a single `<anchor()>` function value into `AnchorSpec`.
13pub 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/// Which axis an inset property resolves along.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89enum Axis {
90    Vertical,
91    Horizontal,
92}
93
94/// Identity of a single `anchor-scope` instance: the element that declared it.
95///
96/// Scopes are distinguished per *instance* (the subtree root that set the
97/// scope), not merely by name. Two unrelated subtrees may both use the same
98/// scope name, and anchors inside them must not leak to positioned elements in
99/// the other subtree.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101struct ScopeInstance {
102    node: usize,
103}
104
105/// A collected anchor: its page-space border box, the writing mode and
106/// direction of the anchor element, the innermost `anchor-scope` instance that
107/// was active where the anchor was declared (`None` means globally scoped), and
108/// the stable identity of the declaring node (used to prefer anchors that are
109/// ancestors of the target).
110#[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
156/// Offsets of a layout box relative to its parent's content box: the
157/// `(border, content)` origins. Children are laid out relative to the parent's
158/// content box, so a node's own border origin uses `border_box`, while the
159/// origin handed to its children uses `content_box`.
160fn 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
190/// Map an `AnchorSide` to a coordinate along `axis`, honoring logical sides.
191fn 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
230/// Whether the logical-start edge is the low-coordinate edge along `axis` for
231/// the given writing mode and direction.
232fn logical_start_is_low(writing_mode: WritingMode, direction: Direction, axis: Axis) -> bool {
233    match writing_mode {
234        WritingMode::HorizontalTb => match axis {
235            // block-start = top
236            Axis::Vertical => true,
237            // inline-start = left (ltr) / right (rtl)
238            Axis::Horizontal => direction == Direction::Ltr,
239        },
240        WritingMode::VerticalRl => match axis {
241            // block flow runs right-to-left
242            Axis::Horizontal => false,
243            // inline flow runs top-to-bottom
244            Axis::Vertical => true,
245        },
246        WritingMode::VerticalLr => match axis {
247            // block flow runs left-to-right
248            Axis::Horizontal => true,
249            // inline flow runs top-to-bottom
250            Axis::Vertical => true,
251        },
252    }
253}
254
255/// Extract the anchor-relevant style plus writing mode/direction for a node.
256fn 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
265/// Push the scope active for this node's subtree. Returns `true` if pushed.
266fn 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
355/// Whether an anchor declared under scope instance `scope` is visible from an
356/// element whose active scope chain is `active`.
357fn scope_visible(scope: Option<ScopeInstance>, active: &[ScopeInstance]) -> bool {
358    match scope {
359        None => true,
360        Some(instance) => active.contains(&instance),
361    }
362}
363
364/// The depth of `scope` among the element's active scope instances, if any.
365fn scope_rank(scope: Option<ScopeInstance>, active: &[ScopeInstance]) -> Option<usize> {
366    scope.and_then(|instance| active.iter().rposition(|a| *a == instance))
367}
368
369/// Of `entries`, return the one whose declaring node is the nearest ancestor
370/// of the target (the deepest entry in the target's ancestor chain), if any.
371fn 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/// Per-element layout facts used to decide whether an anchor is *acceptable*
387/// for a given positioned element (CSS “acceptable anchor element”).
388///
389/// An anchor is acceptable for a target when it is laid out strictly before the
390/// target in a compatible containing-block context: either the anchor is an
391/// ancestor of the target, or walking the anchor's containing-block chain
392/// reaches an element that shares the target's containing block and that
393/// element is not itself an out-of-flow box laid out after the target.
394///
395/// This is what stops two same-name anchors living in *different* containing
396/// blocks (e.g. two sibling `position: relative` cards) from being visible to
397/// each other's targets.
398#[derive(Default)]
399struct AnchorContext {
400    /// Containing block element of each node (`None` = initial containing
401    /// block / viewport). Keyed by the node identity used in `AnchorEntry`.
402    containing_block: HashMap<usize, Option<usize>>,
403    /// Pre-order index, used to approximate flat-tree order.
404    order: HashMap<usize, usize>,
405    /// Nodes whose box is out of flow (`position: absolute | fixed`).
406    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        // Ancestors are always laid out before the target and can anchor it.
456        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    // Per CSS Anchor Positioning the anchor element is the nearest ancestor of
496    // the target (if any) among the visible candidates, otherwise the last one
497    // in DOM order. Prefer the nearest ancestor first.
498    if let Some(entry) = nearest_ancestor(&visible, target_ancestors) {
499        return Some(entry);
500    }
501    // Fall back to the last visible candidate in document order (the existing
502    // scope-depth preference keeps the innermost declared scope winning).
503    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
521/// Extract a pixel margin, treating `auto` and non-pixel lengths as `0`.
522fn 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    // Scope chain for this element's own lookups (excludes its own
555    // `anchor-scope`, which only affects descendants).
556    let active: Vec<ScopeInstance> = scopes.clone();
557
558    if let Some(cs) = &container_style {
559        // The inset positions the margin box, so a border edge target is
560        // offset by the corresponding margin (`auto` margins stay `0`).
561        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        // Only one edge per axis is honored: translation cannot satisfy both
611        // simultaneously (that would require resizing the box).
612        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    // Children are positioned relative to this node's content box, so a shift
630    // of this node moves them too.
631    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    // This node becomes an ancestor of its children, so record it before
639    // descending for the ancestor-preference lookup.
640    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
660/// Resolve CSS anchor positioning for all positioned elements.
661///
662/// Walks the tree after layout to collect anchor rects (by `anchor-name`),
663/// then shifts elements that use `anchor()` inset functions.
664pub 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    // Anchors may themselves move while resolving (e.g. an anchor nested in a
668    // subtree that also gets shifted), so re-collect and re-resolve until the
669    // layout stabilizes. The pass count is bounded to guarantee termination.
670    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        // Inline-start in RTL horizontal is the right edge.
750        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                // Non-zero content offset: verifies children are resolved
894                // relative to the parent content box.
895                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        // Anchor page origin = root content (5,7) + anchor border (10,20) = (15,27).
921        let target = root.children[1].node().unwrap();
922        let target_box = target.layout_box.iter().next().unwrap();
923        // Target border page origin = root content (5,7) + target border y (20) = 27.
924        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        // Anchor bottom page = 20 + 40 = 60. Target border y should become 30 so
956        // its bottom edge lands on the anchor bottom.
957        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        // root -> [scope_holder -> anchor, target]
973        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        // `--a` is scoped to `scope_holder`'s subtree, so the outside target
1003        // must not move.
1004        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        // Margin box top (border y - 5) must align with the anchor top (20).
1040        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        // Only the top edge is honored (no resizing), so the result matches the
1075        // top-only resolution.
1076        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        // Target's ancestor chain (root -> ... -> 1) contains node 1, which is
1112        // *not* the last entry, so the ancestor must win.
1113        let found = find_anchor_entry(&name_spec(), &cs, &anchors, &[], 99, &[0, 1, 7], &ctx);
1114        assert_eq!(found.unwrap().node_id, 1);
1115
1116        // Without any ancestor, fall back to the last entry in DOM order.
1117        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        // Both 1 and 3 are ancestors; the deeper one (1) must win even though 3
1129        // appears first in DOM order.
1130        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        // Two different `anchor-scope: --a` instances both hold a `--a` anchor.
1137        // A target inside instance 10 must only ever see the anchor under 10,
1138        // never the one under 20 (which shares the same scope *name*).
1139        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        // A target with no active scope sees neither scoped anchor.
1156        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        // Two sibling `position: relative` cards, each with a static `--a`
1162        // anchor. A target whose containing block is card 1 must not accept
1163        // card 2's anchor.
1164        let ctx = AnchorContext {
1165            containing_block: HashMap::from([
1166                (1, Some(10)), // card 1 -> parent 10
1167                (2, Some(20)), // card 2 -> parent 20
1168                (10, Some(0)), // parent 10
1169                (20, Some(0)), // parent 20
1170                (0, None),     // root -> ICB
1171                (3, Some(10)), // target inside card 1
1172            ]),
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        // An out-of-flow anchor sharing the target's containing block is only
1186        // acceptable when it is laid out before the target. Target (node 5) is
1187        // ordered between the two anchors.
1188        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}