diff --git a/lib/ruin_app/example/03_fine_grained_list.rs b/lib/ruin_app/example/03_fine_grained_list.rs index 1cfa8e9..6b18733 100644 --- a/lib/ruin_app/example/03_fine_grained_list.rs +++ b/lib/ruin_app/example/03_fine_grained_list.rs @@ -4,8 +4,7 @@ use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, fmt}; fn install_tracing() { - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("warn")); + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")); let fmt_layer = fmt::layer() .with_target(true) .with_thread_ids(true) diff --git a/lib/ui/src/layout.rs b/lib/ui/src/layout.rs index 45ccba2..02e8df9 100644 --- a/lib/ui/src/layout.rs +++ b/lib/ui/src/layout.rs @@ -263,6 +263,7 @@ fn layout_element( clip_rect: Option, ) -> LayoutNode { perf_stats.nodes += 1; + let rect = snap_rect(rect); // Viewport culling: skip fully off-screen elements inside a scroll box. if let Some(clip) = clip_rect @@ -813,11 +814,16 @@ fn layout_container_children( let mut fixed_total = 0.0; let mut flex_total = 0.0; for child in &element.children { + let child_insets = content_insets(&child.style); + let main_inset = main_axis_padding(child_insets, element.style.direction); + let cross_inset = cross_axis_padding(child_insets, element.style.direction); + // style.width/height are content-box; add insets to get the outer (allocated) size. let cross = child_cross_size(child, element.style.direction) + .map(|c| (c + cross_inset).max(0.0)) .unwrap_or(available_cross) .clamp(0.0, available_cross); - let explicit_main = - child_main_size(child, element.style.direction).map(|main| main.max(0.0)); + let explicit_main = child_main_size(child, element.style.direction) + .map(|main| (main + main_inset).max(0.0)); let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0; let measured_main = explicit_main.unwrap_or_else(|| { if is_flex { @@ -902,13 +908,22 @@ fn intrinsic_main_size( layout_cache: &mut LayoutCache, ) -> f32 { if let Some(text) = child.text_node() { + // cross_size and available_main are outer sizes; subtract insets to get content dimensions + // for text measurement so wrapping is constrained to the actual content area. + let child_insets = content_insets(&child.style); let constraints = match direction { - FlexDirection::Row => (Some(available_main.max(0.0)), Some(cross_size.max(0.0))), - FlexDirection::Column => (Some(cross_size.max(0.0)), None), + FlexDirection::Row => ( + Some((available_main - main_axis_padding(child_insets, direction)).max(0.0)), + Some((cross_size - cross_axis_padding(child_insets, direction)).max(0.0)), + ), + FlexDirection::Column => ( + Some((cross_size - cross_axis_padding(child_insets, direction)).max(0.0)), + None, + ), }; let content = text_system.measure_spans(&text.spans, &text.style, constraints.0, constraints.1); - let padding = main_axis_padding(content_insets(&child.style), direction); + let padding = main_axis_padding(child_insets, direction); return main_axis_size(content, direction) + padding; } @@ -945,19 +960,24 @@ fn intrinsic_container_content_size( let mut height = gap_total; for child in &element.children { let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none(); - let child_size = intrinsic_size( - child, - UiSize::new( - child.style.width.unwrap_or(content_size.width), - child.style.height.unwrap_or(content_size.height), - ), - text_system, - perf_stats, - layout_cache, - ); - width = width.max(child.style.width.unwrap_or(child_size.width)); + let child_insets = content_insets(&child.style); + // Offer outer size: content-box style.w/h + insets, or parent's content width. + let offered_w = child + .style + .width + .map(|w| w + horizontal_insets(child_insets)) + .unwrap_or(content_size.width); + let offered_h = child + .style + .height + .map(|h| h + vertical_insets(child_insets)) + .unwrap_or(content_size.height); + let child_size = + intrinsic_size(child, UiSize::new(offered_w, offered_h), text_system, perf_stats, layout_cache); + // child_size is always outer; accumulate directly. + width = width.max(child_size.width); if !skip_main { - height += child.style.height.unwrap_or(child_size.height); + height += child_size.height; } } UiSize::new(width, height) @@ -975,17 +995,21 @@ fn intrinsic_container_content_size( child_main_sizes.push(None); continue; } - let child_size = intrinsic_size( - child, - UiSize::new( - child.style.width.unwrap_or(content_size.width), - child.style.height.unwrap_or(content_size.height), - ), - text_system, - perf_stats, - layout_cache, - ); - let child_main = child.style.width.unwrap_or(child_size.width); + let child_insets = content_insets(&child.style); + let offered_w = child + .style + .width + .map(|w| w + horizontal_insets(child_insets)) + .unwrap_or(content_size.width); + let offered_h = child + .style + .height + .map(|h| h + vertical_insets(child_insets)) + .unwrap_or(content_size.height); + let child_size = + intrinsic_size(child, UiSize::new(offered_w, offered_h), text_system, perf_stats, layout_cache); + // child_size.width is outer; use directly as the fixed main contribution. + let child_main = child_size.width; fixed_main += child_main; child_main_sizes.push(Some(child_main)); } @@ -999,12 +1023,15 @@ fn intrinsic_container_content_size( remaining_main * (child_flex_weight(child) / flex_total) } }); + let child_insets = content_insets(&child.style); + let offered_h = child + .style + .height + .map(|h| h + vertical_insets(child_insets)) + .unwrap_or(content_size.height); let child_size = intrinsic_size( child, - UiSize::new( - child_main, - child.style.height.unwrap_or(content_size.height), - ), + UiSize::new(child_main, offered_h), text_system, perf_stats, layout_cache, @@ -1012,7 +1039,7 @@ fn intrinsic_container_content_size( if !skip_main { width += child_main; } - height = height.max(child.style.height.unwrap_or(child_size.height)); + height = height.max(child_size.height); } UiSize::new(width, height) } @@ -1060,22 +1087,30 @@ fn intrinsic_size_inner( perf_stats: &mut LayoutPerfStats, layout_cache: &mut LayoutCache, ) -> UiSize { + // style.width/height are content-box (content dimensions). + // available_size is the outer size the parent is offering. + // All returned sizes are outer (content + insets). + if let Some(text) = element.text_node() { + // Measure at content dimensions: use explicit if set, else available minus insets. + let content_w = element + .style + .width + .unwrap_or_else(|| (available_size.width - horizontal_insets(insets)).max(0.0)); + let content_h = element + .style + .height + .unwrap_or_else(|| (available_size.height - vertical_insets(insets)).max(0.0)); let measured = text_system.measure_spans( &text.spans, &text.style, - Some(available_size.width.max(0.0)), - Some(available_size.height.max(0.0)), + Some(content_w.max(0.0)), + Some(content_h.max(0.0)), ); + // Return outer: explicit (content) + insets, or measured + insets for auto. return UiSize::new( - element - .style - .width - .unwrap_or(measured.width + horizontal_insets(insets)), - element - .style - .height - .unwrap_or(measured.height + vertical_insets(insets)), + element.style.width.unwrap_or(measured.width) + horizontal_insets(insets), + element.style.height.unwrap_or(measured.height) + vertical_insets(insets), ); } @@ -1092,10 +1127,13 @@ fn intrinsic_size_inner( let scroll_gutter = element .scroll_box_node() .map_or(0.0, |scroll_box| scroll_box.scrollbar.gutter_width.max(0.0)); - let content_width = - explicit_width.unwrap_or(available_size.width).max(0.0) - horizontal_insets(insets); - let content_height = - explicit_height.unwrap_or(available_size.height).max(0.0) - vertical_insets(insets); + // Content dimensions: explicit = already content; auto = available_outer - insets. + let content_width = explicit_width + .map(|w| w.max(0.0)) + .unwrap_or_else(|| (available_size.width - horizontal_insets(insets)).max(0.0)); + let content_height = explicit_height + .map(|h| h.max(0.0)) + .unwrap_or_else(|| (available_size.height - vertical_insets(insets)).max(0.0)); let content_size = UiSize::new( (content_width - scroll_gutter).max(0.0), content_height.max(0.0), @@ -1103,8 +1141,12 @@ fn intrinsic_size_inner( if element.children.is_empty() { return UiSize::new( - explicit_width.unwrap_or(horizontal_insets(insets) + scroll_gutter), - explicit_height.unwrap_or(vertical_insets(insets)), + explicit_width + .map(|w| w + horizontal_insets(insets) + scroll_gutter) + .unwrap_or(horizontal_insets(insets) + scroll_gutter), + explicit_height + .map(|h| h + vertical_insets(insets)) + .unwrap_or(vertical_insets(insets)), ); } @@ -1118,8 +1160,11 @@ fn intrinsic_size_inner( UiSize::new( explicit_width + .map(|w| w + horizontal_insets(insets) + scroll_gutter) .unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter), - explicit_height.unwrap_or(intrinsic_content.height + vertical_insets(insets)), + explicit_height + .map(|h| h + vertical_insets(insets)) + .unwrap_or(intrinsic_content.height + vertical_insets(insets)), ) } @@ -1567,6 +1612,26 @@ fn main_axis_padding(edges: Edges, direction: FlexDirection) -> f32 { } } +fn cross_axis_padding(edges: Edges, direction: FlexDirection) -> f32 { + match direction { + FlexDirection::Row => edges.top + edges.bottom, + FlexDirection::Column => edges.left + edges.right, + } +} + +/// Snap a rect to the integer pixel grid. +/// +/// Origin and far edge are each rounded independently so that adjacent +/// elements (where one's far edge equals the other's near edge) always +/// butt flush against each other with no gap or overlap. +fn snap_rect(r: Rect) -> Rect { + let x0 = r.origin.x.round(); + let y0 = r.origin.y.round(); + let x1 = (r.origin.x + r.size.width).round(); + let y1 = (r.origin.y + r.size.height).round(); + Rect::new(x0, y0, (x1 - x0).max(0.0), (y1 - y0).max(0.0)) +} + fn main_axis_size(size: UiSize, direction: FlexDirection) -> f32 { match direction { FlexDirection::Row => size.width, @@ -2258,7 +2323,6 @@ mod tests { #[test] fn scroll_culling_skips_off_screen_children() { - use crate::tree::ScrollbarStyle; let mut text_system = TextSystem::new(); let mut layout_cache = LayoutCache::new();