Pixel-grid snapping and corrected border box sizing
This commit is contained in:
@@ -4,8 +4,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
|||||||
use tracing_subscriber::{EnvFilter, fmt};
|
use tracing_subscriber::{EnvFilter, fmt};
|
||||||
|
|
||||||
fn install_tracing() {
|
fn install_tracing() {
|
||||||
let filter = EnvFilter::try_from_default_env()
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
|
||||||
.unwrap_or_else(|_| EnvFilter::new("warn"));
|
|
||||||
let fmt_layer = fmt::layer()
|
let fmt_layer = fmt::layer()
|
||||||
.with_target(true)
|
.with_target(true)
|
||||||
.with_thread_ids(true)
|
.with_thread_ids(true)
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ fn layout_element(
|
|||||||
clip_rect: Option<Rect>,
|
clip_rect: Option<Rect>,
|
||||||
) -> LayoutNode {
|
) -> LayoutNode {
|
||||||
perf_stats.nodes += 1;
|
perf_stats.nodes += 1;
|
||||||
|
let rect = snap_rect(rect);
|
||||||
|
|
||||||
// Viewport culling: skip fully off-screen elements inside a scroll box.
|
// Viewport culling: skip fully off-screen elements inside a scroll box.
|
||||||
if let Some(clip) = clip_rect
|
if let Some(clip) = clip_rect
|
||||||
@@ -813,11 +814,16 @@ fn layout_container_children(
|
|||||||
let mut fixed_total = 0.0;
|
let mut fixed_total = 0.0;
|
||||||
let mut flex_total = 0.0;
|
let mut flex_total = 0.0;
|
||||||
for child in &element.children {
|
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)
|
let cross = child_cross_size(child, element.style.direction)
|
||||||
|
.map(|c| (c + cross_inset).max(0.0))
|
||||||
.unwrap_or(available_cross)
|
.unwrap_or(available_cross)
|
||||||
.clamp(0.0, available_cross);
|
.clamp(0.0, available_cross);
|
||||||
let explicit_main =
|
let explicit_main = child_main_size(child, element.style.direction)
|
||||||
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
.map(|main| (main + main_inset).max(0.0));
|
||||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||||
if is_flex {
|
if is_flex {
|
||||||
@@ -902,13 +908,22 @@ fn intrinsic_main_size(
|
|||||||
layout_cache: &mut LayoutCache,
|
layout_cache: &mut LayoutCache,
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
if let Some(text) = child.text_node() {
|
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 {
|
let constraints = match direction {
|
||||||
FlexDirection::Row => (Some(available_main.max(0.0)), Some(cross_size.max(0.0))),
|
FlexDirection::Row => (
|
||||||
FlexDirection::Column => (Some(cross_size.max(0.0)), None),
|
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 =
|
let content =
|
||||||
text_system.measure_spans(&text.spans, &text.style, constraints.0, constraints.1);
|
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;
|
return main_axis_size(content, direction) + padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,19 +960,24 @@ fn intrinsic_container_content_size(
|
|||||||
let mut height = gap_total;
|
let mut height = gap_total;
|
||||||
for child in &element.children {
|
for child in &element.children {
|
||||||
let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none();
|
let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none();
|
||||||
let child_size = intrinsic_size(
|
let child_insets = content_insets(&child.style);
|
||||||
child,
|
// Offer outer size: content-box style.w/h + insets, or parent's content width.
|
||||||
UiSize::new(
|
let offered_w = child
|
||||||
child.style.width.unwrap_or(content_size.width),
|
.style
|
||||||
child.style.height.unwrap_or(content_size.height),
|
.width
|
||||||
),
|
.map(|w| w + horizontal_insets(child_insets))
|
||||||
text_system,
|
.unwrap_or(content_size.width);
|
||||||
perf_stats,
|
let offered_h = child
|
||||||
layout_cache,
|
.style
|
||||||
);
|
.height
|
||||||
width = width.max(child.style.width.unwrap_or(child_size.width));
|
.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 {
|
if !skip_main {
|
||||||
height += child.style.height.unwrap_or(child_size.height);
|
height += child_size.height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiSize::new(width, height)
|
UiSize::new(width, height)
|
||||||
@@ -975,17 +995,21 @@ fn intrinsic_container_content_size(
|
|||||||
child_main_sizes.push(None);
|
child_main_sizes.push(None);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let child_size = intrinsic_size(
|
let child_insets = content_insets(&child.style);
|
||||||
child,
|
let offered_w = child
|
||||||
UiSize::new(
|
.style
|
||||||
child.style.width.unwrap_or(content_size.width),
|
.width
|
||||||
child.style.height.unwrap_or(content_size.height),
|
.map(|w| w + horizontal_insets(child_insets))
|
||||||
),
|
.unwrap_or(content_size.width);
|
||||||
text_system,
|
let offered_h = child
|
||||||
perf_stats,
|
.style
|
||||||
layout_cache,
|
.height
|
||||||
);
|
.map(|h| h + vertical_insets(child_insets))
|
||||||
let child_main = child.style.width.unwrap_or(child_size.width);
|
.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;
|
fixed_main += child_main;
|
||||||
child_main_sizes.push(Some(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)
|
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(
|
let child_size = intrinsic_size(
|
||||||
child,
|
child,
|
||||||
UiSize::new(
|
UiSize::new(child_main, offered_h),
|
||||||
child_main,
|
|
||||||
child.style.height.unwrap_or(content_size.height),
|
|
||||||
),
|
|
||||||
text_system,
|
text_system,
|
||||||
perf_stats,
|
perf_stats,
|
||||||
layout_cache,
|
layout_cache,
|
||||||
@@ -1012,7 +1039,7 @@ fn intrinsic_container_content_size(
|
|||||||
if !skip_main {
|
if !skip_main {
|
||||||
width += child_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)
|
UiSize::new(width, height)
|
||||||
}
|
}
|
||||||
@@ -1060,22 +1087,30 @@ fn intrinsic_size_inner(
|
|||||||
perf_stats: &mut LayoutPerfStats,
|
perf_stats: &mut LayoutPerfStats,
|
||||||
layout_cache: &mut LayoutCache,
|
layout_cache: &mut LayoutCache,
|
||||||
) -> UiSize {
|
) -> 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() {
|
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(
|
let measured = text_system.measure_spans(
|
||||||
&text.spans,
|
&text.spans,
|
||||||
&text.style,
|
&text.style,
|
||||||
Some(available_size.width.max(0.0)),
|
Some(content_w.max(0.0)),
|
||||||
Some(available_size.height.max(0.0)),
|
Some(content_h.max(0.0)),
|
||||||
);
|
);
|
||||||
|
// Return outer: explicit (content) + insets, or measured + insets for auto.
|
||||||
return UiSize::new(
|
return UiSize::new(
|
||||||
element
|
element.style.width.unwrap_or(measured.width) + horizontal_insets(insets),
|
||||||
.style
|
element.style.height.unwrap_or(measured.height) + vertical_insets(insets),
|
||||||
.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
|
let scroll_gutter = element
|
||||||
.scroll_box_node()
|
.scroll_box_node()
|
||||||
.map_or(0.0, |scroll_box| scroll_box.scrollbar.gutter_width.max(0.0));
|
.map_or(0.0, |scroll_box| scroll_box.scrollbar.gutter_width.max(0.0));
|
||||||
let content_width =
|
// Content dimensions: explicit = already content; auto = available_outer - insets.
|
||||||
explicit_width.unwrap_or(available_size.width).max(0.0) - horizontal_insets(insets);
|
let content_width = explicit_width
|
||||||
let content_height =
|
.map(|w| w.max(0.0))
|
||||||
explicit_height.unwrap_or(available_size.height).max(0.0) - vertical_insets(insets);
|
.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(
|
let content_size = UiSize::new(
|
||||||
(content_width - scroll_gutter).max(0.0),
|
(content_width - scroll_gutter).max(0.0),
|
||||||
content_height.max(0.0),
|
content_height.max(0.0),
|
||||||
@@ -1103,8 +1141,12 @@ fn intrinsic_size_inner(
|
|||||||
|
|
||||||
if element.children.is_empty() {
|
if element.children.is_empty() {
|
||||||
return UiSize::new(
|
return UiSize::new(
|
||||||
explicit_width.unwrap_or(horizontal_insets(insets) + scroll_gutter),
|
explicit_width
|
||||||
explicit_height.unwrap_or(vertical_insets(insets)),
|
.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(
|
UiSize::new(
|
||||||
explicit_width
|
explicit_width
|
||||||
|
.map(|w| w + horizontal_insets(insets) + scroll_gutter)
|
||||||
.unwrap_or(intrinsic_content.width + 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 {
|
fn main_axis_size(size: UiSize, direction: FlexDirection) -> f32 {
|
||||||
match direction {
|
match direction {
|
||||||
FlexDirection::Row => size.width,
|
FlexDirection::Row => size.width,
|
||||||
@@ -2258,7 +2323,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scroll_culling_skips_off_screen_children() {
|
fn scroll_culling_skips_off_screen_children() {
|
||||||
use crate::tree::ScrollbarStyle;
|
|
||||||
let mut text_system = TextSystem::new();
|
let mut text_system = TextSystem::new();
|
||||||
let mut layout_cache = LayoutCache::new();
|
let mut layout_cache = LayoutCache::new();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user