1use crate::controls::MacInsets;
8use crate::layout::subpixel_snap_rect;
9use crate::theme::MacTheme;
10use egui::{
11 CornerRadius, Id, Key, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
12 UiBuilder, Vec2,
13};
14use std::collections::BTreeSet;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct MacCollectionViewFlowLayout {
19 pub item_size: Vec2,
21 pub minimum_line_spacing: f32,
23 pub minimum_interitem_spacing: f32,
25 pub section_inset: MacInsets,
27}
28
29impl Default for MacCollectionViewFlowLayout {
30 fn default() -> Self {
31 let theme = MacTheme::default();
32 let m = theme.collection_view();
33 let sz = m.item_size.points();
34 Self {
35 item_size: Vec2::new(sz, sz),
36 minimum_line_spacing: m.minimum_line_spacing.points(),
37 minimum_interitem_spacing: m.minimum_interitem_spacing.points(),
38 section_inset: MacInsets {
39 left: m.section_inset.points(),
40 right: m.section_inset.points(),
41 top: m.section_inset.points(),
42 bottom: m.section_inset.points(),
43 },
44 }
45 }
46}
47
48impl MacCollectionViewFlowLayout {
49 pub fn compute_column_count(&self, available_width: f32) -> usize {
53 let span = self.item_size.x + self.minimum_interitem_spacing;
54 if span <= 0.0 {
55 1
56 } else {
57 let cols = ((available_width + self.minimum_interitem_spacing) / span).floor();
58 (cols as usize).max(1)
59 }
60 }
61
62 pub fn compute_item_rect(&self, origin: Pos2, index: usize, columns: usize) -> Rect {
64 let cols = columns.max(1);
65 let col = index % cols;
66 let row = index / cols;
67
68 let x = origin.x
69 + self.section_inset.left
70 + (col as f32) * (self.item_size.x + self.minimum_interitem_spacing);
71 let y = origin.y
72 + self.section_inset.top
73 + (row as f32) * (self.item_size.y + self.minimum_line_spacing);
74
75 Rect::from_min_size(Pos2::new(x, y), self.item_size)
76 }
77}
78
79pub struct MacCollectionView<'a, T> {
103 items: &'a [T],
104 selected_indices: &'a mut BTreeSet<usize>,
105 layout: MacCollectionViewFlowLayout,
106 theme: Option<&'a MacTheme>,
107}
108
109impl<'a, T> MacCollectionView<'a, T> {
110 pub fn new(items: &'a [T], selected_indices: &'a mut BTreeSet<usize>) -> Self {
112 Self {
113 items,
114 selected_indices,
115 layout: MacCollectionViewFlowLayout::default(),
116 theme: None,
117 }
118 }
119
120 pub fn layout(mut self, layout: MacCollectionViewFlowLayout) -> Self {
122 self.layout = layout;
123 self
124 }
125
126 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
128 self.theme = Some(theme);
129 self
130 }
131
132 pub fn show_with_response<F>(self, ui: &mut Ui, mut render_item: F) -> (usize, Response)
134 where
135 F: FnMut(&mut Ui, &T),
136 {
137 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
138 let theme: &MacTheme = &theme_cow;
139 let metrics = theme.collection_view();
140
141 let available_w = ui.available_width();
142 let columns = self.layout.compute_column_count(available_w);
143 let item_count = self.items.len();
144 let rows = if item_count == 0 {
145 0
146 } else {
147 item_count.div_ceil(columns)
148 };
149
150 let total_h = self.layout.section_inset.top
151 + (rows as f32) * self.layout.item_size.y
152 + ((rows.saturating_sub(1)) as f32) * self.layout.minimum_line_spacing
153 + self.layout.section_inset.bottom;
154
155 let desired_size = Vec2::new(available_w, total_h.max(self.layout.item_size.y));
156 let (rect, mut response) =
157 ui.allocate_exact_size(desired_size, Sense::click_and_drag() | Sense::focusable_noninteractive());
158 let rect = subpixel_snap_rect(rect, ui.ctx().pixels_per_point());
159
160 if response.has_focus() && !self.items.is_empty() {
162 let last_selected = self.selected_indices.iter().copied().last().unwrap_or(0);
163 let mut next_selected = None;
164
165 if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowRight)) {
166 if last_selected + 1 < item_count {
167 next_selected = Some(last_selected + 1);
168 }
169 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowLeft)) {
170 if last_selected > 0 {
171 next_selected = Some(last_selected - 1);
172 }
173 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowDown)) {
174 if last_selected + columns < item_count {
175 next_selected = Some(last_selected + columns);
176 }
177 } else if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowUp)) && last_selected >= columns {
178 next_selected = Some(last_selected - columns);
179 }
180
181 if let Some(new_idx) = next_selected {
182 self.selected_indices.clear();
183 self.selected_indices.insert(new_idx);
184 response.mark_changed();
185 }
186 }
187
188 let clip = ui.clip_rect();
189 let painter = ui.painter_at(rect);
190 let sel_corner_r = metrics.selection_corner_radius.points();
191 let sel_stroke_w = metrics.selection_stroke_width.points();
192
193 let mut rendered_count = 0usize;
194
195 let row_pitch = self.layout.item_size.y + self.layout.minimum_line_spacing;
196 let (first_idx, last_idx) = if row_pitch <= 0.0 || rows == 0 {
197 (0, item_count)
198 } else {
199 let origin_y = rect.min.y + self.layout.section_inset.top;
200 let rel_top = (clip.min.y - origin_y).max(0.0);
201 let first_row = ((rel_top / row_pitch).floor() as usize).saturating_sub(1);
202 let rel_bottom = (clip.max.y - origin_y).max(0.0);
203 let last_row = (((rel_bottom / row_pitch).ceil() as usize) + 1).min(rows);
204 (
205 (first_row * columns).min(item_count),
206 (last_row * columns).min(item_count),
207 )
208 };
209
210 for (idx, item) in self.items[first_idx..last_idx]
211 .iter()
212 .enumerate()
213 .map(|(i, it)| (first_idx + i, it))
214 {
215 let item_rect = self.layout.compute_item_rect(rect.min, idx, columns);
216
217 if !clip.intersects(item_rect) {
219 continue;
220 }
221
222 rendered_count += 1;
223
224 let is_selected = self.selected_indices.contains(&idx);
225 let item_hovered = ui
226 .input(|inp| inp.pointer.hover_pos())
227 .is_some_and(|p| item_rect.contains(p));
228
229 if item_hovered && ui.input(|inp| inp.pointer.primary_clicked()) {
230 response.request_focus();
231 if !ui.input(|inp| inp.modifiers.shift || inp.modifiers.command) {
232 self.selected_indices.clear();
233 }
234 self.selected_indices.insert(idx);
235 response.mark_changed();
236 }
237
238 let bg_color = if is_selected {
240 theme.selected_control()
241 } else if item_hovered {
242 theme.control()
243 } else {
244 theme.control_background()
245 };
246
247 painter.rect(
248 item_rect,
249 CornerRadius::same(sel_corner_r as u8),
250 bg_color,
251 Stroke::new(1.0, theme.separator()),
252 StrokeKind::Inside,
253 );
254
255 if is_selected {
256 painter.rect_stroke(
257 item_rect,
258 CornerRadius::same(sel_corner_r as u8),
259 Stroke::new(sel_stroke_w, theme.control_accent()),
260 StrokeKind::Outside,
261 );
262 }
263
264 let mut child_ui = ui.new_child(
266 UiBuilder::new()
267 .id_salt(Id::NULL.with("coll_item").with(idx))
268 .max_rect(item_rect),
269 );
270 render_item(&mut child_ui, item);
271 }
272
273 if response.dragged() {
275 if let (Some(press_pos), Some(current_pos)) = (
276 ui.input(|i| i.pointer.press_origin()),
277 ui.input(|i| i.pointer.interact_pos()),
278 ) {
279 let marquee_rect = Rect::from_two_pos(press_pos, current_pos);
280 let fill = theme
281 .selected_control()
282 .gamma_multiply(metrics.marquee_alpha_fill.points());
283 let stroke = Stroke::new(
284 1.0,
285 theme
286 .control_accent()
287 .gamma_multiply(metrics.marquee_alpha_stroke.points()),
288 );
289
290 painter.rect(
291 marquee_rect,
292 CornerRadius::same(0),
293 fill,
294 stroke,
295 StrokeKind::Inside,
296 );
297
298 let (m_first_idx, m_last_idx) = if row_pitch <= 0.0 || rows == 0 {
300 (0, item_count)
301 } else {
302 let origin_y = rect.min.y + self.layout.section_inset.top;
303 let rel_top = (marquee_rect.min.y - origin_y).max(0.0);
304 let first_row = ((rel_top / row_pitch).floor() as usize).saturating_sub(1);
305 let rel_bottom = (marquee_rect.max.y - origin_y).max(0.0);
306 let last_row = (((rel_bottom / row_pitch).ceil() as usize) + 1).min(rows);
307 (
308 (first_row * columns).min(item_count),
309 (last_row * columns).min(item_count),
310 )
311 };
312
313 for idx in m_first_idx..m_last_idx {
314 let item_rect = self.layout.compute_item_rect(rect.min, idx, columns);
315 if marquee_rect.intersects(item_rect) {
316 self.selected_indices.insert(idx);
317 response.mark_changed();
318 }
319 }
320 }
321 }
322
323 (rendered_count, response)
324 }
325
326 pub fn show_with_item_tracker<F>(self, ui: &mut Ui, render_item: F) -> usize
328 where
329 F: FnMut(&mut Ui, &T),
330 {
331 self.show_with_response(ui, render_item).0
332 }
333
334 pub fn show(self, ui: &mut Ui) -> Response {
336 self.show_with_response(ui, |_ui, _item| {}).1
337 }
338}