use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
ops::Range,
sync::Arc,
};
use derive_setters::Setters;
use tessera_ui::{
ComputedData, Constraint, DimensionValue, Dp, MeasurementError, NodeId, ParentConstraint, Px,
PxPosition, State, key,
layout::{LayoutInput, LayoutOutput, LayoutSpec},
remember, tessera,
};
use crate::{
alignment::{CrossAxisAlignment, MainAxisAlignment},
scrollable::{ScrollableArgs, ScrollableController, scrollable_with_controller},
};
const DEFAULT_VIEWPORT_LINES: usize = 8;
#[derive(Clone, Debug)]
pub enum GridCells {
Fixed(usize),
Adaptive(Dp),
FixedSize(Dp),
}
impl GridCells {
pub fn fixed(count: usize) -> Self {
Self::Fixed(count)
}
pub fn adaptive(min_size: Dp) -> Self {
Self::Adaptive(min_size)
}
pub fn fixed_size(size: Dp) -> Self {
Self::FixedSize(size)
}
fn resolve_slots(&self, available: Px, spacing: Px) -> Vec<Px> {
match self {
Self::Fixed(count) => {
let count = (*count).max(1);
calculate_cells_cross_axis_size(available, count, spacing)
}
Self::Adaptive(min_size) => {
let min_px = ensure_positive_px(Px::from(*min_size));
let spacing = sanitize_spacing(spacing);
let available_i32 = available.0.max(0);
let min_i32 = min_px.0.max(1);
let spacing_i32 = spacing.0.max(0);
let count =
((available_i32 + spacing_i32) / (min_i32 + spacing_i32)).max(1) as usize;
calculate_cells_cross_axis_size(available, count, spacing)
}
Self::FixedSize(size) => {
let cell_size = ensure_positive_px(Px::from(*size));
let spacing = sanitize_spacing(spacing);
let available_i32 = available.0.max(0);
let cell_i32 = cell_size.0.max(1);
let spacing_i32 = spacing.0.max(0);
if cell_i32 + spacing_i32 < available_i32 + spacing_i32 {
let count =
((available_i32 + spacing_i32) / (cell_i32 + spacing_i32)).max(1) as usize;
vec![cell_size; count]
} else {
vec![available.max(Px::ZERO)]
}
}
}
}
}
pub struct LazyGridController {
cache: LazyGridCache,
}
impl Default for LazyGridController {
fn default() -> Self {
Self::new()
}
}
impl LazyGridController {
pub fn new() -> Self {
Self {
cache: LazyGridCache::default(),
}
}
}
#[derive(Clone, Setters)]
pub struct LazyVerticalGridArgs {
pub scrollable: ScrollableArgs,
pub columns: GridCells,
pub main_axis_spacing: Dp,
pub cross_axis_spacing: Dp,
pub cross_axis_alignment: MainAxisAlignment,
pub item_alignment: CrossAxisAlignment,
pub overscan: usize,
pub estimated_item_size: Dp,
pub content_padding: Dp,
pub max_viewport_main: Option<Px>,
}
impl Default for LazyVerticalGridArgs {
fn default() -> Self {
Self {
scrollable: ScrollableArgs::default(),
columns: GridCells::fixed(2),
main_axis_spacing: Dp(0.0),
cross_axis_spacing: Dp(0.0),
cross_axis_alignment: MainAxisAlignment::Start,
item_alignment: CrossAxisAlignment::Stretch,
overscan: 2,
estimated_item_size: Dp(48.0),
content_padding: Dp(0.0),
max_viewport_main: Some(Px(8192)),
}
}
}
#[derive(Clone, Setters)]
pub struct LazyHorizontalGridArgs {
pub scrollable: ScrollableArgs,
pub rows: GridCells,
pub main_axis_spacing: Dp,
pub cross_axis_spacing: Dp,
pub cross_axis_alignment: MainAxisAlignment,
pub item_alignment: CrossAxisAlignment,
pub overscan: usize,
pub estimated_item_size: Dp,
pub content_padding: Dp,
pub max_viewport_main: Option<Px>,
}
impl Default for LazyHorizontalGridArgs {
fn default() -> Self {
Self {
scrollable: ScrollableArgs::default(),
rows: GridCells::fixed(2),
main_axis_spacing: Dp(0.0),
cross_axis_spacing: Dp(0.0),
cross_axis_alignment: MainAxisAlignment::Start,
item_alignment: CrossAxisAlignment::Stretch,
overscan: 2,
estimated_item_size: Dp(48.0),
content_padding: Dp(0.0),
max_viewport_main: Some(Px(8192)),
}
}
}
pub struct LazyGridScope<'a> {
slots: &'a mut Vec<LazySlot>,
}
impl<'a> LazyGridScope<'a> {
pub fn items<F>(&mut self, count: usize, builder: F)
where
F: Fn(usize) + Send + Sync + 'static,
{
self.slots.push(LazySlot::items(count, builder, None));
}
pub fn items_with_key<K, KF, F>(&mut self, count: usize, key_provider: KF, builder: F)
where
K: Hash,
KF: Fn(usize) -> K + Send + Sync + 'static,
F: Fn(usize) + Send + Sync + 'static,
{
let key_provider = Arc::new(move |idx| {
let key = key_provider(idx);
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
hasher.finish()
});
self.slots
.push(LazySlot::items(count, builder, Some(key_provider)));
}
pub fn item<F>(&mut self, builder: F)
where
F: Fn() + Send + Sync + 'static,
{
self.items(1, move |_| {
builder();
});
}
pub fn items_from_iter<I, T, F>(&mut self, iter: I, builder: F)
where
I: IntoIterator<Item = T>,
T: Send + Sync + 'static,
F: Fn(usize, &T) + Send + Sync + 'static,
{
let items: Arc<Vec<T>> = Arc::new(iter.into_iter().collect());
if items.is_empty() {
return;
}
let builder = Arc::new(builder);
let count = items.len();
self.slots.push(LazySlot::items(
count,
{
let items = items.clone();
let builder = builder.clone();
move |idx| {
if let Some(item) = items.get(idx) {
builder(idx, item);
}
}
},
None,
));
}
pub fn items_from_iter_with_key<I, T, K, KF, F>(
&mut self,
iter: I,
key_provider: KF,
builder: F,
) where
I: IntoIterator<Item = T>,
T: Send + Sync + 'static,
K: Hash,
KF: Fn(usize, &T) -> K + Send + Sync + 'static,
F: Fn(usize, &T) + Send + Sync + 'static,
{
let items: Arc<Vec<T>> = Arc::new(iter.into_iter().collect());
if items.is_empty() {
return;
}
let builder = Arc::new(builder);
let key_provider = Arc::new(key_provider);
let count = items.len();
let slot_builder = {
let items = items.clone();
let builder = builder.clone();
move |idx: usize| {
if let Some(item) = items.get(idx) {
builder(idx, item);
}
}
};
let slot_key_provider = {
let items = items.clone();
let key_provider = key_provider.clone();
move |idx: usize| -> u64 {
if let Some(item) = items.get(idx) {
let key = key_provider(idx, item);
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
hasher.finish()
} else {
0
}
}
};
self.slots.push(LazySlot::items(
count,
slot_builder,
Some(Arc::new(slot_key_provider)),
));
}
pub fn items_from_iter_values<I, T, F>(&mut self, iter: I, builder: F)
where
I: IntoIterator<Item = T>,
T: Send + Sync + 'static,
F: Fn(&T) + Send + Sync + 'static,
{
self.items_from_iter(iter, move |_, item| builder(item));
}
}
pub type LazyVerticalGridScope<'a> = LazyGridScope<'a>;
pub type LazyHorizontalGridScope<'a> = LazyGridScope<'a>;
#[tessera]
pub fn lazy_vertical_grid<F>(args: LazyVerticalGridArgs, configure: F)
where
F: FnOnce(&mut LazyVerticalGridScope),
{
let controller = remember(LazyGridController::new);
lazy_vertical_grid_with_controller(args, controller, configure);
}
#[tessera]
pub fn lazy_vertical_grid_with_controller<F>(
args: LazyVerticalGridArgs,
controller: State<LazyGridController>,
configure: F,
) where
F: FnOnce(&mut LazyVerticalGridScope),
{
let mut slots = Vec::new();
{
let mut scope = LazyVerticalGridScope { slots: &mut slots };
configure(&mut scope);
}
let mut scrollable_args = args.scrollable.clone();
scrollable_args.vertical = true;
scrollable_args.horizontal = false;
let view_args = LazyGridViewArgs {
axis: LazyGridAxis::Vertical,
grid_cells: args.columns,
main_axis_spacing: sanitize_spacing(Px::from(args.main_axis_spacing)),
cross_axis_spacing: sanitize_spacing(Px::from(args.cross_axis_spacing)),
cross_axis_alignment: args.cross_axis_alignment,
item_alignment: args.item_alignment,
estimated_line_main: ensure_positive_px(Px::from(args.estimated_item_size)),
overscan: args.overscan,
max_viewport_main: args.max_viewport_main,
padding_main: sanitize_spacing(Px::from(args.content_padding)),
padding_cross: sanitize_spacing(Px::from(args.content_padding)),
};
let scroll_controller = remember(ScrollableController::default);
scrollable_with_controller(scrollable_args, scroll_controller, move || {
lazy_grid_view(view_args, controller, slots.clone(), scroll_controller);
});
}
#[tessera]
pub fn lazy_horizontal_grid<F>(args: LazyHorizontalGridArgs, configure: F)
where
F: FnOnce(&mut LazyHorizontalGridScope),
{
let controller = remember(LazyGridController::new);
lazy_horizontal_grid_with_controller(args, controller, configure);
}
#[tessera]
pub fn lazy_horizontal_grid_with_controller<F>(
args: LazyHorizontalGridArgs,
controller: State<LazyGridController>,
configure: F,
) where
F: FnOnce(&mut LazyHorizontalGridScope),
{
let mut slots = Vec::new();
{
let mut scope = LazyHorizontalGridScope { slots: &mut slots };
configure(&mut scope);
}
let mut scrollable_args = args.scrollable.clone();
scrollable_args.vertical = false;
scrollable_args.horizontal = true;
let view_args = LazyGridViewArgs {
axis: LazyGridAxis::Horizontal,
grid_cells: args.rows,
main_axis_spacing: sanitize_spacing(Px::from(args.main_axis_spacing)),
cross_axis_spacing: sanitize_spacing(Px::from(args.cross_axis_spacing)),
cross_axis_alignment: args.cross_axis_alignment,
item_alignment: args.item_alignment,
estimated_line_main: ensure_positive_px(Px::from(args.estimated_item_size)),
overscan: args.overscan,
max_viewport_main: args.max_viewport_main,
padding_main: sanitize_spacing(Px::from(args.content_padding)),
padding_cross: sanitize_spacing(Px::from(args.content_padding)),
};
let scroll_controller = remember(ScrollableController::default);
scrollable_with_controller(scrollable_args, scroll_controller, move || {
lazy_grid_view(view_args, controller, slots.clone(), scroll_controller);
});
}
#[derive(Clone)]
struct LazyGridViewArgs {
axis: LazyGridAxis,
grid_cells: GridCells,
main_axis_spacing: Px,
cross_axis_spacing: Px,
cross_axis_alignment: MainAxisAlignment,
item_alignment: CrossAxisAlignment,
estimated_line_main: Px,
overscan: usize,
max_viewport_main: Option<Px>,
padding_main: Px,
padding_cross: Px,
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
struct ZeroLayout;
impl LayoutSpec for ZeroLayout {
fn measure(
&self,
_input: &LayoutInput<'_>,
_output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
Ok(ComputedData::ZERO)
}
}
#[derive(Clone, PartialEq, Eq)]
struct VisibleGridLayoutItem {
line_index: usize,
slot_index: usize,
}
#[derive(Clone)]
struct LazyGridLayout {
axis: LazyGridAxis,
item_alignment: CrossAxisAlignment,
estimated_line_main: Px,
main_spacing: Px,
max_viewport_main: Option<Px>,
padding_main: Px,
padding_cross: Px,
viewport_limit: Px,
line_range: Range<usize>,
slots: GridSlots,
visible_items: Vec<VisibleGridLayoutItem>,
controller: State<LazyGridController>,
scroll_controller: State<ScrollableController>,
}
impl PartialEq for LazyGridLayout {
fn eq(&self, other: &Self) -> bool {
self.axis == other.axis
&& self.item_alignment == other.item_alignment
&& self.estimated_line_main == other.estimated_line_main
&& self.main_spacing == other.main_spacing
&& self.max_viewport_main == other.max_viewport_main
&& self.padding_main == other.padding_main
&& self.padding_cross == other.padding_cross
&& self.viewport_limit == other.viewport_limit
&& self.line_range == other.line_range
&& self.slots == other.slots
&& self.visible_items == other.visible_items
}
}
impl LayoutSpec for LazyGridLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
if input.children_ids().len() != self.visible_items.len() {
return Err(MeasurementError::MeasureFnFailed(
"Lazy grid measured child count mismatch".into(),
));
}
let mut measured_items = Vec::with_capacity(self.visible_items.len());
let line_count = self.line_range.end.saturating_sub(self.line_range.start);
let mut line_max = vec![Px::ZERO; line_count];
for (visible, child_id) in self.visible_items.iter().zip(input.children_ids().iter()) {
let cell_cross = self
.slots
.sizes
.get(visible.slot_index)
.copied()
.unwrap_or(Px::ZERO);
let child_constraint = self.axis.child_constraint(cell_cross, self.item_alignment);
let child_size = input.measure_child(*child_id, &child_constraint)?;
let line_idx = visible.line_index - self.line_range.start;
if let Some(line_value) = line_max.get_mut(line_idx) {
*line_value = (*line_value).max(self.axis.main(&child_size));
}
measured_items.push(MeasuredGridItem {
child_id: *child_id,
line_index: visible.line_index,
slot_index: visible.slot_index,
size: child_size,
});
}
let (placements, total_main) = self.controller.with_mut(|c| {
for (offset, line_main) in line_max.iter().enumerate() {
let line_index = self.line_range.start + offset;
c.cache
.record_line_measurement(line_index, *line_main, self.estimated_line_main);
}
let mut placements = Vec::with_capacity(measured_items.len());
for item in &measured_items {
let line_offset = c.cache.offset_for_line(
item.line_index,
self.estimated_line_main,
self.main_spacing,
);
let cell_cross = self
.slots
.sizes
.get(item.slot_index)
.copied()
.unwrap_or(Px::ZERO);
let cell_offset = compute_cell_offset(
cell_cross,
self.axis.cross(&item.size),
self.item_alignment,
);
let cross_offset = self.padding_cross
+ self
.slots
.positions
.get(item.slot_index)
.copied()
.unwrap_or(Px::ZERO)
+ cell_offset;
let position = self
.axis
.position(line_offset + self.padding_main, cross_offset);
placements.push(GridPlacement {
child_id: item.child_id,
position,
});
}
let total_main = c
.cache
.total_main_size(self.estimated_line_main, self.main_spacing);
Ok::<_, MeasurementError>((placements, total_main))
})?;
let total_main_with_padding = total_main + self.padding_main + self.padding_main;
let cross_with_padding = self.slots.cross_size + self.padding_cross + self.padding_cross;
let size = self
.axis
.pack_size(total_main_with_padding, cross_with_padding);
self.scroll_controller
.with_mut(|c| c.override_child_size(size));
let reported_main = clamp_reported_main(
self.axis,
input.parent_constraint(),
total_main_with_padding,
self.viewport_limit,
self.max_viewport_main,
);
for placement in placements {
output.place_child(placement.child_id, placement.position);
}
Ok(self.axis.pack_size(reported_main, cross_with_padding))
}
}
#[tessera]
fn lazy_grid_view(
view_args: LazyGridViewArgs,
controller: State<LazyGridController>,
slots: Vec<LazySlot>,
scroll_controller: State<ScrollableController>,
) {
let plan = LazySlotPlan::new(slots);
let total_count = plan.total_count();
let visible_size = scroll_controller.with(|s| s.visible_size());
let available_cross = view_args.axis.visible_cross(visible_size);
let available_cross = (available_cross - view_args.padding_cross * 2).max(Px::ZERO);
let grid_slots = resolve_grid_slots(
available_cross,
&view_args.grid_cells,
view_args.cross_axis_spacing,
view_args.cross_axis_alignment,
);
let slots_per_line = grid_slots.len();
controller.with_mut(|c| c.cache.set_item_count(total_count, slots_per_line));
let total_main = controller.with(|c| {
c.cache
.total_main_size(view_args.estimated_line_main, view_args.main_axis_spacing)
});
let total_main_with_padding = total_main + view_args.padding_main + view_args.padding_main;
let cross_with_padding =
grid_slots.cross_size + view_args.padding_cross + view_args.padding_cross;
scroll_controller.with_mut(|c| {
c.override_child_size(
view_args
.axis
.pack_size(total_main_with_padding, cross_with_padding),
);
});
let scroll_offset = view_args
.axis
.scroll_offset(scroll_controller.with(|s| s.child_position()));
let padding_main = view_args.padding_main;
let viewport_span = resolve_viewport_span(
view_args.axis.visible_span(visible_size),
view_args.estimated_line_main,
view_args.main_axis_spacing,
);
let viewport_span = (viewport_span - (padding_main * 2)).max(Px::ZERO);
let visible_plan = controller.with(|c| {
compute_visible_items(
&plan,
&c.cache,
total_count,
slots_per_line,
scroll_offset,
viewport_span,
view_args.overscan,
view_args.estimated_line_main,
view_args.main_axis_spacing,
)
});
if visible_plan.items.is_empty() {
layout(ZeroLayout);
return;
}
let viewport_limit = viewport_span + padding_main + padding_main;
let visible_layout_items = visible_plan
.items
.iter()
.map(|item| VisibleGridLayoutItem {
line_index: item.line_index,
slot_index: item.slot_index,
})
.collect();
layout(LazyGridLayout {
axis: view_args.axis,
item_alignment: view_args.item_alignment,
estimated_line_main: view_args.estimated_line_main,
main_spacing: view_args.main_axis_spacing,
max_viewport_main: view_args.max_viewport_main,
padding_main,
padding_cross: view_args.padding_cross,
viewport_limit,
line_range: visible_plan.line_range.clone(),
slots: grid_slots.clone(),
visible_items: visible_layout_items,
controller,
scroll_controller,
});
for child in visible_plan.items {
key(child.key_hash, || {
(child.builder)(child.local_index);
});
}
}
fn resolve_viewport_span(current: Px, estimated: Px, spacing: Px) -> Px {
if current > Px::ZERO {
current
} else {
let per_line = estimated + spacing;
px_mul(per_line, DEFAULT_VIEWPORT_LINES.max(1))
}
}
#[allow(clippy::too_many_arguments)]
fn compute_visible_items(
plan: &LazySlotPlan,
cache: &LazyGridCache,
total_count: usize,
slots_per_line: usize,
scroll_offset: Px,
viewport_span: Px,
overscan: usize,
estimated_line_main: Px,
spacing: Px,
) -> VisibleGridPlan {
let total_lines = cache.line_count();
if total_count == 0 || slots_per_line == 0 || total_lines == 0 {
return VisibleGridPlan::empty();
}
let mut start_line = cache.line_index_for_offset(scroll_offset, estimated_line_main, spacing);
let end_target = scroll_offset + viewport_span;
let mut end_line = cache.line_index_for_offset(end_target, estimated_line_main, spacing) + 1;
start_line = start_line.saturating_sub(overscan);
end_line = (end_line + overscan).min(total_lines);
if start_line >= end_line {
end_line = (start_line + 1).min(total_lines);
start_line = start_line.saturating_sub(1);
}
let mut items = Vec::new();
for line in start_line..end_line {
let start_index = line * slots_per_line;
let end_index = (start_index + slots_per_line).min(total_count);
for index in start_index..end_index {
if let Some((slot, local_index)) = plan.resolve(index) {
let key_hash = if let Some(provider) = &slot.key_provider {
provider(local_index)
} else {
let mut hasher = DefaultHasher::new();
index.hash(&mut hasher);
hasher.finish()
};
items.push(VisibleGridItem {
local_index,
line_index: line,
slot_index: index - start_index,
builder: slot.builder.clone(),
key_hash,
});
}
}
}
VisibleGridPlan {
items,
line_range: start_line..end_line,
}
}
fn clamp_reported_main(
axis: LazyGridAxis,
parent_constraint: ParentConstraint<'_>,
total_main: Px,
viewport_span: Px,
fallback_limit: Option<Px>,
) -> Px {
let viewport = viewport_span.max(Px::ZERO);
let mut report = total_main.min(viewport);
if let Some(max_value) = axis.constraint_max(parent_constraint).or(fallback_limit) {
report = report.min(max_value.max(Px::ZERO));
}
report
}
fn compute_cell_offset(cell_cross: Px, child_cross: Px, alignment: CrossAxisAlignment) -> Px {
match alignment {
CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => Px::ZERO,
CrossAxisAlignment::Center => (cell_cross - child_cross).max(Px::ZERO) / 2,
CrossAxisAlignment::End => (cell_cross - child_cross).max(Px::ZERO),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum LazyGridAxis {
Vertical,
Horizontal,
}
impl LazyGridAxis {
fn main(&self, size: &ComputedData) -> Px {
match self {
Self::Vertical => size.height,
Self::Horizontal => size.width,
}
}
fn cross(&self, size: &ComputedData) -> Px {
match self {
Self::Vertical => size.width,
Self::Horizontal => size.height,
}
}
fn pack_size(&self, main: Px, cross: Px) -> ComputedData {
match self {
Self::Vertical => ComputedData {
width: cross,
height: main,
},
Self::Horizontal => ComputedData {
width: main,
height: cross,
},
}
}
fn position(&self, main: Px, cross: Px) -> PxPosition {
match self {
Self::Vertical => PxPosition { x: cross, y: main },
Self::Horizontal => PxPosition { x: main, y: cross },
}
}
fn visible_span(&self, size: ComputedData) -> Px {
match self {
Self::Vertical => size.height,
Self::Horizontal => size.width,
}
}
fn visible_cross(&self, size: ComputedData) -> Px {
match self {
Self::Vertical => size.width,
Self::Horizontal => size.height,
}
}
fn scroll_offset(&self, position: PxPosition) -> Px {
match self {
Self::Vertical => (-position.y).max(Px::ZERO),
Self::Horizontal => (-position.x).max(Px::ZERO),
}
}
fn child_constraint(&self, cross: Px, alignment: CrossAxisAlignment) -> Constraint {
let cross = cross.max(Px::ZERO);
let cross_dim = match alignment {
CrossAxisAlignment::Stretch => DimensionValue::Fixed(cross),
_ => DimensionValue::Wrap {
min: None,
max: Some(cross),
},
};
match self {
Self::Vertical => Constraint::new(
cross_dim,
DimensionValue::Wrap {
min: None,
max: None,
},
),
Self::Horizontal => Constraint::new(
DimensionValue::Wrap {
min: None,
max: None,
},
cross_dim,
),
}
}
fn constraint_max(&self, constraint: ParentConstraint<'_>) -> Option<Px> {
match self {
Self::Vertical => constraint.height().get_max(),
Self::Horizontal => constraint.width().get_max(),
}
}
}
#[derive(Clone, PartialEq, Eq)]
struct GridSlots {
sizes: Vec<Px>,
positions: Vec<Px>,
cross_size: Px,
}
impl GridSlots {
fn len(&self) -> usize {
self.sizes.len()
}
}
fn resolve_grid_slots(
available_cross: Px,
grid_cells: &GridCells,
spacing: Px,
alignment: MainAxisAlignment,
) -> GridSlots {
let spacing = sanitize_spacing(spacing);
let sizes = grid_cells.resolve_slots(available_cross, spacing);
if sizes.is_empty() {
return GridSlots {
sizes: Vec::new(),
positions: Vec::new(),
cross_size: Px::ZERO,
};
}
let total_sizes = sizes.iter().copied().fold(Px::ZERO, |acc, size| acc + size);
let base_spacing = if sizes.len() > 1 {
px_mul(spacing, sizes.len().saturating_sub(1))
} else {
Px::ZERO
};
let content_size = total_sizes + base_spacing;
let available_space = (available_cross - content_size).max(Px::ZERO);
let (start_cross, extra_spacing) =
calculate_alignment_offsets(available_space, sizes.len(), alignment);
let gap = spacing + extra_spacing;
let mut positions = Vec::with_capacity(sizes.len());
let mut cursor = start_cross;
for (pos, size) in sizes.iter().enumerate() {
positions.push(cursor);
cursor += *size;
if pos + 1 < sizes.len() {
cursor += gap;
}
}
let cross_size =
positions.last().copied().unwrap_or(Px::ZERO) + sizes.last().copied().unwrap_or(Px::ZERO);
GridSlots {
sizes,
positions,
cross_size,
}
}
fn calculate_alignment_offsets(
available_space: Px,
count: usize,
alignment: MainAxisAlignment,
) -> (Px, Px) {
match alignment {
MainAxisAlignment::Start => (Px::ZERO, Px::ZERO),
MainAxisAlignment::Center => (available_space / 2, Px::ZERO),
MainAxisAlignment::End => (available_space, Px::ZERO),
MainAxisAlignment::SpaceEvenly => {
if count > 0 {
let s = available_space / (count as i32 + 1);
(s, s)
} else {
(Px::ZERO, Px::ZERO)
}
}
MainAxisAlignment::SpaceBetween => {
if count > 1 {
(Px::ZERO, available_space / (count as i32 - 1))
} else if count == 1 {
(available_space / 2, Px::ZERO)
} else {
(Px::ZERO, Px::ZERO)
}
}
MainAxisAlignment::SpaceAround => {
if count > 0 {
let s = available_space / (count as i32);
(s / 2, s)
} else {
(Px::ZERO, Px::ZERO)
}
}
}
}
#[derive(Clone)]
struct GridPlacement {
child_id: NodeId,
position: PxPosition,
}
#[derive(Clone)]
struct MeasuredGridItem {
child_id: NodeId,
line_index: usize,
slot_index: usize,
size: ComputedData,
}
#[derive(Clone)]
enum LazySlot {
Items(LazyItemsSlot),
}
impl LazySlot {
fn items<F>(
count: usize,
builder: F,
key_provider: Option<Arc<dyn Fn(usize) -> u64 + Send + Sync>>,
) -> Self
where
F: Fn(usize) + Send + Sync + 'static,
{
Self::Items(LazyItemsSlot {
count,
builder: Arc::new(builder),
key_provider,
})
}
fn len(&self) -> usize {
match self {
Self::Items(slot) => slot.count,
}
}
}
#[derive(Clone)]
struct LazyItemsSlot {
count: usize,
builder: Arc<dyn Fn(usize) + Send + Sync>,
key_provider: Option<Arc<dyn Fn(usize) -> u64 + Send + Sync>>,
}
#[derive(Clone)]
struct LazySlotPlan {
entries: Vec<LazySlotEntry>,
total_count: usize,
}
impl LazySlotPlan {
fn new(slots: Vec<LazySlot>) -> Self {
let mut entries = Vec::with_capacity(slots.len());
let mut cursor = 0;
for slot in slots {
let len = slot.len();
entries.push(LazySlotEntry {
start: cursor,
len,
slot,
});
cursor += len;
}
Self {
entries,
total_count: cursor,
}
}
fn total_count(&self) -> usize {
self.total_count
}
fn resolve(&self, index: usize) -> Option<(&LazyItemsSlot, usize)> {
self.entries.iter().find_map(|entry| {
if index >= entry.start && index < entry.start + entry.len {
let local_index = index - entry.start;
match &entry.slot {
LazySlot::Items(slot) => Some((slot, local_index)),
}
} else {
None
}
})
}
}
#[derive(Clone)]
struct LazySlotEntry {
start: usize,
len: usize,
slot: LazySlot,
}
#[derive(Clone)]
struct VisibleGridItem {
local_index: usize,
line_index: usize,
slot_index: usize,
builder: Arc<dyn Fn(usize) + Send + Sync>,
key_hash: u64,
}
#[derive(Clone)]
struct VisibleGridPlan {
items: Vec<VisibleGridItem>,
line_range: Range<usize>,
}
impl VisibleGridPlan {
fn empty() -> Self {
Self {
items: Vec::new(),
line_range: 0..0,
}
}
}
#[derive(Default)]
struct LazyGridCache {
total_items: usize,
slots_per_line: usize,
measured_line_main: Vec<Option<Px>>,
fenwick: FenwickTree,
}
impl LazyGridCache {
fn set_item_count(&mut self, count: usize, slots_per_line: usize) {
if self.total_items == count && self.slots_per_line == slots_per_line {
return;
}
self.total_items = count;
self.slots_per_line = slots_per_line.max(1);
let lines = line_count(count, self.slots_per_line);
self.measured_line_main = vec![None; lines];
self.fenwick.resize(lines);
}
fn line_count(&self) -> usize {
self.measured_line_main.len()
}
fn record_line_measurement(&mut self, index: usize, actual: Px, estimated: Px) {
if index >= self.measured_line_main.len() {
return;
}
let previous = self.measured_line_main[index];
if previous == Some(actual) {
return;
}
let prev_delta = previous.map(|val| val - estimated).unwrap_or(Px::ZERO);
let new_delta = actual - estimated;
let delta_change = new_delta - prev_delta;
self.measured_line_main[index] = Some(actual);
self.fenwick.update(index, delta_change);
}
fn offset_for_line(&self, index: usize, estimated: Px, spacing: Px) -> Px {
if self.measured_line_main.is_empty() {
return Px::ZERO;
}
let clamped = index.min(self.measured_line_main.len());
let spacing_contrib = px_mul(spacing, clamped);
let estimated_contrib = px_mul(estimated, clamped);
spacing_contrib + estimated_contrib + self.fenwick.prefix_sum(clamped)
}
fn total_main_size(&self, estimated: Px, spacing: Px) -> Px {
let line_count = self.measured_line_main.len();
if line_count == 0 {
return Px::ZERO;
}
let spacing_contrib = px_mul(spacing, line_count.saturating_sub(1));
let estimated_contrib = px_mul(estimated, line_count);
spacing_contrib + estimated_contrib + self.fenwick.prefix_sum(line_count)
}
fn line_index_for_offset(&self, offset: Px, estimated: Px, spacing: Px) -> usize {
if self.measured_line_main.is_empty() {
return 0;
}
let mut low = 0usize;
let mut high = self.measured_line_main.len();
while low < high {
let mid = (low + high) / 2;
if self.offset_for_line(mid, estimated, spacing) <= offset {
low = mid + 1;
} else {
high = mid;
}
}
low.saturating_sub(1)
.min(self.measured_line_main.len().saturating_sub(1))
}
}
#[derive(Default, Clone)]
struct FenwickTree {
data: Vec<i64>,
}
impl FenwickTree {
fn resize(&mut self, len: usize) {
self.data.clear();
self.data.resize(len + 1, 0);
}
fn update(&mut self, index: usize, delta: Px) {
if self.data.is_empty() {
return;
}
let mut i = index + 1;
let delta_i64 = delta.0 as i64;
while i < self.data.len() {
self.data[i] = self.data[i].saturating_add(delta_i64);
i += i & (!i + 1);
}
}
fn prefix_sum(&self, count: usize) -> Px {
if self.data.is_empty() {
return Px::ZERO;
}
let mut idx = count;
let mut sum = 0i64;
while idx > 0 {
sum = sum.saturating_add(self.data[idx]);
idx &= idx - 1;
}
px_from_i64(sum)
}
}
fn line_count(item_count: usize, slots_per_line: usize) -> usize {
if item_count == 0 || slots_per_line == 0 {
0
} else {
item_count.div_ceil(slots_per_line)
}
}
fn calculate_cells_cross_axis_size(available: Px, slot_count: usize, spacing: Px) -> Vec<Px> {
let slot_count = slot_count.max(1);
let spacing_total = px_mul(spacing, slot_count.saturating_sub(1));
let available_without_spacing = (available - spacing_total).max(Px::ZERO);
let base = available_without_spacing.0 / slot_count as i32;
let remainder = available_without_spacing.0 % slot_count as i32;
(0..slot_count)
.map(|index| {
let extra = if (index as i32) < remainder { 1 } else { 0 };
Px(base + extra)
})
.collect()
}
fn px_mul(px: Px, times: usize) -> Px {
if times == 0 {
return Px::ZERO;
}
px_from_i64(px.0 as i64 * times as i64)
}
fn px_from_i64(value: i64) -> Px {
if value > i64::from(i32::MAX) {
Px(i32::MAX)
} else if value < i64::from(i32::MIN) {
Px(i32::MIN)
} else {
Px(value as i32)
}
}
fn ensure_positive_px(px: Px) -> Px {
if px <= Px::ZERO { Px(1) } else { px }
}
fn sanitize_spacing(px: Px) -> Px {
if px < Px::ZERO { Px::ZERO } else { px }
}