1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
use egui::{Color32, Frame, NumExt as _, Ui};
/// Per-item configuration for [`CardLayout`].
pub struct CardLayoutItem {
/// Frame drawn around this card. If `None`, uses the [`CardLayout`]'s default frame.
pub frame: Option<Frame>,
pub min_width: f32,
}
/// A virtualized card layout that arranges items in a responsive grid.
///
/// Items are laid out left-to-right, wrapping into rows. Each row is as wide as the
/// available space, with items growing proportionally from their `min_width`.
/// Only rows that intersect the visible (clip) rectangle are rendered;
/// row heights are measured each frame and cached for the next frame's layout.
pub struct CardLayout {
items: Vec<CardLayoutItem>,
default_frame: Frame,
hover_fill: Option<Color32>,
all_rows_use_available_width: bool,
}
/// Pre-computed assignment of items to a single row.
struct RowAssignment {
first_item: usize,
num_items: usize,
total_width: f32,
}
#[derive(Default, Debug, Clone)]
struct RowStats {
max_height: f32,
}
impl CardLayout {
/// Create a layout where every card has the same minimum width and frame.
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,
}
}
/// Create a layout with per-item configuration and a shared default frame.
pub fn new(items: Vec<CardLayoutItem>, default_frame: Frame) -> Self {
Self {
items,
default_frame,
hover_fill: None,
all_rows_use_available_width: true,
}
}
/// Whether all rows stretch to fill the available width (default: `true`).
///
/// When set to `false`, cards on the last row keep the same width
/// they would have on a full row.
pub fn all_rows_use_available_width(mut self, value: bool) -> Self {
self.all_rows_use_available_width = value;
self
}
/// Set a fill color used for hovered cards (replaces the default frame fill).
///
/// The card's frame fill is swapped to this color when the pointer is over the card.
pub fn hover_fill(mut self, color: Color32) -> Self {
self.hover_fill = Some(color);
self
}
/// Render the grid.
///
/// `show_item` receives `(ui, item_index, card_hovered)`. The `card_hovered`
/// flag is `true` when the pointer is over the card, which lets content
/// (e.g. a flag button) adapt its appearance based on parent hover state.
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;
// Assign items to rows based on available width.
let rows = Self::assign_items_to_rows(&items, available_width, item_spacing.x);
// Read cached row heights from previous frame.
// For rows without cached data, use the nearest known row height (or 100 as a last resort).
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();
// Reserve full content height so the scrollbar is correct.
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 std::iter::zip(&rows, &row_heights).enumerate() {
// Skip rows outside the visible area.
if row_y > visible.max.y {
break; // Done!
}
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 {
// Use the first row's growth factor so last-row cards
// stay the same width as cards on full rows.
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)),
);
// Check hover on the pre-computed card rect *before* painting.
// This replicates the logic of `container_hovered()`: pointer is
// inside the rect on this layer and nothing is being dragged.
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; // +1 to account for spacing to the right of the card.
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()
}
}