Merge origin/main into macos
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -54,7 +54,7 @@ pub struct RoutedPointerEvent {
|
||||
#[derive(Default)]
|
||||
pub struct PointerRouter {
|
||||
hovered: Vec<HitTarget>,
|
||||
pressed: Option<HitTarget>,
|
||||
pressed: Option<Vec<HitTarget>>,
|
||||
}
|
||||
|
||||
impl PointerRouter {
|
||||
@@ -71,7 +71,11 @@ impl PointerRouter {
|
||||
}
|
||||
|
||||
pub fn pressed_target(&self) -> Option<&HitTarget> {
|
||||
self.pressed.as_ref()
|
||||
self.pressed.as_ref().and_then(|targets| targets.last())
|
||||
}
|
||||
|
||||
pub fn pressed_targets(&self) -> &[HitTarget] {
|
||||
self.pressed.as_deref().unwrap_or(&[])
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
@@ -114,7 +118,12 @@ impl PointerRouter {
|
||||
|
||||
match event.kind {
|
||||
PointerEventKind::Move => {
|
||||
if let Some(target) = self.pressed.clone().or(hit_target.clone()) {
|
||||
if let Some(target) = self
|
||||
.pressed
|
||||
.as_ref()
|
||||
.and_then(|targets| targets.last().cloned())
|
||||
.or(hit_target.clone())
|
||||
{
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Move,
|
||||
target,
|
||||
@@ -125,7 +134,7 @@ impl PointerRouter {
|
||||
}
|
||||
PointerEventKind::Down { button } => {
|
||||
if let Some(target) = hit_target {
|
||||
self.pressed = Some(target.clone());
|
||||
self.pressed = Some(self.hovered.clone());
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Down { button },
|
||||
target,
|
||||
@@ -135,7 +144,12 @@ impl PointerRouter {
|
||||
}
|
||||
}
|
||||
PointerEventKind::Up { button } => {
|
||||
if let Some(target) = self.pressed.take().or(hit_target) {
|
||||
if let Some(target) = self
|
||||
.pressed
|
||||
.take()
|
||||
.and_then(|targets| targets.last().cloned())
|
||||
.or(hit_target)
|
||||
{
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Up { button },
|
||||
target,
|
||||
|
||||
@@ -7,7 +7,9 @@ use crate::scene::{
|
||||
UiSize,
|
||||
};
|
||||
use crate::text::TextSystem;
|
||||
use crate::tree::{CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style};
|
||||
use crate::tree::{
|
||||
AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style,
|
||||
};
|
||||
|
||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||
let mut text_system = TextSystem::new();
|
||||
@@ -27,6 +29,11 @@ pub fn layout_scene_with_text_system(
|
||||
pub struct LayoutSnapshot {
|
||||
pub scene: SceneSnapshot,
|
||||
pub interaction_tree: InteractionTree,
|
||||
/// Minimum size the root element needs in logical units. Accounts for
|
||||
/// `min_width`/`min_height` constraints and the natural minimum of fixed-
|
||||
/// size content. Scroll boxes contribute only their own explicit/min size
|
||||
/// on the scroll axis, not their content.
|
||||
pub root_min_size: UiSize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -242,11 +249,13 @@ pub fn layout_snapshot_with_cache(
|
||||
"layout snapshot perf"
|
||||
);
|
||||
}
|
||||
let root_min_size = element_min_size(root);
|
||||
LayoutSnapshot {
|
||||
scene,
|
||||
interaction_tree: InteractionTree {
|
||||
root: interaction_root,
|
||||
},
|
||||
root_min_size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +273,7 @@ fn layout_element(
|
||||
clip_rect: Option<Rect>,
|
||||
) -> 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
|
||||
@@ -774,7 +784,11 @@ fn point_in_rounded_rect(point: crate::scene::Point, rect: Rect, radius: f32) ->
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct MeasuredChild {
|
||||
/// Offered cross-axis size (full available_cross for stretch; explicit value otherwise).
|
||||
cross: f32,
|
||||
/// Natural cross-axis size: set when parent uses non-stretch alignment, child has no
|
||||
/// explicit cross size, and the child is not flex. `None` means use `cross`.
|
||||
natural_cross: Option<f32>,
|
||||
main: f32,
|
||||
is_flex: bool,
|
||||
}
|
||||
@@ -804,15 +818,29 @@ 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);
|
||||
// min-size constraints (content-box) converted to outer.
|
||||
let min_cross_outer = child_min_cross_size(child, element.style.direction)
|
||||
.map(|c| (c + cross_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||
.map(|m| (m + main_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
// style.width/height are content-box; add insets to get the outer (allocated) size.
|
||||
// min-cross acts as floor; allow exceeding available_cross only when min demands it.
|
||||
let cross = child_cross_size(child, element.style.direction)
|
||||
.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));
|
||||
.map(|c| (c + cross_inset).max(0.0).max(min_cross_outer))
|
||||
.unwrap_or(available_cross.max(min_cross_outer))
|
||||
.min(available_cross.max(min_cross_outer));
|
||||
let explicit_main = child_main_size(child, element.style.direction)
|
||||
.map(|main| (main + main_inset).max(0.0).max(min_main_outer));
|
||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||
if is_flex {
|
||||
0.0
|
||||
// Flex items get their share of remaining space later; record min floor now.
|
||||
min_main_outer
|
||||
} else {
|
||||
perf_stats.intrinsic_calls += 1;
|
||||
if child.text_node().is_some() {
|
||||
@@ -833,9 +861,33 @@ fn layout_container_children(
|
||||
if let Some(intrinsic_started) = intrinsic_started {
|
||||
perf_stats.intrinsic_ms += intrinsic_started.elapsed().as_secs_f64() * 1_000.0;
|
||||
}
|
||||
intrinsic
|
||||
// Apply min-main floor to intrinsic result.
|
||||
intrinsic.max(min_main_outer)
|
||||
}
|
||||
});
|
||||
// For non-stretch alignment, measure the child's natural cross size so it is not
|
||||
// forcibly stretched to fill the container's cross axis. Only done for non-flex
|
||||
// children with no explicit cross-axis style, since flex main size is unknown here.
|
||||
// align_self on the child overrides the parent's align_items for this child.
|
||||
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||
let natural_cross = if effective_align != AlignItems::Stretch
|
||||
&& !is_flex
|
||||
&& child_cross_size(child, element.style.direction).is_none()
|
||||
{
|
||||
let offered = match element.style.direction {
|
||||
FlexDirection::Row => UiSize::new(measured_main, available_cross),
|
||||
FlexDirection::Column => UiSize::new(available_cross, measured_main),
|
||||
};
|
||||
let natural = intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
||||
Some(
|
||||
cross_axis_size(natural, element.style.direction)
|
||||
.max(min_cross_outer)
|
||||
.min(available_cross.max(min_cross_outer)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if is_flex {
|
||||
flex_total += child_flex_weight(child);
|
||||
} else {
|
||||
@@ -843,6 +895,7 @@ fn layout_container_children(
|
||||
}
|
||||
measured_children.push(MeasuredChild {
|
||||
cross,
|
||||
natural_cross,
|
||||
main: measured_main,
|
||||
is_flex,
|
||||
});
|
||||
@@ -853,20 +906,39 @@ fn layout_container_children(
|
||||
let mut children = Vec::with_capacity(element.children.len());
|
||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||
let child_main = if measured.is_flex {
|
||||
if flex_total <= 0.0 {
|
||||
let allocated = if flex_total <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
remaining_main * (child_flex_weight(child) / flex_total)
|
||||
}
|
||||
};
|
||||
// Apply min-main floor to flex allocation.
|
||||
let child_insets = content_insets(&child.style);
|
||||
let main_inset = main_axis_padding(child_insets, element.style.direction);
|
||||
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||
.map(|m| (m + main_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
allocated.max(min_main_outer)
|
||||
} else {
|
||||
measured.main
|
||||
};
|
||||
// For non-stretch alignment, use the child's natural cross size; otherwise stretch.
|
||||
// align_self on the child overrides the parent's align_items for this child.
|
||||
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||
let actual_cross = measured.natural_cross.unwrap_or(measured.cross);
|
||||
let cross_origin = {
|
||||
let base = cross_axis_origin(content, element.style.direction);
|
||||
match effective_align {
|
||||
AlignItems::Stretch | AlignItems::Start => base,
|
||||
AlignItems::Center => base + (available_cross - actual_cross) * 0.5,
|
||||
AlignItems::End => base + available_cross - actual_cross,
|
||||
}
|
||||
};
|
||||
let child_rect = child_rect(
|
||||
content,
|
||||
cross_origin,
|
||||
element.style.direction,
|
||||
cursor,
|
||||
child_main.max(0.0),
|
||||
measured.cross,
|
||||
actual_cross,
|
||||
);
|
||||
children.push(layout_element(
|
||||
child,
|
||||
@@ -1021,15 +1093,25 @@ fn layout_interaction_skeleton_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);
|
||||
let min_cross_outer = child_min_cross_size(child, element.style.direction)
|
||||
.map(|c| (c + cross_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||
.map(|m| (m + main_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
let cross = child_cross_size(child, element.style.direction)
|
||||
.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));
|
||||
.map(|c| (c + cross_inset).max(0.0).max(min_cross_outer))
|
||||
.unwrap_or(available_cross.max(min_cross_outer))
|
||||
.min(available_cross.max(min_cross_outer));
|
||||
let explicit_main = child_main_size(child, element.style.direction)
|
||||
.map(|main| (main + main_inset).max(0.0).max(min_main_outer));
|
||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||
if is_flex {
|
||||
0.0
|
||||
min_main_outer
|
||||
} else {
|
||||
intrinsic_main_size(
|
||||
child,
|
||||
@@ -1040,8 +1122,27 @@ fn layout_interaction_skeleton_children(
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
)
|
||||
.max(min_main_outer)
|
||||
}
|
||||
});
|
||||
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||
let natural_cross = if effective_align != AlignItems::Stretch
|
||||
&& !is_flex
|
||||
&& child_cross_size(child, element.style.direction).is_none()
|
||||
{
|
||||
let offered = match element.style.direction {
|
||||
FlexDirection::Row => UiSize::new(measured_main, available_cross),
|
||||
FlexDirection::Column => UiSize::new(available_cross, measured_main),
|
||||
};
|
||||
let natural = intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
||||
Some(
|
||||
cross_axis_size(natural, element.style.direction)
|
||||
.max(min_cross_outer)
|
||||
.min(available_cross.max(min_cross_outer)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if is_flex {
|
||||
flex_total += child_flex_weight(child);
|
||||
} else {
|
||||
@@ -1049,6 +1150,7 @@ fn layout_interaction_skeleton_children(
|
||||
}
|
||||
measured_children.push(MeasuredChild {
|
||||
cross,
|
||||
natural_cross,
|
||||
main: measured_main,
|
||||
is_flex,
|
||||
});
|
||||
@@ -1059,20 +1161,36 @@ fn layout_interaction_skeleton_children(
|
||||
let mut children = Vec::with_capacity(element.children.len());
|
||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||
let child_main = if measured.is_flex {
|
||||
if flex_total <= 0.0 {
|
||||
let allocated = if flex_total <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
remaining_main * (child_flex_weight(child) / flex_total)
|
||||
}
|
||||
};
|
||||
let child_insets = content_insets(&child.style);
|
||||
let main_inset = main_axis_padding(child_insets, element.style.direction);
|
||||
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||
.map(|m| (m + main_inset).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
allocated.max(min_main_outer)
|
||||
} else {
|
||||
measured.main
|
||||
};
|
||||
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||
let actual_cross = measured.natural_cross.unwrap_or(measured.cross);
|
||||
let cross_origin = {
|
||||
let base = cross_axis_origin(content, element.style.direction);
|
||||
match effective_align {
|
||||
AlignItems::Stretch | AlignItems::Start => base,
|
||||
AlignItems::Center => base + (available_cross - actual_cross) * 0.5,
|
||||
AlignItems::End => base + available_cross - actual_cross,
|
||||
}
|
||||
};
|
||||
let child_rect = child_rect(
|
||||
content,
|
||||
cross_origin,
|
||||
element.style.direction,
|
||||
cursor,
|
||||
child_main.max(0.0),
|
||||
measured.cross,
|
||||
actual_cross,
|
||||
);
|
||||
children.push(layout_interaction_skeleton(
|
||||
child,
|
||||
@@ -1097,13 +1215,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;
|
||||
}
|
||||
|
||||
@@ -1140,19 +1267,29 @@ 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_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(
|
||||
child.style.width.unwrap_or(content_size.width),
|
||||
child.style.height.unwrap_or(content_size.height),
|
||||
),
|
||||
UiSize::new(offered_w, offered_h),
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
width = width.max(child.style.width.unwrap_or(child_size.width));
|
||||
// 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)
|
||||
@@ -1170,17 +1307,26 @@ fn intrinsic_container_content_size(
|
||||
child_main_sizes.push(None);
|
||||
continue;
|
||||
}
|
||||
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(
|
||||
child.style.width.unwrap_or(content_size.width),
|
||||
child.style.height.unwrap_or(content_size.height),
|
||||
),
|
||||
UiSize::new(offered_w, offered_h),
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
let child_main = child.style.width.unwrap_or(child_size.width);
|
||||
// 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));
|
||||
}
|
||||
@@ -1194,12 +1340,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,
|
||||
@@ -1207,7 +1356,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)
|
||||
}
|
||||
@@ -1255,30 +1404,51 @@ 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).
|
||||
|
||||
let min_outer_w = element
|
||||
.style
|
||||
.min_width
|
||||
.map(|mw| mw + horizontal_insets(insets))
|
||||
.unwrap_or(0.0);
|
||||
let min_outer_h = element
|
||||
.style
|
||||
.min_height
|
||||
.map(|mh| mh + vertical_insets(insets))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
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))
|
||||
.max(min_outer_w),
|
||||
(element.style.height.unwrap_or(measured.height) + vertical_insets(insets))
|
||||
.max(min_outer_h),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(image) = element.image_node() {
|
||||
let resolved = resolve_image_element_size(element, image.resource.size());
|
||||
return UiSize::new(
|
||||
resolved.width + horizontal_insets(insets),
|
||||
resolved.height + vertical_insets(insets),
|
||||
(resolved.width + horizontal_insets(insets)).max(min_outer_w),
|
||||
(resolved.height + vertical_insets(insets)).max(min_outer_h),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1287,10 +1457,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),
|
||||
@@ -1298,8 +1471,14 @@ 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))
|
||||
.max(min_outer_w),
|
||||
(explicit_height
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(vertical_insets(insets)))
|
||||
.max(min_outer_h),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1312,9 +1491,159 @@ fn intrinsic_size_inner(
|
||||
);
|
||||
|
||||
UiSize::new(
|
||||
explicit_width
|
||||
.unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter),
|
||||
explicit_height.unwrap_or(intrinsic_content.height + vertical_insets(insets)),
|
||||
(explicit_width
|
||||
.map(|w| w + horizontal_insets(insets) + scroll_gutter)
|
||||
.unwrap_or(intrinsic_content.width + horizontal_insets(insets) + scroll_gutter))
|
||||
.max(min_outer_w),
|
||||
(explicit_height
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(intrinsic_content.height + vertical_insets(insets)))
|
||||
.max(min_outer_h),
|
||||
)
|
||||
}
|
||||
|
||||
/// Computes the minimum logical size that the element's content requires.
|
||||
///
|
||||
/// Unlike [`intrinsic_size`], this function is designed for window minimum-size
|
||||
/// propagation. The key difference is scroll boxes: a vertically-scrolling box
|
||||
/// does not propagate its content height to the window minimum — only its own
|
||||
/// explicit `height` or `min_height` contributes. Flex children without an
|
||||
/// explicit size or `min_*` constraint contribute 0 on the main axis (they
|
||||
/// can shrink). Text nodes with no explicit size contribute 0 (they wrap).
|
||||
///
|
||||
/// The result is in the same coordinate space as [`intrinsic_size`] (outer /
|
||||
/// including padding and border insets).
|
||||
pub fn element_min_size(element: &Element) -> UiSize {
|
||||
let insets = content_insets(&element.style);
|
||||
let min_outer_w = element
|
||||
.style
|
||||
.min_width
|
||||
.map(|mw| (mw + horizontal_insets(insets)).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
let min_outer_h = element
|
||||
.style
|
||||
.min_height
|
||||
.map(|mh| (mh + vertical_insets(insets)).max(0.0))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Text: can wrap to any width; only explicit size or min_* constrain the window.
|
||||
if element.text_node().is_some() {
|
||||
let w = element
|
||||
.style
|
||||
.width
|
||||
.map(|w| w + horizontal_insets(insets))
|
||||
.unwrap_or(0.0)
|
||||
.max(min_outer_w);
|
||||
let h = element
|
||||
.style
|
||||
.height
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(0.0)
|
||||
.max(min_outer_h);
|
||||
return UiSize::new(w, h);
|
||||
}
|
||||
|
||||
// Image: natural size counts as a minimum.
|
||||
if let Some(image) = element.image_node() {
|
||||
let resolved = resolve_image_element_size(element, image.resource.size());
|
||||
return UiSize::new(
|
||||
(resolved.width + horizontal_insets(insets)).max(min_outer_w),
|
||||
(resolved.height + vertical_insets(insets)).max(min_outer_h),
|
||||
);
|
||||
}
|
||||
|
||||
let explicit_w = element.style.width;
|
||||
let explicit_h = element.style.height;
|
||||
|
||||
// Scroll box: content scrolls on the vertical axis; use only explicit/min height.
|
||||
// Width still accumulates child widths (no horizontal scroll yet).
|
||||
if element.scroll_box_node().is_some() {
|
||||
let scroll_gutter = element
|
||||
.scroll_box_node()
|
||||
.map_or(0.0, |sb| sb.scrollbar.gutter_width.max(0.0));
|
||||
let w = explicit_w
|
||||
.map(|w| w + horizontal_insets(insets) + scroll_gutter)
|
||||
.unwrap_or(horizontal_insets(insets) + scroll_gutter)
|
||||
.max(min_outer_w);
|
||||
let h = explicit_h
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(vertical_insets(insets))
|
||||
.max(min_outer_h);
|
||||
return UiSize::new(w, h);
|
||||
}
|
||||
|
||||
// Regular container: sum/max children's minimums.
|
||||
if element.children.is_empty() {
|
||||
return UiSize::new(
|
||||
explicit_w
|
||||
.map(|w| w + horizontal_insets(insets))
|
||||
.unwrap_or(horizontal_insets(insets))
|
||||
.max(min_outer_w),
|
||||
explicit_h
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(vertical_insets(insets))
|
||||
.max(min_outer_h),
|
||||
);
|
||||
}
|
||||
|
||||
let gap_total = element.style.gap * element.children.len().saturating_sub(1) as f32;
|
||||
let children_min = match element.style.direction {
|
||||
FlexDirection::Column => {
|
||||
let mut width: f32 = 0.0;
|
||||
let mut height = gap_total;
|
||||
for child in &element.children {
|
||||
let child_min = element_min_size(child);
|
||||
// Flex children with no explicit height contribute their min_height only;
|
||||
// if that is also unset they contribute 0 (they can shrink freely).
|
||||
let skip_main = child.style.flex_grow > 0.0 && child.style.height.is_none();
|
||||
width = width.max(child_min.width);
|
||||
if !skip_main {
|
||||
height += child_min.height;
|
||||
} else {
|
||||
// Still count the min_height floor even for flex children.
|
||||
let child_insets = content_insets(&child.style);
|
||||
let min_h = child
|
||||
.style
|
||||
.min_height
|
||||
.map(|mh| mh + vertical_insets(child_insets))
|
||||
.unwrap_or(0.0);
|
||||
height += min_h;
|
||||
}
|
||||
}
|
||||
UiSize::new(width, height)
|
||||
}
|
||||
FlexDirection::Row => {
|
||||
let mut width = gap_total;
|
||||
let mut height: f32 = 0.0;
|
||||
for child in &element.children {
|
||||
let child_min = element_min_size(child);
|
||||
let skip_main = child.style.flex_grow > 0.0 && child.style.width.is_none();
|
||||
if !skip_main {
|
||||
width += child_min.width;
|
||||
} else {
|
||||
let child_insets = content_insets(&child.style);
|
||||
let min_w = child
|
||||
.style
|
||||
.min_width
|
||||
.map(|mw| mw + horizontal_insets(child_insets))
|
||||
.unwrap_or(0.0);
|
||||
width += min_w;
|
||||
}
|
||||
height = height.max(child_min.height);
|
||||
}
|
||||
UiSize::new(width, height)
|
||||
}
|
||||
};
|
||||
|
||||
UiSize::new(
|
||||
explicit_w
|
||||
.map(|w| w + horizontal_insets(insets))
|
||||
.unwrap_or(children_min.width + horizontal_insets(insets))
|
||||
.max(min_outer_w),
|
||||
explicit_h
|
||||
.map(|h| h + vertical_insets(insets))
|
||||
.unwrap_or(children_min.height + vertical_insets(insets))
|
||||
.max(min_outer_h),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1758,6 +2087,20 @@ fn child_cross_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn child_min_main_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
match direction {
|
||||
FlexDirection::Row => child.style.min_width,
|
||||
FlexDirection::Column => child.style.min_height,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_min_cross_size(child: &Element, direction: FlexDirection) -> Option<f32> {
|
||||
match direction {
|
||||
FlexDirection::Row => child.style.min_height,
|
||||
FlexDirection::Column => child.style.min_width,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_flex_weight(child: &Element) -> f32 {
|
||||
if child.style.flex_grow > 0.0 {
|
||||
child.style.flex_grow
|
||||
@@ -1773,6 +2116,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,
|
||||
@@ -1794,16 +2157,23 @@ fn main_axis_origin(rect: Rect, direction: FlexDirection) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn cross_axis_origin(content: Rect, direction: FlexDirection) -> f32 {
|
||||
match direction {
|
||||
FlexDirection::Row => content.origin.y,
|
||||
FlexDirection::Column => content.origin.x,
|
||||
}
|
||||
}
|
||||
|
||||
fn child_rect(
|
||||
content: Rect,
|
||||
cross_origin: f32,
|
||||
direction: FlexDirection,
|
||||
main_origin: f32,
|
||||
main_size: f32,
|
||||
cross_size: f32,
|
||||
) -> Rect {
|
||||
match direction {
|
||||
FlexDirection::Row => Rect::new(main_origin, content.origin.y, main_size, cross_size),
|
||||
FlexDirection::Column => Rect::new(content.origin.x, main_origin, cross_size, main_size),
|
||||
FlexDirection::Row => Rect::new(main_origin, cross_origin, main_size, cross_size),
|
||||
FlexDirection::Column => Rect::new(cross_origin, main_origin, cross_size, main_size),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! sibling crates.
|
||||
|
||||
pub(crate) mod trace_targets {
|
||||
#![allow(dead_code)]
|
||||
pub const PLATFORM: &str = "ruin_ui::platform";
|
||||
pub const SCENE: &str = "ruin_ui::scene";
|
||||
#[allow(dead_code)]
|
||||
@@ -31,7 +32,8 @@ pub use interaction::{
|
||||
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
||||
pub use layout::{
|
||||
HitTarget, InteractionTree, LayoutCache, LayoutNode, LayoutPath, LayoutSnapshot, ScrollMetrics,
|
||||
TextHitTarget, layout_snapshot, layout_snapshot_with_cache, layout_snapshot_with_text_system,
|
||||
TextHitTarget, element_min_size, layout_snapshot, layout_snapshot_with_cache,
|
||||
layout_snapshot_with_text_system,
|
||||
};
|
||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||
pub use platform::{
|
||||
@@ -49,8 +51,8 @@ pub use text::{
|
||||
TextStyle, TextSystem, TextWrap,
|
||||
};
|
||||
pub use tree::{
|
||||
Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element, ElementId,
|
||||
FlexDirection, ScrollbarStyle, Style,
|
||||
AlignItems, Border, BoxShadow, BoxShadowKind, CornerRadius, CursorIcon, Edges, Element,
|
||||
ElementId, FlexDirection, ScrollbarStyle, Style,
|
||||
};
|
||||
pub use window::{
|
||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||
|
||||
@@ -80,6 +80,9 @@ impl Color {
|
||||
/// safe to reserve as a sentinel. No user-facing API sets this value directly.
|
||||
pub const SENTINEL: Self = Self::rgba(0, 0, 0, 0);
|
||||
|
||||
pub const BLACK: Self = Self::rgb(0, 0, 0);
|
||||
pub const WHITE: Self = Self::rgb(255, 255, 255);
|
||||
|
||||
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ pub enum FlexDirection {
|
||||
Column,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum AlignItems {
|
||||
#[default]
|
||||
Stretch,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum CursorIcon {
|
||||
Default,
|
||||
@@ -180,8 +189,13 @@ impl Default for ScrollbarStyle {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Style {
|
||||
pub direction: FlexDirection,
|
||||
pub align_items: AlignItems,
|
||||
/// Per-element override of the parent container's `align_items`. `None` means inherit.
|
||||
pub align_self: Option<AlignItems>,
|
||||
pub width: Option<f32>,
|
||||
pub height: Option<f32>,
|
||||
pub min_width: Option<f32>,
|
||||
pub min_height: Option<f32>,
|
||||
pub flex_grow: f32,
|
||||
pub gap: f32,
|
||||
pub padding: Edges,
|
||||
@@ -198,8 +212,12 @@ impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Stretch,
|
||||
align_self: None,
|
||||
width: None,
|
||||
height: None,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
flex_grow: 0.0,
|
||||
gap: 0.0,
|
||||
padding: Edges::ZERO,
|
||||
@@ -332,6 +350,20 @@ impl Element {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_width(mut self, min_width: f32) -> Self {
|
||||
self.style.min_width = Some(min_width.max(0.0));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_height(mut self, min_height: f32) -> Self {
|
||||
self.style.min_height = Some(min_height.max(0.0));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn min_size(self, min_width: f32, min_height: f32) -> Self {
|
||||
self.min_width(min_width).min_height(min_height)
|
||||
}
|
||||
|
||||
pub fn flex(mut self, flex_grow: f32) -> Self {
|
||||
self.style.flex_grow = flex_grow.max(0.0);
|
||||
self
|
||||
@@ -382,6 +414,16 @@ impl Element {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn align_items(mut self, align: AlignItems) -> Self {
|
||||
self.style.align_items = align;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn align_self(mut self, align: AlignItems) -> Self {
|
||||
self.style.align_self = Some(align);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor(mut self, cursor: CursorIcon) -> Self {
|
||||
self.style.cursor = Some(cursor);
|
||||
self
|
||||
@@ -527,11 +569,21 @@ impl Hash for ScrollbarStyle {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for AlignItems {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(*self as u8).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Style {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.direction.hash(state);
|
||||
self.align_items.hash(state);
|
||||
self.align_self.hash(state);
|
||||
self.width.map(f32::to_bits).hash(state);
|
||||
self.height.map(f32::to_bits).hash(state);
|
||||
self.min_width.map(f32::to_bits).hash(state);
|
||||
self.min_height.map(f32::to_bits).hash(state);
|
||||
self.flex_grow.to_bits().hash(state);
|
||||
self.gap.to_bits().hash(state);
|
||||
self.padding.hash(state);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Common window model types.
|
||||
|
||||
use crate::scene::UiSize;
|
||||
use crate::scene::{Color, UiSize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct WindowId(u64);
|
||||
@@ -60,6 +60,7 @@ pub struct WindowSpec {
|
||||
pub maximized: bool,
|
||||
pub fullscreen: bool,
|
||||
pub resizable: bool,
|
||||
pub base_color: Color,
|
||||
}
|
||||
|
||||
impl WindowSpec {
|
||||
@@ -77,9 +78,15 @@ impl WindowSpec {
|
||||
maximized: false,
|
||||
fullscreen: false,
|
||||
resizable: true,
|
||||
base_color: Color::rgb(0x08, 0x0C, 0x14),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_color(mut self, color: Color) -> Self {
|
||||
self.base_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn key(mut self, key: impl Into<String>) -> Self {
|
||||
self.key = Some(key.into());
|
||||
self
|
||||
|
||||
Reference in New Issue
Block a user