Skip to main content

orinium_browser/engine/ui/components/
custom_node_bridge.rs

1//! Unified layout bridge for custom nodes.
2//!
3//! Implements the unified [`CustomLayouter`] trait for both block and inline
4//! formatting contexts. The resolved [`Display`] is taken from the owned
5//! [`Style`] (`layout_style`) at layout time and drives the shape of the
6//! returned [`LayoutBox`].
7
8use ui_layout::{
9    AutoSizeBehavior, BoxModel, CustomLayouter, Display, InlineBox, LayoutBox, LayoutContext,
10    OuterDisplay, Rect, SizeStyle, Style,
11};
12
13use crate::engine::ui::custom_node::{ContentSize, CustomNode};
14
15use super::inline_cache::resolve_border_box_size;
16
17/// A [`CustomLayouter`] implementation for custom nodes.
18///
19/// Holds the resolved [`Style`] so the layout engine can account for borders
20/// and padding when sizing the element. The actual content rendering is
21/// delegated to a separate [`CustomNode`](crate::engine::ui::CustomNode).
22///
23/// The element participates in the formatting context reported by the owned
24/// style's [`OuterDisplay`]:
25///
26/// - [`OuterDisplay::Inline`] → `layout` returns an atomic
27///   [`LayoutBox::InlineBox`]. The flow engine creates its line span so the
28///   element's full border-box height contributes to the line box.
29/// - [`OuterDisplay::Block`] → `layout` returns a [`LayoutBox::BlockBox`]
30///   positioned at the origin; the engine translates it to its final position.
31/// - [`Display::None`] / [`Display::Contents`] → the element produces nothing.
32///
33/// [`measure`](Self::measure) is implemented for every context so flex sizing
34/// and auto-height work regardless of the display value.
35#[derive(Debug)]
36pub struct CustomNodeBridge {
37    node: std::sync::Arc<dyn CustomNode>,
38    layout_style: Style,
39}
40
41impl CustomNodeBridge {
42    pub fn new(node: std::sync::Arc<dyn CustomNode>, layout_style: Style) -> Self {
43        Self { node, layout_style }
44    }
45
46    /// Builds the intrinsic-content child of a styled custom element.
47    ///
48    /// The containing `LayoutNode` owns the element's CSS sizing and spacing;
49    /// applying them again to this bridge would duplicate padding and borders.
50    pub fn new_content(node: std::sync::Arc<dyn CustomNode>, element_style: &Style) -> Self {
51        let mut content_style = element_style.clone();
52        content_style.size = SizeStyle {
53            auto_behavior: AutoSizeBehavior::ShrinkToFit,
54            ..Default::default()
55        };
56        content_style.box_sizing = Default::default();
57        content_style.spacing = Default::default();
58        Self::new(node, content_style)
59    }
60
61    /// Returns the resolved [`Style`] that drives this element's layout.
62    ///
63    /// The layout engine reads [`Display`](ui_layout::Display) from it, exactly
64    /// as for a childless `LayoutNode`.
65    pub fn style(&self) -> &Style {
66        &self.layout_style
67    }
68
69    fn resolve_size(
70        &self,
71        containing_width: Option<f32>,
72        containing_height: Option<f32>,
73        viewport_width: f32,
74        viewport_height: f32,
75    ) -> ContentSize {
76        resolve_border_box_size(
77            self.node.as_ref(),
78            &self.layout_style,
79            containing_width,
80            containing_height,
81            viewport_width,
82            viewport_height,
83        )
84    }
85}
86
87fn resolve_len(len: &ui_layout::Length, container: Option<f32>, viewport: f32) -> f32 {
88    len.resolve_with(container, viewport, 0.0).unwrap_or(0.0)
89}
90
91fn spacing_px(
92    style: &Style,
93    cw: Option<f32>,
94    ch: Option<f32>,
95    vw: f32,
96    vh: f32,
97) -> (f32, f32, f32, f32, f32, f32, f32, f32) {
98    let cbw = cw.unwrap_or(vw);
99    let cbh = ch.unwrap_or(vh);
100    (
101        resolve_len(&style.spacing.border_left, Some(cbw), vw),
102        resolve_len(&style.spacing.border_top, Some(cbh), vh),
103        resolve_len(&style.spacing.border_right, Some(cbw), vw),
104        resolve_len(&style.spacing.border_bottom, Some(cbh), vh),
105        resolve_len(&style.spacing.padding_left, Some(cbw), vw),
106        resolve_len(&style.spacing.padding_top, Some(cbh), vh),
107        resolve_len(&style.spacing.padding_right, Some(cbw), vw),
108        resolve_len(&style.spacing.padding_bottom, Some(cbh), vh),
109    )
110}
111
112/// Geometry parameters for building a box model.
113struct BoxGeometry {
114    x: f32,
115    y: f32,
116    border_w: f32,
117    border_h: f32,
118    cw: Option<f32>,
119    ch: Option<f32>,
120    vw: f32,
121    vh: f32,
122}
123
124fn make_box_model(geometry: &BoxGeometry, style: &Style) -> BoxModel {
125    let (bl, bt, br, bb, pl, pt, pr, pb) =
126        spacing_px(style, geometry.cw, geometry.ch, geometry.vw, geometry.vh);
127    let border_box = Rect {
128        x: geometry.x,
129        y: geometry.y,
130        width: geometry.border_w.max(0.0),
131        height: geometry.border_h.max(0.0),
132    };
133    let padding_box = Rect {
134        x: geometry.x + bl,
135        y: geometry.y + bt,
136        width: (geometry.border_w - bl - br).max(0.0),
137        height: (geometry.border_h - bt - bb).max(0.0),
138    };
139    let content_box = Rect {
140        x: geometry.x + bl + pl,
141        y: geometry.y + bt + pt,
142        width: (geometry.border_w - bl - br - pl - pr).max(0.0),
143        height: (geometry.border_h - bt - bb - pt - pb).max(0.0),
144    };
145    BoxModel {
146        sticky_edges: None,
147        border_box,
148        padding_box,
149        content_box,
150        children_box: content_box,
151    }
152}
153
154impl CustomLayouter for CustomNodeBridge {
155    fn layout(&mut self, ctx: &LayoutContext) -> LayoutBox {
156        match self.layout_style.display {
157            Display::OutsideInner {
158                outer: OuterDisplay::Inline,
159                ..
160            } => {
161                let x = ctx.start_pos.0;
162                let y = ctx.start_pos.1;
163
164                let resolved = self.resolve_size(
165                    Some(ctx.available_inline_size),
166                    None,
167                    ctx.viewport_width,
168                    ctx.viewport_height,
169                );
170                let box_model = make_box_model(
171                    &BoxGeometry {
172                        x,
173                        y,
174                        border_w: resolved.width,
175                        border_h: resolved.height,
176                        cw: Some(ctx.available_inline_size),
177                        ch: None,
178                        vw: ctx.viewport_width,
179                        vh: ctx.viewport_height,
180                    },
181                    &self.layout_style,
182                );
183
184                LayoutBox::InlineBox(InlineBox {
185                    box_model,
186                    // Atomic inline replaced elements leave positioning to
187                    // the flow engine. A pre-populated span is interpreted as
188                    // text-flow output and only contributes `line-height`,
189                    // which collapses a tall image's containing inline-block.
190                    line_spans: Vec::new(),
191                })
192            }
193            Display::OutsideInner {
194                outer: OuterDisplay::Block,
195                ..
196            } => {
197                let resolved = self.resolve_size(
198                    ctx.containing_block_width,
199                    ctx.containing_block_height,
200                    ctx.viewport_width,
201                    ctx.viewport_height,
202                );
203                let box_model = make_box_model(
204                    &BoxGeometry {
205                        x: 0.0,
206                        y: 0.0,
207                        border_w: resolved.width,
208                        border_h: resolved.height,
209                        cw: ctx.containing_block_width,
210                        ch: ctx.containing_block_height,
211                        vw: ctx.viewport_width,
212                        vh: ctx.viewport_height,
213                    },
214                    &self.layout_style,
215                );
216
217                LayoutBox::BlockBox(box_model)
218            }
219            Display::None | Display::Contents => LayoutBox::None,
220        }
221    }
222
223    fn measure(&self, ctx: &LayoutContext) -> ui_layout::MeasureResult {
224        let resolved = self.resolve_size(
225            ctx.containing_block_width,
226            ctx.containing_block_height,
227            ctx.viewport_width,
228            ctx.viewport_height,
229        );
230
231        ui_layout::MeasureResult {
232            width: resolved.width,
233            height: resolved.height,
234        }
235    }
236
237    fn write_debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        write!(f, "CustomNodeBridge")
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    use crate::engine::layouter::types::{TextFlowStyle, TextStyle};
247    use crate::engine::renderer_model::DrawCommand;
248    use ui_layout::InnerDisplay;
249
250    #[derive(Debug)]
251    struct TestNode {
252        width: f32,
253        height: f32,
254    }
255
256    impl CustomNode for TestNode {
257        fn draw_sized(
258            &self,
259            _cmd_buf: &mut Vec<DrawCommand>,
260            _text_style: &TextStyle,
261            _text_flow_style: &TextFlowStyle,
262            _style: &Style,
263            _size: ContentSize,
264        ) {
265        }
266
267        fn intrinsic_size(&self) -> ContentSize {
268            ContentSize {
269                width: self.width,
270                height: self.height,
271            }
272        }
273    }
274
275    fn bridge(display: Display) -> CustomNodeBridge {
276        let style = Style {
277            display,
278            ..Style::default()
279        };
280        CustomNodeBridge::new(
281            std::sync::Arc::new(TestNode {
282                width: 200.0,
283                height: 100.0,
284            }),
285            style,
286        )
287    }
288
289    #[test]
290    fn block_context_reports_block() {
291        assert_eq!(
292            bridge(Display::OutsideInner {
293                outer: OuterDisplay::Block,
294                inner: InnerDisplay::Flow,
295            })
296            .style()
297            .display
298            .outer(),
299            Some(OuterDisplay::Block)
300        );
301    }
302
303    #[test]
304    fn block_layout_returns_block_box_at_origin() {
305        let mut b = bridge(Display::OutsideInner {
306            outer: OuterDisplay::Block,
307            inner: InnerDisplay::Flow,
308        });
309        match b.layout(&LayoutContext::default()) {
310            LayoutBox::BlockBox(bm) => {
311                assert_eq!(bm.border_box.x, 0.0);
312                assert_eq!(bm.border_box.y, 0.0);
313                assert_eq!(bm.border_box.width, 200.0);
314                assert_eq!(bm.border_box.height, 100.0);
315            }
316            other => panic!("expected BlockBox, got {:?}", other),
317        }
318    }
319
320    #[test]
321    fn block_measure_returns_intrinsic_size() {
322        let b = bridge(Display::OutsideInner {
323            outer: OuterDisplay::Block,
324            inner: InnerDisplay::Flow,
325        });
326        let m = b.measure(&LayoutContext::default());
327        assert_eq!(m.width, 200.0);
328        assert_eq!(m.height, 100.0);
329    }
330
331    #[test]
332    fn inline_context_reports_inline() {
333        assert_eq!(
334            bridge(Display::OutsideInner {
335                outer: OuterDisplay::Inline,
336                inner: InnerDisplay::Flow,
337            })
338            .style()
339            .display
340            .outer(),
341            Some(OuterDisplay::Inline)
342        );
343    }
344
345    #[test]
346    fn inline_layout_returns_an_atomic_unpositioned_box() {
347        let mut b = bridge(Display::OutsideInner {
348            outer: OuterDisplay::Inline,
349            inner: InnerDisplay::Flow,
350        });
351        let ctx = LayoutContext {
352            start_pos: (10.0, 20.0),
353            available_inline_size: 300.0,
354            viewport_width: 800.0,
355            viewport_height: 600.0,
356            ..LayoutContext::default()
357        };
358        match b.layout(&ctx) {
359            LayoutBox::InlineBox(inline) => {
360                assert_eq!(inline.box_model.border_box.x, 10.0);
361                assert_eq!(inline.box_model.border_box.y, 20.0);
362                assert_eq!(inline.box_model.border_box.width, 200.0);
363                assert_eq!(inline.box_model.border_box.height, 100.0);
364                assert!(inline.line_spans.is_empty());
365            }
366            other => panic!("expected InlineBox, got {:?}", other),
367        }
368    }
369
370    #[test]
371    fn content_bridge_does_not_apply_element_spacing_or_size_twice() {
372        use ui_layout::{Length, LengthOrAuto};
373
374        let style = Style {
375            display: Display::OutsideInner {
376                outer: OuterDisplay::Inline,
377                inner: InnerDisplay::FlowRoot,
378            },
379            size: SizeStyle {
380                width: LengthOrAuto::Length(Length::Px(300.0)),
381                height: LengthOrAuto::Length(Length::Px(200.0)),
382                ..Default::default()
383            },
384            spacing: ui_layout::Spacing {
385                padding_left: Length::Px(13.0),
386                padding_right: Length::Px(13.0),
387                border_left: Length::Px(1.0),
388                border_right: Length::Px(1.0),
389                ..Default::default()
390            },
391            ..Default::default()
392        };
393        let mut bridge = CustomNodeBridge::new_content(
394            std::sync::Arc::new(TestNode {
395                width: 40.0,
396                height: 20.0,
397            }),
398            &style,
399        );
400        let ctx = LayoutContext {
401            available_inline_size: 500.0,
402            viewport_width: 800.0,
403            viewport_height: 600.0,
404            ..LayoutContext::default()
405        };
406
407        match bridge.layout(&ctx) {
408            LayoutBox::InlineBox(inline) => {
409                assert_eq!(inline.box_model.border_box.size(), (40.0, 20.0));
410                assert_eq!(inline.box_model.content_box.size(), (40.0, 20.0));
411            }
412            other => panic!("expected InlineBox, got {:?}", other),
413        }
414    }
415
416    #[test]
417    fn inline_measure_returns_intrinsic_size() {
418        let b = bridge(Display::OutsideInner {
419            outer: OuterDisplay::Inline,
420            inner: InnerDisplay::Flow,
421        });
422        let m = b.measure(&LayoutContext::default());
423        assert_eq!(m.width, 200.0);
424        assert_eq!(m.height, 100.0);
425    }
426
427    #[test]
428    fn none_context_skips_element() {
429        let mut b = bridge(Display::None);
430        assert_eq!(b.style().display, Display::None);
431        assert!(matches!(
432            b.layout(&LayoutContext::default()),
433            LayoutBox::None
434        ));
435    }
436
437    #[test]
438    fn inline_size_resolves_against_available_inline_size() {
439        use ui_layout::{Length, LengthOrAuto};
440
441        let mut style = Style {
442            size: ui_layout::SizeStyle {
443                width: LengthOrAuto::Length(Length::Percent(50.0)),
444                ..Default::default()
445            },
446            ..Default::default()
447        };
448        style.display = Display::OutsideInner {
449            outer: OuterDisplay::Inline,
450            inner: InnerDisplay::Flow,
451        };
452        let b = CustomNodeBridge::new(
453            std::sync::Arc::new(TestNode {
454                width: 200.0,
455                height: 100.0,
456            }),
457            style,
458        );
459        let mut b = b;
460        let ctx = LayoutContext {
461            start_pos: (0.0, 0.0),
462            available_inline_size: 150.0,
463            viewport_width: 800.0,
464            viewport_height: 600.0,
465            ..LayoutContext::default()
466        };
467        match b.layout(&ctx) {
468            LayoutBox::InlineBox(inline) => {
469                assert_eq!(inline.box_model.border_box.width, 75.0);
470            }
471            other => panic!("expected InlineBox, got {:?}", other),
472        }
473    }
474}