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(
136 egui::RichText::new(title).font(font_id).color(label_color),
137 )
138 .wrap_mode(egui::TextWrapMode::Extend),
139 );
140 self.advance_spacing();
141 }
142
143 self.item_count += 1;
144 content(self.ui)
145 }
146
147 pub fn separator(&mut self) {
149 self.draw_separator();
150 }
151
152 pub fn spacing(&mut self, pts: f32) {
154 self.ui.add_space(pts);
155 }
156
157 fn advance_spacing(&mut self) {
158 self.ui.add_space(self.spacing);
159 }
160
161 fn draw_separator(&mut self) {
162 self.ui.add_space(self.spacing * 0.5);
163 let scale = self.ui.pixels_per_point();
164 let th = self.separator_thickness;
165 let color = self.theme.separator();
166
167 match self.orientation {
168 MacStackOrientation::Vertical => {
169 let avail_w = self.ui.available_width();
170 let (rect, _) = self
171 .ui
172 .allocate_exact_size(Vec2::new(avail_w, th), egui::Sense::hover());
173 let y = subpixel_snap(rect.center().y, scale);
174 self.ui.painter().line_segment(
175 [Pos2::new(rect.left(), y), Pos2::new(rect.right(), y)],
176 Stroke::new(th, color),
177 );
178 }
179 MacStackOrientation::Horizontal => {
180 let avail_h = if let Some(ch) = self.container_height {
181 (ch - 2.0 * self.padding).max(1.0)
182 } else {
183 let current_h = self.ui.min_rect().height();
184 if current_h > 0.0 {
185 current_h
186 } else {
187 self.theme.stack_view().default_item_height.points()
188 }
189 };
190 let (rect, _) = self
191 .ui
192 .allocate_exact_size(Vec2::new(th, avail_h), egui::Sense::hover());
193 let x = subpixel_snap(rect.center().x, scale);
194 self.ui.painter().line_segment(
195 [Pos2::new(x, rect.top()), Pos2::new(x, rect.bottom())],
196 Stroke::new(th, color),
197 );
198 }
199 }
200 self.ui.add_space(self.spacing * 0.5);
201 }
202}
203
204impl<'a> MacStackView<'a> {
205 pub fn new(orientation: MacStackOrientation) -> Self {
207 Self {
208 orientation,
209 alignment: MacStackAlignment::Leading,
210 theme: None,
211 spacing: None,
212 padding: None,
213 show_separators: false,
214 width: None,
215 height: None,
216 }
217 }
218
219 pub fn vertical() -> Self {
221 Self::new(MacStackOrientation::Vertical)
222 }
223
224 pub fn horizontal() -> Self {
226 Self::new(MacStackOrientation::Horizontal)
227 }
228
229 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
231 self.theme = Some(theme);
232 self
233 }
234
235 pub fn alignment(mut self, alignment: MacStackAlignment) -> Self {
237 self.alignment = alignment;
238 self
239 }
240
241 pub fn spacing(mut self, spacing: f32) -> Self {
243 self.spacing = Some(spacing);
244 self
245 }
246
247 pub fn padding(mut self, padding: f32) -> Self {
249 self.padding = Some(padding);
250 self
251 }
252
253 pub fn show_separators(mut self, show: bool) -> Self {
255 self.show_separators = show;
256 self
257 }
258
259 pub fn width(mut self, width: f32) -> Self {
261 self.width = Some(width);
262 self
263 }
264
265 pub fn height(mut self, height: f32) -> Self {
267 self.height = Some(height);
268 self
269 }
270
271 pub fn show<R>(
273 self,
274 ui: &mut Ui,
275 add_contents: impl FnOnce(&mut MacStackUi<'_>) -> R,
276 ) -> MacStackViewOutput<R> {
277 let fallback_theme;
278 let theme = match self.theme {
279 Some(t) => t,
280 None => {
281 fallback_theme = MacTheme::default();
282 &fallback_theme
283 }
284 };
285
286 let metrics = theme.stack_view();
287 let spacing = self
288 .spacing
289 .unwrap_or_else(|| metrics.default_spacing.points());
290 let padding = self
291 .padding
292 .unwrap_or_else(|| metrics.edge_padding.points());
293 let separator_thickness = metrics.separator_thickness.points();
294
295 let available = ui.available_rect_before_wrap();
296 let target_w = self.width.unwrap_or(available.width());
297 let target_h = self.height.unwrap_or(available.height());
298 let container_rect = Rect::from_min_size(available.min, Vec2::new(target_w, target_h));
299
300 let inner_rect = container_rect.shrink(padding);
301
302 let mut child_ui = match self.orientation {
303 MacStackOrientation::Vertical => {
304 let layout = egui::Layout::top_down(match self.alignment {
305 MacStackAlignment::Leading => egui::Align::Min,
306 MacStackAlignment::Center => egui::Align::Center,
307 MacStackAlignment::Trailing => egui::Align::Max,
308 });
309 ui.new_child(UiBuilder::new().max_rect(inner_rect).layout(layout))
310 }
311 MacStackOrientation::Horizontal => {
312 let layout = egui::Layout::left_to_right(match self.alignment {
313 MacStackAlignment::Leading => egui::Align::Min,
314 MacStackAlignment::Center => egui::Align::Center,
315 MacStackAlignment::Trailing => egui::Align::Max,
316 });
317 ui.new_child(UiBuilder::new().max_rect(inner_rect).layout(layout))
318 }
319 };
320
321 let mut stack_ui = MacStackUi {
322 ui: &mut child_ui,
323 theme,
324 orientation: self.orientation,
325 spacing,
326 separator_thickness,
327 show_separators: self.show_separators,
328 container_height: self.height,
329 padding,
330 item_count: 0,
331 section_count: 0,
332 };
333
334 let inner = add_contents(&mut stack_ui);
335 let items_rect = stack_ui.ui.min_rect();
336 let content_w = if items_rect.is_positive() {
337 items_rect.width() + padding * 2.0
338 } else {
339 padding * 2.0
340 };
341 let content_h = if items_rect.is_positive() {
342 items_rect.height() + padding * 2.0
343 } else {
344 padding * 2.0
345 };
346 let final_w = self.width.unwrap_or(content_w).max(content_w);
347 let final_h = self.height.unwrap_or(content_h).max(content_h);
348 let final_rect = Rect::from_min_size(container_rect.min, Vec2::new(final_w, final_h));
349
350 ui.allocate_rect(final_rect, egui::Sense::hover());
351
352 MacStackViewOutput {
353 inner,
354 rect: final_rect,
355 item_count: stack_ui.item_count,
356 section_count: stack_ui.section_count,
357 }
358 }
359}