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    BoxModel, CustomLayouter, Display, InlineBox, LayoutBox, LayoutContext, OuterDisplay, Rect,
10    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    /// Returns the resolved [`Style`] that drives this element's layout.
47    ///
48    /// The layout engine reads [`Display`](ui_layout::Display) from it, exactly
49    /// as for a childless `LayoutNode`.
50    pub fn style(&self) -> &Style {
51        &self.layout_style
52    }
53
54    fn resolve_size(
55        &self,
56        containing_width: Option<f32>,
57        containing_height: Option<f32>,
58        viewport_width: f32,
59        viewport_height: f32,
60    ) -> ContentSize {
61        resolve_border_box_size(
62            self.node.as_ref(),
63            &self.layout_style,
64            containing_width,
65            containing_height,
66            viewport_width,
67            viewport_height,
68        )
69    }
70}
71
72impl CustomLayouter for CustomNodeBridge {
73    fn layout(&mut self, ctx: &LayoutContext) -> LayoutBox {
74        match self.layout_style.display {
75            Display::OutsideInner {
76                outer: OuterDisplay::Inline,
77                ..
78            } => {
79                let x = ctx.start_pos.0;
80                let y = ctx.start_pos.1;
81
82                let resolved = self.resolve_size(
83                    Some(ctx.available_inline_size),
84                    None,
85                    ctx.viewport_width,
86                    ctx.viewport_height,
87                );
88                let (use_width, use_height) = (resolved.width, resolved.height);
89
90                let rect = Rect {
91                    x,
92                    y,
93                    width: use_width,
94                    height: use_height,
95                };
96                let box_model = BoxModel {
97                    sticky_edges: None,
98                    border_box: rect,
99                    padding_box: rect,
100                    content_box: rect,
101                    children_box: rect,
102                };
103
104                LayoutBox::InlineBox(InlineBox {
105                    box_model,
106                    // Atomic inline replaced elements leave positioning to
107                    // the flow engine. A pre-populated span is interpreted as
108                    // text-flow output and only contributes `line-height`,
109                    // which collapses a tall image's containing inline-block.
110                    line_spans: Vec::new(),
111                })
112            }
113            Display::OutsideInner {
114                outer: OuterDisplay::Block,
115                ..
116            } => {
117                let resolved = self.resolve_size(
118                    ctx.containing_block_width,
119                    ctx.containing_block_height,
120                    ctx.viewport_width,
121                    ctx.viewport_height,
122                );
123
124                let rect = Rect {
125                    x: 0.0,
126                    y: 0.0,
127                    width: resolved.width,
128                    height: resolved.height,
129                };
130                let box_model = BoxModel {
131                    sticky_edges: None,
132                    border_box: rect,
133                    padding_box: rect,
134                    content_box: rect,
135                    children_box: rect,
136                };
137
138                LayoutBox::BlockBox(box_model)
139            }
140            Display::None | Display::Contents => LayoutBox::None,
141        }
142    }
143
144    fn measure(&self, ctx: &LayoutContext) -> ui_layout::MeasureResult {
145        let resolved = self.resolve_size(
146            ctx.containing_block_width,
147            ctx.containing_block_height,
148            ctx.viewport_width,
149            ctx.viewport_height,
150        );
151
152        ui_layout::MeasureResult {
153            width: resolved.width,
154            height: resolved.height,
155        }
156    }
157
158    fn write_debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "CustomNodeBridge")
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    use crate::engine::layouter::types::{TextFlowStyle, TextStyle};
168    use crate::engine::renderer_model::DrawCommand;
169    use ui_layout::InnerDisplay;
170
171    #[derive(Debug)]
172    struct TestNode {
173        width: f32,
174        height: f32,
175    }
176
177    impl CustomNode for TestNode {
178        fn draw_sized(
179            &self,
180            _cmd_buf: &mut Vec<DrawCommand>,
181            _text_style: &TextStyle,
182            _text_flow_style: &TextFlowStyle,
183            _style: &Style,
184            _size: ContentSize,
185        ) {
186        }
187
188        fn intrinsic_size(&self) -> ContentSize {
189            ContentSize {
190                width: self.width,
191                height: self.height,
192            }
193        }
194    }
195
196    fn bridge(display: Display) -> CustomNodeBridge {
197        let mut style = Style::default();
198        style.display = display;
199        CustomNodeBridge::new(
200            std::sync::Arc::new(TestNode {
201                width: 200.0,
202                height: 100.0,
203            }),
204            style,
205        )
206    }
207
208    #[test]
209    fn block_context_reports_block() {
210        assert_eq!(
211            bridge(Display::OutsideInner {
212                outer: OuterDisplay::Block,
213                inner: InnerDisplay::Flow,
214            })
215            .style()
216            .display
217            .outer(),
218            Some(OuterDisplay::Block)
219        );
220    }
221
222    #[test]
223    fn block_layout_returns_block_box_at_origin() {
224        let mut b = bridge(Display::OutsideInner {
225            outer: OuterDisplay::Block,
226            inner: InnerDisplay::Flow,
227        });
228        match b.layout(&LayoutContext::default()) {
229            LayoutBox::BlockBox(bm) => {
230                assert_eq!(bm.border_box.x, 0.0);
231                assert_eq!(bm.border_box.y, 0.0);
232                assert_eq!(bm.border_box.width, 200.0);
233                assert_eq!(bm.border_box.height, 100.0);
234            }
235            other => panic!("expected BlockBox, got {:?}", other),
236        }
237    }
238
239    #[test]
240    fn block_measure_returns_intrinsic_size() {
241        let b = bridge(Display::OutsideInner {
242            outer: OuterDisplay::Block,
243            inner: InnerDisplay::Flow,
244        });
245        let m = b.measure(&LayoutContext::default());
246        assert_eq!(m.width, 200.0);
247        assert_eq!(m.height, 100.0);
248    }
249
250    #[test]
251    fn inline_context_reports_inline() {
252        assert_eq!(
253            bridge(Display::OutsideInner {
254                outer: OuterDisplay::Inline,
255                inner: InnerDisplay::Flow,
256            })
257            .style()
258            .display
259            .outer(),
260            Some(OuterDisplay::Inline)
261        );
262    }
263
264    #[test]
265    fn inline_layout_returns_an_atomic_unpositioned_box() {
266        let mut b = bridge(Display::OutsideInner {
267            outer: OuterDisplay::Inline,
268            inner: InnerDisplay::Flow,
269        });
270        let ctx = LayoutContext {
271            start_pos: (10.0, 20.0),
272            available_inline_size: 300.0,
273            viewport_width: 800.0,
274            viewport_height: 600.0,
275            ..LayoutContext::default()
276        };
277        match b.layout(&ctx) {
278            LayoutBox::InlineBox(inline) => {
279                assert_eq!(inline.box_model.border_box.x, 10.0);
280                assert_eq!(inline.box_model.border_box.y, 20.0);
281                assert_eq!(inline.box_model.border_box.width, 200.0);
282                assert_eq!(inline.box_model.border_box.height, 100.0);
283                assert!(inline.line_spans.is_empty());
284            }
285            other => panic!("expected InlineBox, got {:?}", other),
286        }
287    }
288
289    #[test]
290    fn inline_measure_returns_intrinsic_size() {
291        let b = bridge(Display::OutsideInner {
292            outer: OuterDisplay::Inline,
293            inner: InnerDisplay::Flow,
294        });
295        let m = b.measure(&LayoutContext::default());
296        assert_eq!(m.width, 200.0);
297        assert_eq!(m.height, 100.0);
298    }
299
300    #[test]
301    fn none_context_skips_element() {
302        let mut b = bridge(Display::None);
303        assert_eq!(b.style().display, Display::None);
304        assert!(matches!(
305            b.layout(&LayoutContext::default()),
306            LayoutBox::None
307        ));
308    }
309
310    #[test]
311    fn inline_size_resolves_against_available_inline_size() {
312        use ui_layout::{Length, LengthOrAuto};
313
314        let mut style = Style {
315            size: ui_layout::SizeStyle {
316                width: LengthOrAuto::Length(Length::Percent(50.0)),
317                ..Default::default()
318            },
319            ..Default::default()
320        };
321        style.display = Display::OutsideInner {
322            outer: OuterDisplay::Inline,
323            inner: InnerDisplay::Flow,
324        };
325        let b = CustomNodeBridge::new(
326            std::sync::Arc::new(TestNode {
327                width: 200.0,
328                height: 100.0,
329            }),
330            style,
331        );
332        let mut b = b;
333        let ctx = LayoutContext {
334            start_pos: (0.0, 0.0),
335            available_inline_size: 150.0,
336            viewport_width: 800.0,
337            viewport_height: 600.0,
338            ..LayoutContext::default()
339        };
340        match b.layout(&ctx) {
341            LayoutBox::InlineBox(inline) => {
342                assert_eq!(inline.box_model.border_box.width, 75.0);
343            }
344            other => panic!("expected InlineBox, got {:?}", other),
345        }
346    }
347}