use egui::{Color32, Frame, NumExt as _, Ui};
pub struct CardLayoutItem {
pub frame: Option<Frame>,
pub min_width: f32,
}
pub struct CardLayout {
items: Vec<CardLayoutItem>,
default_frame: Frame,
hover_fill: Option<Color32>,
all_rows_use_available_width: bool,
}
struct RowAssignment {
first_item: usize,
num_items: usize,
total_width: f32,
}
#[derive(Default, Debug, Clone)]
struct RowStats {
max_height: f32,
}
impl CardLayout {
pub fn uniform(num_items: usize, min_width: f32, frame: Frame) -> Self {
Self {
items: (0..num_items)
.map(|_| CardLayoutItem {
min_width,
frame: None,
})
.collect(),
default_frame: frame,
hover_fill: None,
all_rows_use_available_width: true,
}
}
pub fn new(items: Vec<CardLayoutItem>, default_frame: Frame) -> Self {
Self {
items,
default_frame,
hover_fill: None,
all_rows_use_available_width: true,
}
}
pub fn all_rows_use_available_width(mut self, value: bool) -> Self {
self.all_rows_use_available_width = value;
self
}
pub fn hover_fill(mut self, color: Color32) -> Self {
self.hover_fill = Some(color);
self
}
pub fn show(self, ui: &mut Ui, mut show_item: impl FnMut(&mut Ui, usize, bool)) {
let Self {
items,
default_frame,
hover_fill,
all_rows_use_available_width,
} = self;
if items.is_empty() {
return;
}
re_tracing::profile_function!();
let available_width = ui.available_width();
let item_spacing = ui.spacing().item_spacing;
let rows = Self::assign_items_to_rows(&items, available_width, item_spacing.x);
let stats_id = ui.id().with("card_layout_row");
let mut last_known_height = 100.0;
let row_heights: Vec<f32> = (0..rows.len())
.map(|i| {
let h = ui
.data(|d| d.get_temp::<RowStats>(stats_id.with(i)))
.map_or(last_known_height, |s| s.max_height);
last_known_height = h;
h
})
.collect();
let total_height =
row_heights.iter().sum::<f32>() + item_spacing.y * rows.len().saturating_sub(1) as f32;
let (full_rect, _) = ui.allocate_exact_size(
egui::vec2(available_width, total_height.at_least(0.0)),
egui::Sense::hover(),
);
let visible = ui.clip_rect();
let mut row_y = full_rect.min.y;
for (row_idx, (row, row_height)) in rows.iter().zip(row_heights.iter()).enumerate() {
if row_y > visible.max.y {
break; }
if row_y + row_height < visible.min.y {
row_y += row_height + item_spacing.y;
ui.skip_ahead_auto_ids(row.num_items);
continue;
}
let gap_space = item_spacing.x * (row.num_items - 1) as f32;
let gap_space_item = gap_space / row.num_items as f32;
let is_last_row = row_idx + 1 == rows.len();
let item_growth = if !all_rows_use_available_width && is_last_row && rows.len() > 1 {
available_width / rows[0].total_width
} else {
available_width / row.total_width
};
let mut card_x = full_rect.min.x;
let mut new_row_stats = RowStats::default();
for i in 0..row.num_items {
let item = &items[row.first_item + i];
let frame = item.frame.unwrap_or(default_frame);
let frame_margin = frame.inner_margin.sum();
let card_width =
(item_growth * item.min_width - gap_space_item).at_most(available_width);
let card_rect = egui::Rect::from_min_size(
egui::pos2(card_x, row_y),
egui::vec2(card_width, *row_height),
);
let mut child_ui = ui.new_child(
egui::UiBuilder::new()
.max_rect(card_rect)
.layout(egui::Layout::left_to_right(egui::Align::Min)),
);
let card_hovered = hover_fill.is_some()
&& child_ui.ctx().dragged_id().is_none()
&& child_ui
.ctx()
.rect_contains_pointer(child_ui.layer_id(), card_rect);
let frame = if let (true, Some(fill)) = (card_hovered, hover_fill) {
frame.fill(fill)
} else {
frame
};
let mut content_height = 0.0;
frame.show(&mut child_ui, |ui| {
ui.set_width((card_width - frame_margin.x).at_most(ui.available_width()));
show_item(ui, row.first_item + i, card_hovered);
content_height = ui.min_size().y;
ui.set_height((row_height - frame_margin.y).at_least(0.0));
});
new_row_stats.max_height = new_row_stats
.max_height
.max(content_height + frame_margin.y);
card_x += card_width + item_spacing.x;
}
ui.data_mut(|d| d.insert_temp(stats_id.with(row_idx), new_row_stats));
row_y += row_height + item_spacing.y;
}
}
fn assign_items_to_rows(
items: &[CardLayoutItem],
available_width: f32,
item_spacing: f32,
) -> Vec<RowAssignment> {
let mut idx = 0;
std::iter::from_fn(|| {
if idx >= items.len() {
return None;
}
let first_item = idx;
let mut total_width = 0.0;
let mut count = 0;
while idx < items.len() {
let spacing = item_spacing * (count + 1) as f32; let needed = total_width + items[idx].min_width + spacing;
if needed > available_width && count > 0 {
break;
}
total_width += items[idx].min_width;
count += 1;
idx += 1;
}
Some(RowAssignment {
first_item,
num_items: idx - first_item,
total_width,
})
})
.collect()
}
}