1use crate::layout::subpixel_snap;
10use crate::theme::MacTheme;
11use egui::{FontId, Pos2, Rect, Stroke, Ui, UiBuilder, Vec2};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum MacStackOrientation {
16 #[default]
18 Vertical,
19 Horizontal,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum MacStackAlignment {
26 #[default]
28 Leading,
29 Center,
31 Trailing,
33}
34
35pub struct MacStackView<'a> {
66 orientation: MacStackOrientation,
67 alignment: MacStackAlignment,
68 theme: Option<&'a MacTheme>,
69 spacing: Option<f32>,
70 padding: Option<f32>,
71 show_separators: bool,
72 width: Option<f32>,
73 height: Option<f32>,
74}
75
76pub struct MacStackUi<'a> {
78 pub ui: &'a mut Ui,
80
81 theme: &'a MacTheme,
82 orientation: MacStackOrientation,
83 spacing: f32,
84 separator_thickness: f32,
85 show_separators: bool,
86 container_height: Option<f32>,
87 padding: f32,
88 item_count: usize,
89 section_count: usize,
90}
91
92#[derive(Debug, Clone)]
94pub struct MacStackViewOutput<R> {
95 pub inner: R,
97 pub rect: Rect,
99 pub item_count: usize,
101 pub section_count: usize,
103}
104
105impl<'a> MacStackUi<'a> {
106 pub fn add<R>(&mut self, item: impl FnOnce(&mut Ui) -> R) -> R {
108 if self.item_count > 0 {
109 if self.show_separators {
110 self.draw_separator();
111 } else {
112 self.advance_spacing();
113 }
114 }
115 self.item_count += 1;
116 item(self.ui)
117 }
118
119 pub fn add_section<R>(
121 &mut self,
122 header: Option<&str>,
123 content: impl FnOnce(&mut Ui) -> R,
124 ) -> R {
125 if self.item_count > 0 || self.section_count > 0 {
126 self.draw_separator();
127 }
128 self.section_count += 1;
129
130 if let Some(title) = header {
131 let label_color = self.theme.secondary_label();
132 let font_size = self.theme.stack_view().section_header_font_size.points();
133 let font_id = FontId::proportional(font_size);
134 self.ui.add(
135 egui::Label::new(egui::RichText::new(title).font(font_id).color(label_color))
136 .wrap_mode(egui::TextWrapMode::Extend),
137 );
138 self.advance_spacing();
139 }
140
141 self.item_count += 1;
142 content(self.ui)
143 }
144
145 pub fn separator(&mut self) {
147 self.draw_separator();
148 }
149
150 pub fn spacing(&mut self, pts: f32) {
152 self.ui.add_space(pts);
153 }
154
155 fn advance_spacing(&mut self) {
156 self.ui.add_space(self.spacing);
157 }
158
159 fn draw_separator(&mut self) {
160 self.ui.add_space(self.spacing * 0.5);
161 let scale = self.ui.pixels_per_point();
162 let th = self.separator_thickness;
163 let color = self.theme.separator();
164
165 match self.orientation {
166 MacStackOrientation::Vertical => {
167 let avail_w = self.ui.available_width();
168 let (rect, _) = self
169 .ui
170 .allocate_exact_size(Vec2::new(avail_w, th), egui::Sense::hover());
171 let y = subpixel_snap(rect.center().y, scale);
172 self.ui.painter().line_segment(
173 [Pos2::new(rect.left(), y), Pos2::new(rect.right(), y)],
174 Stroke::new(th, color),
175 );
176 }
177 MacStackOrientation::Horizontal => {
178 let avail_h = if let Some(ch) = self.container_height {
179 (ch - 2.0 * self.padding).max(1.0)
180 } else {
181 let current_h = self.ui.min_rect().height();
182 if current_h > 0.0 {
183 current_h
184 } else {
185 self.theme.stack_view().default_item_height.points()
186 }
187 };
188 let (rect, _) = self
189 .ui
190 .allocate_exact_size(Vec2::new(th, avail_h), egui::Sense::hover());
191 let x = subpixel_snap(rect.center().x, scale);
192 self.ui.painter().line_segment(
193 [Pos2::new(x, rect.top()), Pos2::new(x, rect.bottom())],
194 Stroke::new(th, color),
195 );
196 }
197 }
198 self.ui.add_space(self.spacing * 0.5);
199 }
200}
201
202impl<'a> MacStackView<'a> {
203 pub fn new(orientation: MacStackOrientation) -> Self {
205 Self {
206 orientation,
207 alignment: MacStackAlignment::Leading,
208 theme: None,
209 spacing: None,
210 padding: None,
211 show_separators: false,
212 width: None,
213 height: None,
214 }
215 }
216
217 pub fn vertical() -> Self {
219 Self::new(MacStackOrientation::Vertical)
220 }
221
222 pub fn horizontal() -> Self {
224 Self::new(MacStackOrientation::Horizontal)
225 }
226
227 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
229 self.theme = Some(theme);
230 self
231 }
232
233 pub fn alignment(mut self, alignment: MacStackAlignment) -> Self {
235 self.alignment = alignment;
236 self
237 }
238
239 pub fn spacing(mut self, spacing: f32) -> Self {
241 self.spacing = Some(spacing);
242 self
243 }
244
245 pub fn padding(mut self, padding: f32) -> Self {
247 self.padding = Some(padding);
248 self
249 }
250
251 pub fn show_separators(mut self, show: bool) -> Self {
253 self.show_separators = show;
254 self
255 }
256
257 pub fn width(mut self, width: f32) -> Self {
259 self.width = Some(width);
260 self
261 }
262
263 pub fn height(mut self, height: f32) -> Self {
265 self.height = Some(height);
266 self
267 }
268
269 pub fn show<R>(
271 self,
272 ui: &mut Ui,
273 add_contents: impl FnOnce(&mut MacStackUi<'_>) -> R,
274 ) -> MacStackViewOutput<R> {
275 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
276 let theme: &MacTheme = &theme_cow;
277
278 let metrics = theme.stack_view();
279 let spacing = self
280 .spacing
281 .unwrap_or_else(|| metrics.default_spacing.points());
282 let padding = self
283 .padding
284 .unwrap_or_else(|| metrics.edge_padding.points());
285 let separator_thickness = metrics.separator_thickness.points();
286
287 let available = ui.available_rect_before_wrap();
288 let target_w = self.width.unwrap_or(available.width());
289 let target_h = self.height.unwrap_or(available.height());
290 let container_rect = Rect::from_min_size(available.min, Vec2::new(target_w, target_h));
291
292 let inner_rect = container_rect.shrink(padding);
293
294 let stack_id = ui.next_auto_id();
295 let mut child_ui = match self.orientation {
296 MacStackOrientation::Vertical => {
297 let layout = egui::Layout::top_down(match self.alignment {
298 MacStackAlignment::Leading => egui::Align::Min,
299 MacStackAlignment::Center => egui::Align::Center,
300 MacStackAlignment::Trailing => egui::Align::Max,
301 });
302 ui.new_child(
303 UiBuilder::new()
304 .id_salt((stack_id, "stack_v"))
305 .max_rect(inner_rect)
306 .layout(layout),
307 )
308 }
309 MacStackOrientation::Horizontal => {
310 let layout = egui::Layout::left_to_right(match self.alignment {
311 MacStackAlignment::Leading => egui::Align::Min,
312 MacStackAlignment::Center => egui::Align::Center,
313 MacStackAlignment::Trailing => egui::Align::Max,
314 });
315 ui.new_child(
316 UiBuilder::new()
317 .id_salt((stack_id, "stack_h"))
318 .max_rect(inner_rect)
319 .layout(layout),
320 )
321 }
322 };
323
324 let mut stack_ui = MacStackUi {
325 ui: &mut child_ui,
326 theme,
327 orientation: self.orientation,
328 spacing,
329 separator_thickness,
330 show_separators: self.show_separators,
331 container_height: self.height,
332 padding,
333 item_count: 0,
334 section_count: 0,
335 };
336
337 let inner = add_contents(&mut stack_ui);
338 let items_rect = stack_ui.ui.min_rect();
339 let content_w = if items_rect.is_positive() {
340 items_rect.width() + padding * 2.0
341 } else {
342 padding * 2.0
343 };
344 let content_h = if items_rect.is_positive() {
345 items_rect.height() + padding * 2.0
346 } else {
347 padding * 2.0
348 };
349 let final_w = self.width.unwrap_or(content_w).max(content_w);
350 let final_h = self.height.unwrap_or(content_h).max(content_h);
351 let final_rect = Rect::from_min_size(container_rect.min, Vec2::new(final_w, final_h));
352
353 ui.allocate_rect(final_rect, egui::Sense::hover());
354
355 MacStackViewOutput {
356 inner,
357 rect: final_rect,
358 item_count: stack_ui.item_count,
359 section_count: stack_ui.section_count,
360 }
361 }
362}