1use 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#[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 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 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
112struct 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 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}