#[cfg(feature = "virtual-list")]
pub fn virtual_list_viewport(
total_count: usize,
row_height: Dp,
offset_y: Dp,
viewport_height: Dp,
overscan_rows: usize,
direction: VirtualListScrollDirection,
) -> VirtualListViewport {
items_repeater_viewport_with_metrics(
total_count,
row_height,
&[],
offset_y,
viewport_height,
overscan_rows,
direction,
)
}
#[cfg(feature = "virtual-list")]
pub fn items_repeater_viewport_with_metrics(
total_count: usize,
row_height: Dp,
item_metrics: &[ZsItemsRepeaterItemMetric],
offset_y: Dp,
viewport_height: Dp,
overscan_rows: usize,
direction: VirtualListScrollDirection,
) -> VirtualListViewport {
let row_height = normalized_items_repeater_estimated_height(row_height);
let item_metrics = normalized_items_repeater_metrics(total_count, item_metrics);
let viewport_height = if viewport_height.0.is_finite() {
viewport_height.0.max(0.0)
} else {
0.0
};
let requested_offset = if offset_y.0.is_finite() {
offset_y.0.max(0.0)
} else {
0.0
};
let content_height =
items_repeater_content_height_normalized(total_count, row_height, &item_metrics);
let max_offset = (content_height - viewport_height as f64).max(0.0) as f32;
let offset_y = requested_offset.min(max_offset);
if total_count == 0 || viewport_height <= 0.0 {
return VirtualListViewport {
offset_y: Dp::new(offset_y),
row_height: Dp::new(row_height),
visible_range: VirtualListRange::new(0, 0),
materialized_range: VirtualListRange::new(0, 0),
direction,
};
}
let offset = f64::from(offset_y);
let viewport_end = offset + f64::from(viewport_height);
let mut start_low = 0usize;
let mut start_high = total_count;
while start_low < start_high {
let middle = start_low + (start_high - start_low) / 2;
let bottom = items_repeater_height_before_normalized(
middle.saturating_add(1),
row_height,
&item_metrics,
);
if bottom <= offset {
start_low = middle.saturating_add(1);
} else {
start_high = middle;
}
}
let start = start_low.min(total_count);
let mut end_low = start.saturating_add(1).min(total_count);
let mut end_high = total_count;
while end_low < end_high {
let middle = end_low + (end_high - end_low) / 2;
let top = items_repeater_height_before_normalized(middle, row_height, &item_metrics);
if top < viewport_end {
end_low = middle.saturating_add(1);
} else {
end_high = middle;
}
}
let end = end_low.max(start.saturating_add(1)).min(total_count);
let visible_range = VirtualListRange::new(start, end);
let materialized_range = VirtualListRange::new(
start.saturating_sub(overscan_rows),
end.saturating_add(overscan_rows).min(total_count),
);
VirtualListViewport {
offset_y: Dp::new(offset_y),
row_height: Dp::new(row_height),
visible_range,
materialized_range,
direction,
}
}
#[cfg(feature = "virtual-list")]
fn normalized_items_repeater_estimated_height(row_height: Dp) -> f32 {
if row_height.0.is_finite() {
row_height.0.max(1.0)
} else {
1.0
}
}
#[cfg(feature = "virtual-list")]
fn normalized_items_repeater_metrics(
total_count: usize,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> Vec<ZsItemsRepeaterItemMetric> {
let mut metrics = item_metrics
.iter()
.copied()
.filter(|metric| metric.index < total_count)
.map(|metric| ZsItemsRepeaterItemMetric::new(metric.index, metric.height))
.collect::<Vec<_>>();
metrics.sort_by_key(|metric| metric.index);
let mut normalized = Vec::with_capacity(metrics.len());
for metric in metrics {
if normalized
.last()
.is_some_and(|current: &ZsItemsRepeaterItemMetric| current.index == metric.index)
{
*normalized.last_mut().expect("metric entry must exist") = metric;
} else {
normalized.push(metric);
}
}
normalized
}
#[cfg(feature = "virtual-list")]
fn items_repeater_height_before_normalized(
index: usize,
estimated_height: f32,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> f64 {
let mut height = index as f64 * f64::from(estimated_height);
for metric in item_metrics
.iter()
.take_while(|metric| metric.index < index)
{
height += f64::from(metric.height.0 - estimated_height);
}
height.max(0.0)
}
#[cfg(feature = "virtual-list")]
fn items_repeater_content_height_normalized(
total_count: usize,
estimated_height: f32,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> f64 {
items_repeater_height_before_normalized(total_count, estimated_height, item_metrics)
}
#[cfg(feature = "virtual-list")]
pub(crate) fn items_repeater_content_height(
total_count: usize,
row_height: Dp,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> Dp {
let row_height = normalized_items_repeater_estimated_height(row_height);
let metrics = normalized_items_repeater_metrics(total_count, item_metrics);
Dp::new(
items_repeater_content_height_normalized(total_count, row_height, &metrics)
.min(f64::from(f32::MAX)) as f32,
)
}
#[cfg(feature = "virtual-list")]
pub(crate) fn items_repeater_item_height(
index: usize,
row_height: Dp,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> Dp {
item_metrics
.binary_search_by_key(&index, |metric| metric.index)
.ok()
.and_then(|position| item_metrics.get(position))
.map(|metric| metric.height)
.unwrap_or_else(|| Dp::new(normalized_items_repeater_estimated_height(row_height)))
}
#[cfg(feature = "virtual-list")]
pub(crate) fn items_repeater_height_before(
index: usize,
row_height: Dp,
item_metrics: &[ZsItemsRepeaterItemMetric],
) -> Dp {
let row_height = normalized_items_repeater_estimated_height(row_height);
Dp::new(
items_repeater_height_before_normalized(index, row_height, item_metrics)
.min(f64::from(f32::MAX)) as f32,
)
}
#[cfg(feature = "virtual-list")]
fn virtual_list_row_bounds(
bounds: Rect,
index: usize,
row_height: Dp,
item_metrics: &[ZsItemsRepeaterItemMetric],
offset_y: Dp,
dpi: Dpi,
) -> Rect {
let row_height_px = items_repeater_item_height(index, row_height, item_metrics)
.to_px(dpi)
.round_i32()
.max(1);
let offset_px = offset_y.to_px(dpi).round_i32().max(0);
let row_top = i64::from(
items_repeater_height_before(index, row_height, item_metrics)
.to_px(dpi)
.round_i32(),
)
.saturating_sub(offset_px as i64)
.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
Rect {
x: bounds.x,
y: bounds.y.saturating_add(row_top),
width: bounds.width,
height: row_height_px,
}
}
#[cfg(feature = "scroll")]
fn scrollbar_rect(rect: crate::UiRect) -> Rect {
Rect {
x: rect.left,
y: rect.top,
width: (rect.right - rect.left).max(0),
height: (rect.bottom - rect.top).max(0),
}
}
#[cfg(feature = "scroll")]
fn scrollbar_hit_slop(dpi: Dpi) -> (i32, i32) {
(
Dp::new(4.0).to_px(dpi).round_i32().max(0),
Dp::new(2.0).to_px(dpi).round_i32().max(0),
)
}
#[cfg(feature = "scroll")]
fn scroll_viewport_bounds<Msg>(node: &ViewNode<Msg>) -> Option<Rect> {
let bounds = node.bounds?;
#[cfg(feature = "tabs")]
if let ViewNodeKind::Tabs { tabs, selected, .. } = &node.kind {
let selected_index = selected
.and_then(|selected| tabs.iter().position(|candidate| candidate.id == selected));
let tab_bounds = tab_layout_bounds(bounds, node.style.padding, node.layout_dpi);
return Some(
crate::zs_tab_view_render_plan_for_tabs(
tab_bounds,
tabs,
selected_index,
node.resolved_platform_style(),
node.layout_dpi,
)
.content_bounds,
);
}
Some(inset_bounds(bounds, node.style.padding, node.layout_dpi))
}
#[cfg(feature = "scroll")]
fn scrollbar_layout<Msg>(node: &ViewNode<Msg>) -> Option<ZsScrollbarLayout> {
if !matches!(node.kind, ViewNodeKind::Scroll { .. })
&& node.style.overflow_y != ViewOverflow::Auto
{
return None;
}
let viewport = scroll_viewport_bounds(node)?;
let content_height = node
.resolved_scroll_content_height
.to_px(node.layout_dpi)
.round_i32()
.max(0);
let maximum_offset_px = content_height.saturating_sub(viewport.height).max(0);
if maximum_offset_px <= 0 || viewport.height <= 0 || viewport.width <= 0 {
return None;
}
let offset_y = match &node.kind {
ViewNodeKind::Scroll { offset_y, .. } => *offset_y,
_ => node.adaptive_scroll_offset_y,
};
let profile = crate::platform_component_profile::PlatformComponentProfile::for_style(
node.resolved_platform_style(),
)
.shell;
let bar_width = profile
.scrollbar_width
.to_px(node.layout_dpi)
.round_i32()
.max(1);
let margin = profile
.scrollbar_margin
.to_px(node.layout_dpi)
.round_i32()
.max(0);
let geometry = crate::shell_layout::ZsShellScrollLayout::new(
viewport.y,
viewport.y.saturating_add(viewport.height),
content_height,
viewport.height,
viewport.x.saturating_add(viewport.width),
margin,
bar_width,
node.layout_dpi,
);
let offset_px = offset_y.to_px(node.layout_dpi).round_i32().max(0);
let track = geometry.track_rect()?;
let thumb = geometry.thumb_rect(offset_px)?;
let (extra_left, extra_right) = scrollbar_hit_slop(node.layout_dpi);
let track_hit = geometry.track_hit_rect(extra_left, extra_right)?;
let thumb_hit = geometry.thumb_hit_rect(offset_px, extra_left)?;
Some(ZsScrollbarLayout {
track: scrollbar_rect(track),
track_hit: scrollbar_rect(track_hit),
thumb: scrollbar_rect(thumb),
thumb_hit: scrollbar_rect(thumb_hit),
maximum_offset: Dp::new(
maximum_offset_px as f32 / node.layout_dpi.scale_factor().max(f32::EPSILON),
),
offset_y: Dp::new(offset_y.0.clamp(
0.0,
maximum_offset_px as f32 / node.layout_dpi.scale_factor().max(f32::EPSILON),
)),
})
}
#[cfg(feature = "virtual-list")]
fn items_repeater_scrollbar_layout<Msg>(
node: &ViewNode<Msg>,
) -> Option<ZsItemsRepeaterScrollbarLayout> {
let bounds = node.bounds?;
let ViewNodeKind::VirtualList {
total_count,
row_height,
item_metrics,
offset_y,
..
} = &node.kind
else {
return None;
};
let content_bounds = inset_bounds(bounds, node.style.padding, node.layout_dpi);
let content_height = items_repeater_content_height(*total_count, *row_height, item_metrics);
let viewport_height = Dp::new(
content_bounds.height.max(0) as f32 / node.layout_dpi.scale_factor().max(f32::EPSILON),
);
let maximum_offset = Dp::new((content_height.0 - viewport_height.0).max(0.0));
if maximum_offset.0 <= 0.0 || content_bounds.height <= 0 || content_bounds.width <= 0 {
return None;
}
let profile = crate::platform_component_profile::PlatformComponentProfile::for_style(
node.resolved_platform_style(),
)
.shell;
let bar_width = profile
.scrollbar_width
.to_px(node.layout_dpi)
.round_i32()
.max(1);
let margin = profile
.scrollbar_margin
.to_px(node.layout_dpi)
.round_i32()
.max(0);
let layout = crate::shell_layout::ZsShellScrollLayout::new(
content_bounds.y,
content_bounds.y.saturating_add(content_bounds.height),
content_height.to_px(node.layout_dpi).round_i32().max(0),
content_bounds.height,
content_bounds.x.saturating_add(content_bounds.width),
margin,
bar_width,
node.layout_dpi,
);
let offset_px = offset_y.to_px(node.layout_dpi).round_i32().max(0);
let track = layout.track_rect()?;
let thumb = layout.thumb_rect(offset_px)?;
let (extra_left, extra_right) = scrollbar_hit_slop(node.layout_dpi);
let track_hit = layout.track_hit_rect(extra_left, extra_right)?;
let thumb_hit = layout.thumb_hit_rect(offset_px, extra_left)?;
Some(ZsItemsRepeaterScrollbarLayout {
track: scrollbar_rect(track),
track_hit: scrollbar_rect(track_hit),
thumb: scrollbar_rect(thumb),
thumb_hit: scrollbar_rect(thumb_hit),
maximum_offset,
})
}
fn split_child_bounds<Msg>(
bounds: Rect,
kind: &ViewNodeKind<Msg>,
children: &[ViewNode<Msg>],
gap: Option<Dp>,
justify: ViewJustify,
align: ViewAlign,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
_resolved_scroll_content_height: Option<Dp>,
) -> Vec<Rect> {
let child_count = children.len();
if child_count == 0 {
return Vec::new();
}
let gap = gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
match kind {
ViewNodeKind::Stack {
direction: ViewStackDirection::Row,
} => {
let widths = allocate_axis_lengths(
bounds.width,
gap,
children,
|style| style.width,
|style| style.min_width,
dpi,
false,
typography_scale,
text_measurements,
bounds.height,
);
let offsets = justified_axis_offsets(bounds.width, gap, &widths, justify);
widths
.into_iter()
.zip(offsets)
.zip(children)
.map(|((width, offset), child)| {
let height = cross_axis_length(
bounds.height,
child,
align,
dpi,
true,
typography_scale,
text_measurements,
width,
);
let rect = Rect {
x: bounds.x.saturating_add(offset),
y: bounds.y.saturating_add(cross_axis_offset(
bounds.height,
height,
align,
true,
)),
width,
height,
};
rect
})
.collect()
}
ViewNodeKind::Stack {
direction: ViewStackDirection::Column,
} => split_column_child_bounds(
bounds,
children,
gap,
justify,
align,
dpi,
typography_scale,
text_measurements,
),
#[cfg(feature = "list")]
ViewNodeKind::List { .. } => split_column_child_bounds(
bounds,
children,
gap,
justify,
align,
dpi,
typography_scale,
text_measurements,
),
#[cfg(feature = "scroll")]
ViewNodeKind::Scroll { offset_y, .. } => {
let offset_y = offset_y.to_px(dpi).round_i32().max(0);
let height = _resolved_scroll_content_height
.map(|height| height.to_px(dpi).round_i32())
.unwrap_or(bounds.height)
.max(bounds.height);
vec![
Rect {
x: bounds.x,
y: bounds.y - offset_y,
width: bounds.width,
height,
};
child_count
]
}
#[cfg(feature = "grid")]
ViewNodeKind::Grid {
columns,
rows,
placements,
column_gap,
row_gap,
} => split_grid_child_bounds(
bounds,
columns,
rows,
placements,
children,
column_gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(gap),
row_gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(gap),
dpi,
typography_scale,
text_measurements,
),
_ => vec![bounds; child_count],
}
}
#[cfg(feature = "grid")]
fn split_grid_child_bounds<Msg>(
bounds: Rect,
columns: &[ZsGridTrack],
rows: &[ZsGridTrack],
placements: &[ZsGridPlacement],
children: &[ViewNode<Msg>],
column_gap: i32,
row_gap: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> Vec<Rect> {
let column_minimums = grid_track_minimums(
columns.len(),
placements,
children,
false,
dpi,
typography_scale,
text_measurements,
);
let column_lengths =
allocate_grid_track_lengths(bounds.width, column_gap, columns, &column_minimums, dpi);
let row_minimums = grid_row_minimums_for_columns(
rows.len(),
placements,
children,
&column_lengths,
column_gap,
row_gap,
dpi,
typography_scale,
text_measurements,
HeightMeasurement::Minimum,
);
let row_lengths = allocate_grid_track_lengths(bounds.height, row_gap, rows, &row_minimums, dpi);
(0..children.len())
.map(|index| {
placements
.get(index)
.and_then(|placement| {
grid_placement_bounds(
bounds,
&column_lengths,
&row_lengths,
column_gap,
row_gap,
*placement,
)
})
.map(|cell| {
constrained_child_bounds(
cell,
&children[index],
dpi,
typography_scale,
text_measurements,
)
})
.unwrap_or(Rect {
x: bounds.x,
y: bounds.y,
width: 0,
height: 0,
})
})
.collect()
}
#[cfg(feature = "virtual-list")]
pub fn items_repeater_viewport(
total_count: usize,
row_height: Dp,
offset_y: Dp,
viewport_height: Dp,
overscan_rows: usize,
direction: ZsItemsRepeaterScrollDirection,
) -> ZsItemsRepeaterViewport {
virtual_list_viewport(
total_count,
row_height,
offset_y,
viewport_height,
overscan_rows,
direction,
)
}
#[cfg(feature = "grid")]
fn grid_track_minimums<Msg>(
track_count: usize,
placements: &[ZsGridPlacement],
children: &[ViewNode<Msg>],
vertical: bool,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> Vec<i32> {
let mut minimums = vec![0; track_count];
for (placement, child) in placements.iter().zip(children) {
let (track, span) = if vertical {
(placement.row, placement.row_span.get())
} else {
(placement.column, placement.column_span.get())
};
if span != 1 || track >= track_count {
continue;
}
let (fixed, minimum) = if vertical {
(child.style.height, child.style.min_height)
} else {
(child.style.width, child.style.min_width)
};
let fixed = fixed
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
.unwrap_or(0);
let minimum = minimum
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
.unwrap_or(0);
let intrinsic = if vertical {
intrinsic_min_height_px(child, 0, dpi, typography_scale, text_measurements)
} else {
intrinsic_min_width_px(child, dpi, typography_scale, text_measurements)
};
minimums[track] = minimums[track].max(fixed.max(minimum).max(intrinsic));
}
minimums
}
#[cfg(feature = "grid")]
fn grid_row_minimums_for_columns<Msg>(
row_count: usize,
placements: &[ZsGridPlacement],
children: &[ViewNode<Msg>],
column_lengths: &[i32],
column_gap: i32,
row_gap: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement: HeightMeasurement,
) -> Vec<i32> {
let mut minimums = vec![0; row_count];
for (placement, child) in placements.iter().zip(children) {
let Some((_, child_width)) = grid_axis_span(
0,
column_lengths,
column_gap,
placement.column,
placement.column_span,
) else {
continue;
};
let child_height = measured_height_px(
child,
child_width,
dpi,
typography_scale,
text_measurements,
measurement,
);
let start = placement.row;
if start >= row_count {
continue;
}
let end = start
.saturating_add(usize::from(placement.row_span.get()))
.min(row_count);
let covered = end.saturating_sub(start);
if covered == 0 {
continue;
}
let current = minimums[start..end]
.iter()
.copied()
.fold(0i32, i32::saturating_add)
.saturating_add(
row_gap
.max(0)
.saturating_mul(covered.saturating_sub(1) as i32),
);
let missing = child_height.saturating_sub(current).max(0);
if missing == 0 {
continue;
}
let quotient = missing / covered as i32;
let remainder = missing % covered as i32;
for (position, minimum) in minimums[start..end].iter_mut().enumerate() {
*minimum = minimum
.saturating_add(quotient)
.saturating_add(i32::from((position as i32) < remainder));
}
}
minimums
}
#[cfg(feature = "grid")]
fn grid_intrinsic_height_px<Msg>(
available_width: i32,
columns: &[ZsGridTrack],
rows: &[ZsGridTrack],
placements: &[ZsGridPlacement],
children: &[ViewNode<Msg>],
column_gap: i32,
row_gap: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement: HeightMeasurement,
) -> i32 {
if rows.is_empty() || columns.is_empty() {
return 0;
}
let column_minimums = grid_track_minimums(
columns.len(),
placements,
children,
false,
dpi,
typography_scale,
text_measurements,
);
let column_lengths = allocate_grid_track_lengths(
available_width.max(0),
column_gap,
columns,
&column_minimums,
dpi,
);
let row_minimums = grid_row_minimums_for_columns(
rows.len(),
placements,
children,
&column_lengths,
column_gap,
row_gap,
dpi,
typography_scale,
text_measurements,
measurement,
);
let rows_height = rows
.iter()
.enumerate()
.map(|(index, row)| {
let fixed = match row {
ZsGridTrack::Fixed(size) => size.to_px(dpi).round_i32().max(0),
ZsGridTrack::Fraction(_) => 0,
};
fixed.max(row_minimums.get(index).copied().unwrap_or(0))
})
.fold(0i32, i32::saturating_add);
rows_height.saturating_add(
row_gap
.max(0)
.saturating_mul(rows.len().saturating_sub(1) as i32),
)
}
#[cfg(feature = "grid")]
fn allocate_grid_track_lengths(
total: i32,
gap: i32,
tracks: &[ZsGridTrack],
minimums: &[i32],
dpi: Dpi,
) -> Vec<i32> {
if tracks.is_empty() {
return Vec::new();
}
let total = total.max(0);
let total_gap = gap
.max(0)
.saturating_mul(tracks.len().saturating_sub(1) as i32)
.min(total);
let available = total.saturating_sub(total_gap);
let requested = tracks
.iter()
.map(|track| match track {
ZsGridTrack::Fixed(size) => Some(size.to_px(dpi).round_i32().max(0)),
ZsGridTrack::Fraction(_) => None,
})
.collect::<Vec<_>>();
let mut lengths = requested
.iter()
.enumerate()
.map(|(index, value)| {
value
.unwrap_or(0)
.max(minimums.get(index).copied().unwrap_or(0))
})
.collect::<Vec<_>>();
let base_total = lengths.iter().copied().fold(0i32, i32::saturating_add);
if base_total >= available {
return lengths;
}
let fractional_indices = tracks
.iter()
.enumerate()
.filter_map(|(index, track)| match track {
ZsGridTrack::Fraction(weight) => Some((index, weight.get())),
ZsGridTrack::Fixed(_) => None,
})
.collect::<Vec<_>>();
if fractional_indices.is_empty() {
return lengths;
}
let fixed_total = tracks
.iter()
.enumerate()
.filter_map(|(index, track)| {
matches!(track, ZsGridTrack::Fixed(_)).then_some(lengths[index])
})
.fold(0i32, i32::saturating_add);
let mut unresolved = fractional_indices;
let mut distributable = available.saturating_sub(fixed_total).max(0);
loop {
let total_weight = unresolved.iter().fold(0u64, |total, (_, weight)| {
total.saturating_add(u64::from(*weight))
});
let constrained = unresolved
.iter()
.filter_map(|(index, weight)| {
let weighted_share = ((distributable as u128 * u128::from(*weight))
/ u128::from(total_weight))
.min(i32::MAX as u128) as i32;
(weighted_share < lengths[*index]).then_some(*index)
})
.collect::<Vec<_>>();
if constrained.is_empty() {
let mut assigned = 0;
for (position, (index, weight)) in unresolved.iter().enumerate() {
let length = if position + 1 == unresolved.len() {
distributable.saturating_sub(assigned)
} else {
((distributable as u128 * u128::from(*weight)) / u128::from(total_weight))
.min(i32::MAX as u128) as i32
};
lengths[*index] = length.max(lengths[*index]);
assigned = assigned.saturating_add(length);
}
break;
}
unresolved.retain(|(index, _)| {
if constrained.contains(index) {
distributable = distributable.saturating_sub(lengths[*index]);
false
} else {
true
}
});
if unresolved.is_empty() {
break;
}
}
lengths
}
#[cfg(any(feature = "grid", feature = "tabs"))]
fn constrained_child_bounds<Msg>(
cell: Rect,
child: &ViewNode<Msg>,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> Rect {
let width = constrained_child_axis_length(
cell.width,
child,
dpi,
false,
typography_scale,
text_measurements,
cell.height,
);
let height = constrained_child_axis_length(
cell.height,
child,
dpi,
true,
typography_scale,
text_measurements,
width,
);
Rect {
x: cell.x,
y: cell.y,
width,
height,
}
}
#[cfg(any(feature = "grid", feature = "tabs"))]
fn constrained_child_axis_length<Msg>(
available: i32,
child: &ViewNode<Msg>,
dpi: Dpi,
vertical: bool,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement_cross: i32,
) -> i32 {
let fixed = if vertical {
child.style.height
} else {
child.style.width
};
let minimum = if vertical {
child.style.min_height
} else {
child.style.min_width
}
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
.unwrap_or(0);
let minimum = minimum.max(if vertical {
intrinsic_min_height_px(
child,
measurement_cross,
dpi,
typography_scale,
text_measurements,
)
} else {
intrinsic_min_width_px(child, dpi, typography_scale, text_measurements)
});
fixed
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
.max(minimum)
})
.unwrap_or_else(|| available.max(minimum))
}
#[cfg(feature = "grid")]
fn grid_placement_bounds(
bounds: Rect,
column_lengths: &[i32],
row_lengths: &[i32],
column_gap: i32,
row_gap: i32,
placement: ZsGridPlacement,
) -> Option<Rect> {
let (x, width) = grid_axis_span(
bounds.x,
column_lengths,
column_gap,
placement.column,
placement.column_span,
)?;
let (y, height) = grid_axis_span(
bounds.y,
row_lengths,
row_gap,
placement.row,
placement.row_span,
)?;
Some(Rect {
x,
y,
width,
height,
})
}
#[cfg(feature = "grid")]
fn grid_axis_span(
origin: i32,
lengths: &[i32],
gap: i32,
start: usize,
span: ZsGridSpan,
) -> Option<(i32, i32)> {
if start >= lengths.len() {
return None;
}
let gap = gap.max(0);
let before = lengths[..start]
.iter()
.fold(0i32, |total, length| total.saturating_add(*length))
.saturating_add(gap.saturating_mul(start as i32));
let end = start
.saturating_add(usize::from(span.get()))
.min(lengths.len());
let covered_tracks = end.saturating_sub(start);
let length = lengths[start..end]
.iter()
.fold(0i32, |total, length| total.saturating_add(*length))
.saturating_add(gap.saturating_mul(covered_tracks.saturating_sub(1) as i32));
Some((origin.saturating_add(before), length.max(0)))
}
fn split_column_child_bounds<Msg>(
bounds: Rect,
children: &[ViewNode<Msg>],
gap: i32,
justify: ViewJustify,
align: ViewAlign,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> Vec<Rect> {
let heights = allocate_axis_lengths(
bounds.height,
gap,
children,
|style| style.height,
|style| style.min_height,
dpi,
true,
typography_scale,
text_measurements,
bounds.width,
);
let offsets = justified_axis_offsets(bounds.height, gap, &heights, justify);
heights
.into_iter()
.zip(offsets)
.zip(children)
.map(|((height, offset), child)| {
let width = cross_axis_length(
bounds.width,
child,
align,
dpi,
false,
typography_scale,
text_measurements,
height,
);
let rect = Rect {
x: bounds
.x
.saturating_add(cross_axis_offset(bounds.width, width, align, false)),
y: bounds.y.saturating_add(offset),
width,
height,
};
rect
})
.collect()
}
fn justified_axis_offsets(
available: i32,
gap: i32,
lengths: &[i32],
justify: ViewJustify,
) -> Vec<i32> {
let gap = gap.max(0);
let item_total = lengths.iter().copied().fold(0i32, i32::saturating_add);
let gap_total = gap.saturating_mul(lengths.len().saturating_sub(1) as i32);
let remaining = available
.max(0)
.saturating_sub(item_total.saturating_add(gap_total));
let count = lengths.len() as i64;
let mut cursor = 0i32;
lengths
.iter()
.enumerate()
.map(|(index, length)| {
let index = index as i64;
let extra = match justify {
ViewJustify::Start => 0,
ViewJustify::Center => remaining / 2,
ViewJustify::End => remaining,
ViewJustify::SpaceBetween if count > 1 => {
(i64::from(remaining) * index / (count - 1)) as i32
}
ViewJustify::SpaceAround if count > 0 => {
(i64::from(remaining) * (index * 2 + 1) / (count * 2)) as i32
}
ViewJustify::SpaceEvenly if count > 0 => {
(i64::from(remaining) * (index + 1) / (count + 1)) as i32
}
_ => 0,
};
let offset = cursor.saturating_add(extra);
cursor = cursor.saturating_add(*length).saturating_add(gap);
offset
})
.collect()
}
fn cross_axis_offset(available: i32, length: i32, align: ViewAlign, auto_centers: bool) -> i32 {
let remaining = available.saturating_sub(length).max(0);
match align {
ViewAlign::Auto if auto_centers => remaining / 2,
ViewAlign::Center => remaining / 2,
ViewAlign::End => remaining,
ViewAlign::Auto | ViewAlign::Start | ViewAlign::Stretch => 0,
}
}
#[cfg(feature = "label")]
fn navigation_intrinsic_height_px<Msg>(
node: &ViewNode<Msg>,
dpi: Dpi,
typography_scale: f32,
) -> i32 {
let explicit = node.style.height.or(node.style.min_height).map(|height| {
typography_aware_length_px(height, dpi, node.typography_scaled_height, typography_scale)
});
if let Some(explicit) = explicit {
return explicit.max(1);
}
let gap = node
.style
.gap
.map(|gap| gap.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
let padding = node
.style
.padding
.map(|padding| padding.to_px(dpi).round_i32().max(0))
.unwrap_or(0)
.saturating_mul(2);
let content = match &node.kind {
ViewNodeKind::Text { style, .. } => Dp::new(
style
.role
.metrics_for(crate::ZsTypographyPlatformStyle::current())
.line_height
* typography_scale,
)
.to_px(dpi)
.round_i32()
.max(1),
#[cfg(feature = "icon")]
ViewNodeKind::Icon { size, .. } => standalone_icon_size_px(node, *size, dpi),
#[cfg(feature = "badge")]
ViewNodeKind::Badge { content, .. } => {
standalone_badge_size_px(node, *content, dpi, typography_scale).1
}
ViewNodeKind::Stack {
direction: ViewStackDirection::Row,
} => node
.children
.iter()
.map(|child| navigation_intrinsic_height_px(child, dpi, typography_scale))
.max()
.unwrap_or(0),
ViewNodeKind::Stack {
direction: ViewStackDirection::Column,
} => node
.children
.iter()
.map(|child| navigation_intrinsic_height_px(child, dpi, typography_scale))
.fold(0i32, i32::saturating_add)
.saturating_add(gap.saturating_mul(node.children.len().saturating_sub(1) as i32)),
ViewNodeKind::Spacer => 0,
_ => {
crate::ZsBaseControlMetrics::for_platform(crate::ZsBaseControlPlatformStyle::current())
.button_height
.to_px(dpi)
.round_i32()
.max(1)
}
};
content.saturating_add(padding).max(1)
}
fn allocate_axis_lengths<Msg>(
total: i32,
gap: i32,
children: &[ViewNode<Msg>],
fixed: impl Fn(&ViewStyle) -> Option<Dp>,
minimum: impl Fn(&ViewStyle) -> Option<Dp>,
dpi: Dpi,
vertical: bool,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
cross_available: i32,
) -> Vec<i32> {
let total = total.max(0);
let total_gap = gap
.saturating_mul(children.len().saturating_sub(1) as i32)
.min(total);
let available = total - total_gap;
let requested = children
.iter()
.map(|child| {
fixed(&child.style).map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
})
.collect::<Vec<_>>();
let mut minimums = Vec::with_capacity(children.len());
let mut preferreds = Vec::with_capacity(children.len());
for child in children {
let declared = minimum(&child.style)
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
.unwrap_or(0);
let (intrinsic_minimum, intrinsic_preferred) = if vertical {
let height = intrinsic_min_height_px(
child,
cross_available,
dpi,
typography_scale,
text_measurements,
);
(height, height)
} else {
(
intrinsic_min_width_px(child, dpi, typography_scale, text_measurements),
intrinsic_preferred_width_px(child, dpi, typography_scale, text_measurements),
)
};
let floor = declared.max(intrinsic_minimum);
minimums.push(floor);
preferreds.push(declared.max(intrinsic_preferred).max(floor));
}
let mut lengths = requested
.iter()
.zip(&preferreds)
.zip(&minimums)
.map(|((fixed, preferred), minimum)| fixed.unwrap_or(*preferred).max(*minimum))
.collect::<Vec<_>>();
let base_total: i32 = lengths.iter().copied().sum();
if base_total > available {
let total_slack: i32 = requested
.iter()
.zip(&minimums)
.zip(&lengths)
.map(|((fixed, minimum), length)| *length - fixed.unwrap_or(*minimum).max(*minimum))
.sum();
let mut remaining_deficit = (base_total - available).min(total_slack);
let mut remaining_slack = total_slack;
for (index, length) in lengths.iter_mut().enumerate() {
if remaining_deficit <= 0 || remaining_slack <= 0 {
break;
}
let floor = requested[index]
.unwrap_or(minimums[index])
.max(minimums[index]);
let slack = (*length - floor).max(0);
if slack == 0 {
continue;
}
let reduction = if slack >= remaining_slack {
remaining_deficit
} else {
((i64::from(remaining_deficit) * i64::from(slack)) / i64::from(remaining_slack))
as i32
}
.min(slack);
*length -= reduction;
remaining_deficit -= reduction;
remaining_slack -= slack;
}
return lengths;
}
let remaining = (available - base_total).max(0);
let flexible = requested
.iter()
.enumerate()
.filter(|(index, value)| value.is_none() && children[*index].style.flex > f32::EPSILON)
.map(|(index, _)| index)
.collect::<Vec<_>>();
if flexible.is_empty() {
return lengths;
}
let flex_total: f32 = flexible
.iter()
.map(|index| children[*index].style.flex.max(0.0))
.sum();
let denominator = flex_total;
let mut assigned = 0;
for (position, index) in flexible.iter().enumerate() {
let length = if position + 1 == flexible.len() {
remaining - assigned
} else {
let weight = children[*index].style.flex.max(0.0);
((remaining as f32) * weight / denominator).floor() as i32
}
.max(0);
lengths[*index] = lengths[*index].saturating_add(length);
assigned += length;
}
lengths
}
fn cross_axis_length<Msg>(
available: i32,
child: &ViewNode<Msg>,
align: ViewAlign,
dpi: Dpi,
vertical: bool,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement_cross: i32,
) -> i32 {
let available = available.max(0);
let fixed = if vertical {
child.style.height
} else {
child.style.width
};
let declared_minimum = if vertical {
child.style.min_height
} else {
child.style.min_width
};
let declared_minimum = declared_minimum
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
})
.unwrap_or(0);
let intrinsic = if vertical {
intrinsic_min_height_px(
child,
measurement_cross,
dpi,
typography_scale,
text_measurements,
)
} else {
intrinsic_min_width_px(child, dpi, typography_scale, text_measurements)
};
let minimum = declared_minimum.max(intrinsic);
let preferred = if vertical {
minimum
} else {
declared_minimum
.max(intrinsic_preferred_width_px(
child,
dpi,
typography_scale,
text_measurements,
))
.max(minimum)
};
#[cfg(feature = "label")]
let text_stretches_width = !vertical && matches!(&child.kind, ViewNodeKind::Text { .. });
#[cfg(not(feature = "label"))]
let text_stretches_width = false;
let padding = child
.style
.padding
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0)
.saturating_mul(2);
let stretches_cross_axis = owns_cross_axis(child) || text_stretches_width;
fixed
.map(|value| {
typography_aware_length_px(
value,
dpi,
vertical && child.typography_scaled_height,
typography_scale,
)
.max(minimum)
})
.unwrap_or_else(|| match align {
ViewAlign::Stretch => available.max(minimum),
ViewAlign::Start | ViewAlign::Center | ViewAlign::End => {
preferred.min(available.max(minimum))
}
ViewAlign::Auto
if child.style.flex <= f32::EPSILON
&& minimum > padding
&& !stretches_cross_axis =>
{
minimum
}
ViewAlign::Auto => available.max(minimum),
})
}
fn owns_cross_axis<Msg>(node: &ViewNode<Msg>) -> bool {
match &node.kind {
ViewNodeKind::Stack { .. } => true,
#[cfg(feature = "grid")]
ViewNodeKind::Grid { .. } => true,
#[cfg(feature = "tabs")]
ViewNodeKind::Tabs { .. } => true,
#[cfg(feature = "list")]
ViewNodeKind::List { .. } => true,
#[cfg(feature = "command-palette")]
ViewNodeKind::CommandPalette { .. } => node.children.first().is_some_and(owns_cross_axis),
#[cfg(feature = "dialog")]
ViewNodeKind::ContentDialog { .. } => node.children.first().is_some_and(owns_cross_axis),
#[cfg(feature = "toast")]
ViewNodeKind::ToastPresenter { .. } => node.children.first().is_some_and(owns_cross_axis),
#[cfg(feature = "flyout")]
ViewNodeKind::Flyout { .. } => node.children.first().is_some_and(owns_cross_axis),
#[cfg(feature = "teaching-tip")]
ViewNodeKind::TeachingTip { .. } => node.children.first().is_some_and(owns_cross_axis),
#[cfg(feature = "menu-flyout")]
ViewNodeKind::MenuFlyout { .. } => node.children.first().is_some_and(owns_cross_axis),
_ => false,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WidthMeasurement {
Minimum,
Preferred,
}
fn intrinsic_min_width_px<Msg>(
node: &ViewNode<Msg>,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
intrinsic_width_px(
node,
dpi,
typography_scale,
text_measurements,
WidthMeasurement::Minimum,
)
}
fn intrinsic_preferred_width_px<Msg>(
node: &ViewNode<Msg>,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
intrinsic_width_px(
node,
dpi,
typography_scale,
text_measurements,
WidthMeasurement::Preferred,
)
}
fn intrinsic_width_px<Msg>(
node: &ViewNode<Msg>,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement: WidthMeasurement,
) -> i32 {
let declared = node
.style
.width
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0)
.max(
node.style
.min_width
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0),
);
let padding = node
.style
.padding
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
let gap = node
.style
.gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
#[cfg(feature = "scroll")]
if matches!(node.kind, ViewNodeKind::Scroll { .. }) {
return declared;
}
let content = match &node.kind {
#[cfg(feature = "label")]
ViewNodeKind::Text { text, style } => match measurement {
WidthMeasurement::Minimum => {
measured_text_min_width_px(text, *style, dpi, typography_scale, text_measurements)
}
WidthMeasurement::Preferred => measured_text_natural_width_px(
text,
*style,
dpi,
typography_scale,
text_measurements,
),
},
#[cfg(feature = "button")]
ViewNodeKind::Button {
label,
presentation: ZsButtonPresentation::Standard,
enabled,
..
} => {
let mut style = SemanticTextStyle::body();
style.role = TextRole::Button;
style.color = if *enabled {
ColorRole::PrimaryText
} else {
ColorRole::DisabledText
};
style.horizontal_align = crate::HorizontalAlign::Center;
let label_width = measured_text_natural_width_px(
label,
style,
dpi,
typography_scale,
text_measurements,
);
let native_padding = if node.style.padding.is_some() {
0
} else {
let metrics =
crate::ZsBaseControlMetrics::for_platform(node.resolved_platform_style());
metrics
.button_padding_left
.to_px(dpi)
.round_i32()
.max(0)
.saturating_add(metrics.button_padding_right.to_px(dpi).round_i32().max(0))
};
label_width.saturating_add(native_padding)
}
#[cfg(feature = "icon")]
ViewNodeKind::Icon { size, .. } => standalone_icon_size_px(node, *size, dpi),
#[cfg(feature = "badge")]
ViewNodeKind::Badge { content, .. } => {
standalone_badge_size_px(node, *content, dpi, typography_scale).0
}
ViewNodeKind::Stack {
direction: ViewStackDirection::Row,
} => node
.children
.iter()
.map(|child| {
intrinsic_width_px(child, dpi, typography_scale, text_measurements, measurement)
})
.fold(0i32, i32::saturating_add)
.saturating_add(gap.saturating_mul(node.children.len().saturating_sub(1) as i32)),
ViewNodeKind::Stack {
direction: ViewStackDirection::Column,
} => node
.children
.iter()
.map(|child| {
intrinsic_width_px(child, dpi, typography_scale, text_measurements, measurement)
})
.max()
.unwrap_or(0),
#[cfg(feature = "list")]
ViewNodeKind::List { .. } => node
.children
.iter()
.map(|child| {
intrinsic_width_px(child, dpi, typography_scale, text_measurements, measurement)
})
.max()
.unwrap_or(0),
ViewNodeKind::Spacer => 0,
_ => node
.children
.iter()
.map(|child| {
intrinsic_width_px(child, dpi, typography_scale, text_measurements, measurement)
})
.max()
.unwrap_or(0),
};
declared.max(content.saturating_add(padding.saturating_mul(2)))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum HeightMeasurement {
Minimum,
#[cfg(feature = "scroll")]
Natural,
}
fn intrinsic_min_height_px<Msg>(
node: &ViewNode<Msg>,
available_width: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
measured_height_px(
node,
available_width,
dpi,
typography_scale,
text_measurements,
HeightMeasurement::Minimum,
)
}
#[cfg(feature = "scroll")]
fn natural_height_px<Msg>(
node: &ViewNode<Msg>,
available_width: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
measured_height_px(
node,
available_width,
dpi,
typography_scale,
text_measurements,
HeightMeasurement::Natural,
)
}
fn measured_height_px<Msg>(
node: &ViewNode<Msg>,
available_width: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
measurement: HeightMeasurement,
) -> i32 {
let declared_height = |value| {
typography_aware_length_px(value, dpi, node.typography_scaled_height, typography_scale)
};
let declared = node
.style
.height
.map(declared_height)
.unwrap_or(0)
.max(node.style.min_height.map(declared_height).unwrap_or(0));
let padding = node
.style
.padding
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
let gap = node
.style
.gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
let content_width = available_width.saturating_sub(padding.saturating_mul(2));
#[cfg(feature = "canvas")]
if let ViewNodeKind::Canvas {
builder: Some(builder),
..
} = &node.kind
{
if node.style.height.is_none() {
let scale = dpi.scale_factor().max(f32::EPSILON);
let context = crate::ZsCanvasLayoutContext::new(
crate::ZsCanvasSize::new(
Dp::new(content_width.max(0) as f32 / scale),
Dp::new(0.0),
),
dpi,
typography_scale,
Some(text_measurements),
);
if let Some(extent) = builder.build(&context).extent_height() {
let content = extent.to_px(dpi).round_i32().max(0);
return declared.max(content.saturating_add(padding.saturating_mul(2)));
}
}
}
#[cfg(feature = "scroll")]
if matches!(node.kind, ViewNodeKind::Scroll { .. }) {
return declared;
}
#[cfg(feature = "scroll")]
if measurement == HeightMeasurement::Minimum && node.style.overflow_y == ViewOverflow::Auto {
#[cfg(feature = "tabs")]
if matches!(node.kind, ViewNodeKind::Tabs { .. }) {
let metrics =
crate::ZsTabViewMetrics::for_platform(crate::ZsTabPlatformStyle::current());
let chrome = metrics
.strip_height
.to_px(dpi)
.round_i32()
.max(0)
.saturating_add(
metrics
.content_padding
.to_px(dpi)
.round_i32()
.max(0)
.saturating_mul(2),
);
return declared.max(chrome.saturating_add(padding.saturating_mul(2)));
}
return declared.max(padding.saturating_mul(2));
}
let content = match &node.kind {
#[cfg(feature = "label")]
ViewNodeKind::Text { text, style } => measured_text_min_height_px(
text,
*style,
content_width,
dpi,
typography_scale,
text_measurements,
),
#[cfg(feature = "icon")]
ViewNodeKind::Icon { size, .. } => standalone_icon_size_px(node, *size, dpi),
#[cfg(feature = "badge")]
ViewNodeKind::Badge { content, .. } => {
standalone_badge_size_px(node, *content, dpi, typography_scale).1
}
ViewNodeKind::Stack {
direction: ViewStackDirection::Row,
} => {
let widths = allocate_axis_lengths(
content_width,
gap,
&node.children,
|style| style.width,
|style| style.min_width,
dpi,
false,
typography_scale,
text_measurements,
0,
);
node.children
.iter()
.zip(widths)
.map(|(child, width)| {
measured_height_px(
child,
width,
dpi,
typography_scale,
text_measurements,
measurement,
)
})
.max()
.unwrap_or(0)
}
ViewNodeKind::Stack {
direction: ViewStackDirection::Column,
} => node
.children
.iter()
.map(|child| {
measured_height_px(
child,
content_width,
dpi,
typography_scale,
text_measurements,
measurement,
)
})
.fold(0i32, i32::saturating_add)
.saturating_add(gap.saturating_mul(node.children.len().saturating_sub(1) as i32)),
#[cfg(feature = "list")]
ViewNodeKind::List { .. } => node
.children
.iter()
.map(|child| {
measured_height_px(
child,
content_width,
dpi,
typography_scale,
text_measurements,
measurement,
)
})
.fold(0i32, i32::saturating_add)
.saturating_add(gap.saturating_mul(node.children.len().saturating_sub(1) as i32)),
#[cfg(feature = "grid")]
ViewNodeKind::Grid {
columns,
rows,
placements,
column_gap,
row_gap,
} => grid_intrinsic_height_px(
content_width,
columns,
rows,
placements,
&node.children,
column_gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(gap),
row_gap
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(gap),
dpi,
typography_scale,
text_measurements,
measurement,
),
#[cfg(feature = "tabs")]
ViewNodeKind::Tabs { .. } => {
let metrics =
crate::ZsTabViewMetrics::for_platform(crate::ZsTabPlatformStyle::current());
let strip = metrics.strip_height.to_px(dpi).round_i32().max(0);
let content_padding = metrics.content_padding.to_px(dpi).round_i32().max(0);
let tab_content_width = content_width.saturating_sub(content_padding.saturating_mul(2));
let selected_height = node
.children
.iter()
.map(|child| {
measured_height_px(
child,
tab_content_width,
dpi,
typography_scale,
text_measurements,
measurement,
)
})
.max()
.unwrap_or(0);
strip
.saturating_add(content_padding.saturating_mul(2))
.saturating_add(selected_height)
}
ViewNodeKind::Spacer => 0,
_ => node
.children
.iter()
.map(|child| {
measured_height_px(
child,
content_width,
dpi,
typography_scale,
text_measurements,
measurement,
)
})
.max()
.unwrap_or(0),
};
declared.max(content.saturating_add(padding.saturating_mul(2)))
}
#[cfg(feature = "icon")]
fn standalone_icon_size_px<Msg>(node: &ViewNode<Msg>, size: crate::ZsIconSize, dpi: Dpi) -> i32 {
node.icon_role
.unwrap_or(size.role())
.logical_size(node.resolved_platform_style())
.to_px(dpi)
.round_i32()
.max(1)
}
#[cfg(feature = "badge")]
fn standalone_badge_size_px<Msg>(
node: &ViewNode<Msg>,
content: crate::ZsBadgeContent,
dpi: Dpi,
typography_scale: f32,
) -> (i32, i32) {
let (width, height) = crate::platform_component_profile::PlatformBadgeProfile::for_platform(
node.resolved_platform_style(),
)
.size(content, typography_scale);
(
width.to_px(dpi).round_i32().max(1),
height.to_px(dpi).round_i32().max(1),
)
}
#[cfg(any(feature = "button", feature = "label"))]
fn measured_text_natural_width_px(
text: &str,
style: SemanticTextStyle,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
if style.wrap != crate::TextWrap::Word {
if let Some(measured) = text_measurements.measure(text, style, 0) {
return measured.width.max(0);
}
}
let units = if style.wrap == crate::TextWrap::Word {
text.lines()
.map(estimated_wrapping_segment_units)
.max()
.unwrap_or(0)
} else {
crate::widget_render::zs_estimated_text_width_units(text)
};
estimated_text_units_px(units, style, dpi, typography_scale)
}
#[cfg(feature = "label")]
fn measured_text_min_width_px(
text: &str,
style: SemanticTextStyle,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
if style.wrap == crate::TextWrap::Word || !style.ellipsis {
return measured_text_natural_width_px(
text,
style,
dpi,
typography_scale,
text_measurements,
);
}
if let Some(measured) = text_measurements.measure("…", style, 0) {
return measured.width.max(1);
}
estimated_text_units_px(
crate::widget_render::zs_estimated_text_width_units("…"),
style,
dpi,
typography_scale,
)
}
#[cfg(feature = "label")]
fn measured_text_min_height_px(
text: &str,
style: SemanticTextStyle,
available_width: i32,
dpi: Dpi,
typography_scale: f32,
text_measurements: &ViewTextMeasurements,
) -> i32 {
if let Some(measured) = text_measurements.measure(text, style, available_width) {
return measured.height.max(1);
}
if style.wrap != crate::TextWrap::Word {
if let Some(measured) = text_measurements.measure(text, style, 0) {
return measured.height.max(1);
}
}
let line_height = Dp::new(
style
.role
.metrics_for(crate::ZsTypographyPlatformStyle::current())
.line_height
* typography_scale.max(0.0),
)
.to_px(dpi)
.round_i32()
.max(1);
let rows = text
.lines()
.map(|line| {
if style.wrap != crate::TextWrap::Word || available_width <= 0 {
return 1;
}
let units = crate::widget_render::zs_estimated_text_width_units(line);
let width = estimated_text_units_px(units, style, dpi, typography_scale);
width
.saturating_add(available_width.saturating_sub(1))
.checked_div(available_width)
.unwrap_or(1)
.max(1)
})
.fold(0i32, i32::saturating_add)
.max(1);
line_height.saturating_mul(rows)
}
#[cfg(any(feature = "button", feature = "label"))]
fn estimated_text_units_px(
units: i32,
style: SemanticTextStyle,
dpi: Dpi,
typography_scale: f32,
) -> i32 {
if units <= 0 {
return 0;
}
let platform = crate::ZsBaseControlPlatformStyle::current();
let base = crate::ZsBaseControlMetrics::for_platform(platform);
let body_size = crate::TextRole::Body.metrics_for(platform).size.max(1.0);
let role_size = style.role.metrics_for(platform).size.max(1.0);
let weight_reserve = match style.weight {
crate::TextWeight::Semibold | crate::TextWeight::Bold => 1.08,
crate::TextWeight::Medium => 1.04,
crate::TextWeight::Automatic | crate::TextWeight::Regular => 1.0,
};
let width = (units.saturating_add(2) as f32)
* base.average_character_width.0
* (role_size / body_size)
* typography_scale.max(0.0)
* weight_reserve;
Dp::new(width).to_px(dpi).ceil_i32().max(1)
}
#[cfg(any(feature = "button", feature = "label"))]
fn estimated_wrapping_segment_units(line: &str) -> i32 {
let mut longest = 0i32;
let mut current_ascii_word = 0i32;
for character in line.chars() {
let width = crate::widget_render::zs_character_width_units(character);
if character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '/' | '.') {
current_ascii_word = current_ascii_word.saturating_add(width);
} else {
longest = longest.max(current_ascii_word).max(width);
current_ascii_word = 0;
}
}
longest.max(current_ascii_word)
}
fn typography_aware_length_px(
value: Dp,
dpi: Dpi,
typography_scaled: bool,
typography_scale: f32,
) -> i32 {
let scale = if typography_scaled {
typography_scale.max(0.0)
} else {
1.0
};
Dp::new(value.0 * scale).to_px(dpi).round_i32().max(0)
}
#[cfg(feature = "date-picker")]
fn clamp_visible_month(month: ZsDate, minimum: ZsDate, maximum: ZsDate) -> ZsDate {
let (minimum, maximum) = if minimum <= maximum {
(minimum, maximum)
} else {
(maximum, minimum)
};
month
.first_day_of_month()
.max(minimum.first_day_of_month())
.min(maximum.first_day_of_month())
}
fn inset_bounds(bounds: Rect, padding: Option<Dp>, dpi: Dpi) -> Rect {
let padding = padding
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
Rect {
x: bounds.x + padding,
y: bounds.y + padding,
width: (bounds.width - padding * 2).max(0),
height: (bounds.height - padding * 2).max(0),
}
}
#[cfg(feature = "list")]
fn horizontal_inset_bounds(bounds: Rect, inset: Option<Dp>, dpi: Dpi) -> Rect {
let inset = inset
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0);
Rect {
x: bounds.x.saturating_add(inset),
y: bounds.y,
width: bounds.width.saturating_sub(inset.saturating_mul(2)).max(0),
height: bounds.height,
}
}
#[cfg(feature = "tabs")]
fn tab_layout_bounds(bounds: Rect, padding: Option<Dp>, dpi: Dpi) -> Rect {
inset_bounds(bounds, padding, dpi)
}
#[cfg(any(feature = "label", feature = "button", feature = "textbox"))]
fn padded_bounds(bounds: Rect, padding: Option<Dp>, dpi: Dpi) -> Rect {
inset_bounds(bounds, padding, dpi)
}
#[cfg(feature = "button")]
fn button_content_bounds(bounds: Rect, padding: Option<Dp>, dpi: Dpi) -> Rect {
if padding.is_some() {
return padded_bounds(bounds, padding, dpi);
}
let metrics =
crate::ZsBaseControlMetrics::for_platform(crate::ZsBaseControlPlatformStyle::current());
side_inset_bounds(
bounds,
metrics.button_padding_left,
metrics.button_padding_top,
metrics.button_padding_right,
metrics.button_padding_bottom,
dpi,
)
}
#[cfg(feature = "textbox")]
fn text_input_content_bounds(bounds: Rect, padding: Option<Dp>, dpi: Dpi) -> Rect {
if padding.is_some() {
return padded_bounds(bounds, padding, dpi);
}
let metrics =
crate::ZsBaseControlMetrics::for_platform(crate::ZsBaseControlPlatformStyle::current());
side_inset_bounds(
bounds,
metrics.text_input_padding_left,
metrics.text_input_padding_top,
metrics.text_input_padding_right,
metrics.text_input_padding_bottom,
dpi,
)
}
#[cfg(any(feature = "button", feature = "textbox"))]
fn side_inset_bounds(bounds: Rect, left: Dp, top: Dp, right: Dp, bottom: Dp, dpi: Dpi) -> Rect {
let left = left.to_px(dpi).round_i32().max(0);
let top = top.to_px(dpi).round_i32().max(0);
let right = right.to_px(dpi).round_i32().max(0);
let bottom = bottom.to_px(dpi).round_i32().max(0);
Rect {
x: bounds.x.saturating_add(left),
y: bounds.y.saturating_add(top),
width: bounds
.width
.saturating_sub(left.saturating_add(right))
.max(0),
height: bounds
.height
.saturating_sub(top.saturating_add(bottom))
.max(0),
}
}
fn radius_px(radius: Option<Dp>, dpi: Dpi) -> i32 {
radius
.map(|value| value.to_px(dpi).round_i32().max(0))
.unwrap_or(0)
}
#[cfg(feature = "scroll")]
fn scroll_max_offset_y(bounds: Option<Rect>, content_height: Option<Dp>, dpi: Dpi) -> Dp {
let viewport_px = bounds
.map(|bounds| bounds.height.max(0) as f32)
.unwrap_or(0.0);
let content_px = content_height
.map(|height| height.to_px(dpi).0.max(0.0))
.unwrap_or(viewport_px);
let scale = (dpi.0 / Dpi::standard().0).max(f32::EPSILON);
Dp::new(((content_px - viewport_px) / scale).max(0.0))
}
fn color_role_for_token(token: ThemeColorToken) -> ColorRole {
match token {
ThemeColorToken::Surface => ColorRole::Surface,
ThemeColorToken::SurfaceRaised => ColorRole::SurfaceRaised,
ThemeColorToken::TextPrimary => ColorRole::PrimaryText,
ThemeColorToken::TextSecondary => ColorRole::SecondaryText,
ThemeColorToken::Accent => ColorRole::Accent,
ThemeColorToken::AccentText => ColorRole::AccentText,
ThemeColorToken::Control => ColorRole::Control,
ThemeColorToken::Border => ColorRole::Border,
ThemeColorToken::Success => ColorRole::Success,
ThemeColorToken::Warning => ColorRole::Warning,
ThemeColorToken::Danger => ColorRole::Danger,
}
}
fn clipped_rect(rect: Rect, clip: Option<Rect>) -> Option<Rect> {
let Some(clip) = clip else {
return Some(rect);
};
let left = rect.x.max(clip.x);
let top = rect.y.max(clip.y);
let right = (rect.x + rect.width).min(clip.x + clip.width);
let bottom = (rect.y + rect.height).min(clip.y + clip.height);
let width = right - left;
let height = bottom - top;
(width > 0 && height > 0).then_some(Rect {
x: left,
y: top,
width,
height,
})
}
const MAX_RETAINED_TEXT_MEASUREMENTS: usize = 4_096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct ViewTextMeasurementKey {
text_fingerprint_a: u64,
text_fingerprint_b: u64,
text_utf8_len: usize,
style_fingerprint: u64,
max_width: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ViewTextMeasurementValue {
size: crate::Size,
last_used: u64,
}
#[derive(Clone)]
pub(crate) struct ViewTextMeasurements {
fingerprint_a: std::collections::hash_map::RandomState,
fingerprint_b: std::collections::hash_map::RandomState,
entries: std::collections::BTreeMap<ViewTextMeasurementKey, ViewTextMeasurementValue>,
next_use: u64,
}
impl Default for ViewTextMeasurements {
fn default() -> Self {
Self {
fingerprint_a: std::collections::hash_map::RandomState::new(),
fingerprint_b: std::collections::hash_map::RandomState::new(),
entries: std::collections::BTreeMap::new(),
next_use: 0,
}
}
}
impl fmt::Debug for ViewTextMeasurements {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ViewTextMeasurements")
.field("entry_count", &self.entries.len())
.finish()
}
}
impl PartialEq for ViewTextMeasurements {
fn eq(&self, other: &Self) -> bool {
self.entries.len() == other.entries.len()
&& self.entries.iter().zip(other.entries.iter()).all(
|((left_key, left_value), (right_key, right_value))| {
left_key == right_key && left_value.size == right_value.size
},
)
}
}
impl Eq for ViewTextMeasurements {}
impl ViewTextMeasurements {
pub(crate) fn insert(
&mut self,
text: impl AsRef<str>,
style: crate::SemanticTextStyle,
max_width: i32,
size: crate::Size,
) {
let key = self.key(text.as_ref(), style, max_width);
let size = size.clamp_non_negative();
self.next_use = self.next_use.saturating_add(1);
self.entries.insert(
key,
ViewTextMeasurementValue {
size,
last_used: self.next_use,
},
);
while self.entries.len() > MAX_RETAINED_TEXT_MEASUREMENTS {
let oldest = self
.entries
.iter()
.min_by_key(|(_, value)| value.last_used)
.map(|(key, _)| *key);
let Some(oldest) = oldest else {
break;
};
self.entries.remove(&oldest);
}
}
pub(crate) fn measure(
&self,
text: &str,
style: crate::SemanticTextStyle,
max_width: i32,
) -> Option<crate::Size> {
self.entries
.get(&self.key(text, style, max_width))
.map(|entry| entry.size)
}
fn key(
&self,
text: &str,
style: crate::SemanticTextStyle,
max_width: i32,
) -> ViewTextMeasurementKey {
use std::hash::{BuildHasher, Hasher};
let mut fingerprint_a = self.fingerprint_a.build_hasher();
fingerprint_a.write(text.as_bytes());
let mut fingerprint_b = self.fingerprint_b.build_hasher();
fingerprint_b.write(text.as_bytes());
ViewTextMeasurementKey {
text_fingerprint_a: fingerprint_a.finish(),
text_fingerprint_b: fingerprint_b.finish(),
text_utf8_len: text.len(),
style_fingerprint: semantic_text_style_fingerprint(style),
max_width: max_width.max(0),
}
}
}
fn semantic_text_style_fingerprint(style: crate::SemanticTextStyle) -> u64 {
let mut fingerprint = style.role as u64;
fingerprint = (fingerprint << 8) | style.color as u64;
fingerprint = (fingerprint << 8) | style.weight as u64;
fingerprint = (fingerprint << 8) | style.horizontal_align as u64;
fingerprint = (fingerprint << 8) | style.vertical_align as u64;
fingerprint = (fingerprint << 8) | style.wrap as u64;
(fingerprint << 1) | u64::from(style.ellipsis)
}
#[cfg(all(test, feature = "label"))]
mod ellipsis_layout_tests {
use super::*;
fn child_bounds(output: &LayoutOutput, id: WidgetId) -> Rect {
output
.children
.iter()
.find(|node| node.component == id.into())
.expect("layout child should expose bounds")
.bounds
}
fn measured_layout(
view: &mut ViewNode<()>,
width: i32,
measurements: ViewTextMeasurements,
) -> LayoutOutput {
view.layout(
&mut ViewLayoutCx::new(
Rect {
x: 0,
y: 0,
width,
height: 200,
},
Dpi::standard(),
)
.with_text_measurements(Arc::new(measurements)),
)
}
#[cfg(feature = "button")]
fn button_label_style() -> SemanticTextStyle {
let mut style = SemanticTextStyle::body();
style.role = TextRole::Button;
style.horizontal_align = crate::HorizontalAlign::Center;
style
}
#[cfg(feature = "button")]
fn button_floor(label: &str) -> i32 {
crate::ZsBaseControlMetrics::current()
.button_minimum_width_for_label(label)
.to_px(Dpi::standard())
.round_i32()
}
#[test]
#[cfg(feature = "button")]
fn ellipsis_label_shrinks_inside_narrow_row_and_keeps_button_visible() {
let label = WidgetId::new(9_401);
let action = WidgetId::new(9_402);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text(path, style).id(label),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 200, measurements);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert!(label_bounds.width < 480);
assert!(label_bounds.width >= 1);
assert!(button_bounds.width >= button_floor("打开"));
assert!(button_bounds.x >= label_bounds.x + label_bounds.width + 8);
assert!(button_bounds.x + button_bounds.width <= 200);
}
#[test]
#[cfg(feature = "button")]
fn ellipsis_label_keeps_preferred_width_when_row_has_room() {
let label = WidgetId::new(9_403);
let action = WidgetId::new(9_404);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text(path, style).id(label),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 800, measurements);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert_eq!(label_bounds.width, 480);
assert_eq!(button_bounds.x, 488);
}
#[test]
fn explicit_width_stays_a_hard_constraint_for_ellipsis_text() {
let label = WidgetId::new(9_405);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
let mut view: ViewNode<()> =
row([styled_text(path, style).id(label).width(Dp::new(300.0))]);
let output = measured_layout(&mut view, 200, measurements);
assert_eq!(child_bounds(&output, label).width, 300);
}
#[test]
#[cfg(feature = "button")]
fn explicit_min_width_bounds_ellipsis_shrinkage() {
let label = WidgetId::new(9_406);
let action = WidgetId::new(9_407);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text(path, style).id(label).min_width(Dp::new(60.0)),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 260, measurements);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert!(label_bounds.width >= 60);
assert!(label_bounds.width < 480);
assert!(button_bounds.x + button_bounds.width <= 260);
}
#[test]
#[cfg(feature = "button")]
fn non_ellipsis_no_wrap_text_keeps_full_width_in_narrow_row() {
let label = WidgetId::new(9_408);
let action = WidgetId::new(9_409);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let mut style = SemanticTextStyle::body();
style.ellipsis = false;
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text(path, style).id(label),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 200, measurements);
assert_eq!(child_bounds(&output, label).width, 480);
}
#[test]
#[cfg(feature = "button")]
fn cjk_and_emoji_ellipsis_label_shrinks_without_covering_button() {
let label = WidgetId::new(9_410);
let action = WidgetId::new(9_411);
let path = "存档/非常/长/的/目录/层级/没有/空格/🔥📁/最终文件.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 520,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text(path, style).id(label),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 220, measurements);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert!(label_bounds.width < 520);
assert!(button_bounds.width >= button_floor("打开"));
assert!(button_bounds.x >= label_bounds.x + label_bounds.width + 8);
assert!(button_bounds.x + button_bounds.width <= 220);
}
#[test]
#[cfg(feature = "button")]
fn ellipsis_label_inside_nested_column_shrinks_in_narrow_row() {
let label = WidgetId::new(9_412);
let column_id = WidgetId::new(9_413);
let action = WidgetId::new(9_414);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = row([
column([styled_text(path, style).id(label)]).id(column_id),
button("打开").id(action),
])
.gap(Dp::new(8.0));
let output = measured_layout(&mut view, 200, measurements);
let column_bounds = child_bounds(&output, column_id);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert!(column_bounds.width < 480);
assert!(label_bounds.width <= column_bounds.width);
assert!(button_bounds.width >= button_floor("打开"));
assert!(button_bounds.x + button_bounds.width <= 200);
}
#[test]
#[cfg(feature = "button")]
fn ellipsis_row_inside_aligned_column_clamps_to_cross_axis() {
let label = WidgetId::new(9_415);
let inner = WidgetId::new(9_416);
let action = WidgetId::new(9_417);
let path = "C:/very/long/path/without-any-spaces/that/keeps/going/segment/file.txt";
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
path,
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = column([row([
styled_text(path, style).id(label),
button("打开").id(action),
])
.gap(Dp::new(8.0))
.id(inner)])
.align(ViewAlign::Start);
let output = measured_layout(&mut view, 200, measurements);
let inner_bounds = child_bounds(&output, inner);
let label_bounds = child_bounds(&output, label);
let button_bounds = child_bounds(&output, action);
assert_eq!(inner_bounds.width, 200);
assert!(label_bounds.width < 480);
assert!(button_bounds.width >= button_floor("打开"));
assert!(button_bounds.x + button_bounds.width <= 200);
}
#[test]
fn two_ellipsis_labels_share_the_narrow_row_deficit() {
let first = WidgetId::new(9_418);
let second = WidgetId::new(9_419);
let style = SemanticTextStyle::body();
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
"a/very/long/path/segment/without/spaces/alpha.txt",
style,
0,
crate::Size {
width: 480,
height: 20,
},
);
measurements.insert(
"b/shorter/path/beta.txt",
style,
0,
crate::Size {
width: 240,
height: 20,
},
);
let mut view: ViewNode<()> = row([
styled_text("a/very/long/path/segment/without/spaces/alpha.txt", style).id(first),
styled_text("b/shorter/path/beta.txt", style).id(second),
]);
let output = measured_layout(&mut view, 300, measurements);
let first_bounds = child_bounds(&output, first);
let second_bounds = child_bounds(&output, second);
assert_eq!(first_bounds.width + second_bounds.width, 300);
assert!(first_bounds.width < 480);
assert!(second_bounds.width < 240);
assert!(first_bounds.width > second_bounds.width);
assert!(second_bounds.x >= first_bounds.x + first_bounds.width);
}
#[test]
#[cfg(feature = "canvas")]
fn padded_canvas_fills_the_stack_cross_axis() {
let surface = WidgetId::new(9_420);
let mut view: ViewNode<()> = column([crate::canvas(crate::ZsCanvasScene::new())
.id(surface)
.height(Dp::new(84.0))
.padding(Dp::new(12.0))]);
let output = measured_layout(&mut view, 400, ViewTextMeasurements::default());
assert_eq!(child_bounds(&output, surface).width, 400);
}
#[test]
#[cfg(feature = "grid")]
fn grid_with_ellipsizable_cells_fills_the_stack_cross_axis() {
let layout = WidgetId::new(9_421);
let mut view: ViewNode<()> = column([grid(
[ZsGridTrack::FLEX, ZsGridTrack::FLEX],
[ZsGridTrack::fixed(Dp::new(56.0))],
[
ZsGridCell::new(0, 0, text("网格单元 A / Grid cell A")),
ZsGridCell::new(0, 1, text("网格单元 B / Grid cell B")),
],
)
.id(layout)
.padding(Dp::new(12.0))]);
let output = measured_layout(&mut view, 400, ViewTextMeasurements::default());
assert_eq!(child_bounds(&output, layout).width, 400);
}
#[test]
#[cfg(feature = "tabs")]
fn tab_view_fills_the_stack_cross_axis() {
let tabs = WidgetId::new(9_423);
let mut view: ViewNode<()> = column([tab_view(
[
ZsTabItem::new(ZsTabId::new(1), "常规 / General", text("General")),
ZsTabItem::new(ZsTabId::new(2), "高级 / Advanced", text("Advanced")),
ZsTabItem::new(ZsTabId::new(3), "关于 / About", text("About")),
],
Some(ZsTabId::new(1)),
)
.id(tabs)]);
let output = measured_layout(&mut view, 600, ViewTextMeasurements::default());
assert_eq!(child_bounds(&output, tabs).width, 600);
}
#[test]
#[cfg(feature = "command-palette")]
fn command_palette_around_a_page_fills_the_stack_cross_axis() {
let palette = WidgetId::new(9_424);
let mut view: ViewNode<()> = column([command_palette(
palette,
false,
"",
Vec::<crate::ZsCommandPaletteItem>::new(),
column([text("Page")]),
)]);
let output = measured_layout(&mut view, 600, ViewTextMeasurements::default());
assert_eq!(child_bounds(&output, palette).width, 600);
}
#[test]
#[cfg(all(feature = "flyout", feature = "button"))]
fn flyout_around_a_button_keeps_the_button_width() {
let host = WidgetId::new(9_425);
let action = WidgetId::new(9_426);
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> = column([flyout(
host,
false,
action,
crate::ZsFlyoutSpec::new(Dp::new(200.0), Dp::new(120.0)),
text("Flyout content"),
button("打开").id(action),
)]);
let output = measured_layout(&mut view, 600, measurements);
assert!(child_bounds(&output, host).width < 600);
}
#[test]
#[cfg(feature = "button")]
fn padded_button_keeps_its_natural_width_in_a_stack() {
let action = WidgetId::new(9_422);
let mut measurements = ViewTextMeasurements::default();
measurements.insert(
"打开",
button_label_style(),
0,
crate::Size {
width: 40,
height: 20,
},
);
let mut view: ViewNode<()> =
column([button("打开").id(action).padding(Dp::new(12.0))]);
let output = measured_layout(&mut view, 400, measurements);
assert!(child_bounds(&output, action).width < 400);
}
}