1pub mod card;
7pub use card::MacCard;
8
9use crate::theme::MacTheme;
10use egui::{CornerRadius, Pos2, Rect, Response, Stroke, StrokeKind, Ui, Vec2};
11
12#[derive(Default)]
37pub struct MacPanel<'a> {
38 title: Option<String>,
39 theme: Option<&'a MacTheme>,
40 rounding: Option<u8>,
41}
42
43impl<'a> MacPanel<'a> {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
51 self.theme = Some(theme);
52 self
53 }
54
55 pub fn title(mut self, title: impl Into<String>) -> Self {
57 self.title = Some(title.into());
58 self
59 }
60
61 pub fn rounding(mut self, r: u8) -> Self {
63 self.rounding = Some(r);
64 self
65 }
66
67 pub fn show<R>(
69 self,
70 ui: &mut Ui,
71 rect: Rect,
72 add_contents: impl FnOnce(&mut Ui) -> R,
73 ) -> (Response, R) {
74 let fallback_theme;
75 let theme = match self.theme {
76 Some(t) => t,
77 None => {
78 fallback_theme = MacTheme::default();
79 &fallback_theme
80 }
81 };
82
83 let popover_metrics = theme.popover_metrics();
84 let rounding = self
85 .rounding
86 .unwrap_or_else(|| popover_metrics.corner_radius.points() as u8);
87
88 let bg_color = theme.window_background();
89 let border_stroke = Stroke::new(popover_metrics.border_width.points(), theme.separator());
90
91 let painter = ui.painter_at(rect);
92 painter.rect(
93 rect,
94 CornerRadius::same(rounding),
95 bg_color,
96 border_stroke,
97 StrokeKind::Inside,
98 );
99
100 let header_height = popover_metrics.header_height.points();
101
102 let content_rect = if let Some(title) = self.title {
103 let title_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), header_height));
104 painter.line_segment(
105 [
106 Pos2::new(rect.min.x, rect.min.y + header_height),
107 Pos2::new(rect.max.x, rect.min.y + header_height),
108 ],
109 border_stroke,
110 );
111 painter.text(
112 title_rect.center(),
113 egui::Align2::CENTER_CENTER,
114 title,
115 egui::FontId::proportional(popover_metrics.header_font_size.points()),
116 theme.label(),
117 );
118 Rect::from_min_max(Pos2::new(rect.min.x, rect.min.y + header_height), rect.max)
119 } else {
120 rect
121 };
122
123 let mut child_ui = ui.new_child(
124 egui::UiBuilder::new()
125 .max_rect(content_rect)
126 .layout(egui::Layout::top_down(egui::Align::Min)),
127 );
128 let ret = add_contents(&mut child_ui);
129 let resp = ui.interact(
130 rect,
131 ui.scope_id().with("mac_panel"),
132 egui::Sense::click_and_drag(),
133 );
134 (resp, ret)
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub enum MacPopoverArrowEdge {
141 #[default]
143 Top,
144 Bottom,
146 Left,
148 Right,
150 None,
152}
153
154pub struct MacPopover<'a> {
179 theme: Option<&'a MacTheme>,
180 arrow_edge: MacPopoverArrowEdge,
181 arrow_offset: Option<f32>,
182 scale: f32,
183 alpha: f32,
184}
185
186impl<'a> Default for MacPopover<'a> {
187 fn default() -> Self {
188 Self {
189 theme: None,
190 arrow_edge: MacPopoverArrowEdge::Top,
191 arrow_offset: None,
192 scale: 1.0,
193 alpha: 1.0,
194 }
195 }
196}
197
198impl<'a> MacPopover<'a> {
199 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
206 self.theme = Some(theme);
207 self
208 }
209
210 pub fn arrow_edge(mut self, edge: MacPopoverArrowEdge) -> Self {
212 self.arrow_edge = edge;
213 self
214 }
215
216 pub fn arrow_offset(mut self, offset: f32) -> Self {
218 self.arrow_offset = Some(offset);
219 self
220 }
221
222 pub fn scale(mut self, scale: f32) -> Self {
224 self.scale = scale;
225 self
226 }
227
228 pub fn alpha(mut self, alpha: f32) -> Self {
230 self.alpha = alpha;
231 self
232 }
233
234 pub fn show<R>(
236 self,
237 ui: &mut Ui,
238 rect: Rect,
239 add_contents: impl FnOnce(&mut Ui) -> R,
240 ) -> (Response, R) {
241 let fallback_theme;
242 let theme = match self.theme {
243 Some(t) => t,
244 None => {
245 fallback_theme = MacTheme::default();
246 &fallback_theme
247 }
248 };
249
250 let metrics = theme.popover_metrics();
251 let corner_radius = metrics.corner_radius.points();
252 let border_width = metrics.border_width.points();
253 let arrow_w = metrics.arrow_base_width.points();
254 let arrow_h = metrics.arrow_height.points();
255
256 let center = rect.center();
258 let scaled_size = rect.size() * self.scale.max(f32::EPSILON);
259 let active_rect = Rect::from_center_size(center, scaled_size);
260
261 let bg_color = theme.window_background().gamma_multiply(self.alpha);
262 let border_color = theme.separator().gamma_multiply(self.alpha);
263 let stroke = Stroke::new(border_width, border_color);
264
265 let painter = ui.painter_at(active_rect);
266
267 painter.rect(
269 active_rect,
270 CornerRadius::same(corner_radius as u8),
271 bg_color,
272 stroke,
273 StrokeKind::Inside,
274 );
275
276 if self.arrow_edge != MacPopoverArrowEdge::None {
278 let mut arrow_points = [Pos2::ZERO; 3];
279 match self.arrow_edge {
280 MacPopoverArrowEdge::Top => {
281 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
282 arrow_points = [
283 Pos2::new(ax - arrow_w * 0.5, active_rect.min.y),
284 Pos2::new(ax, active_rect.min.y - arrow_h),
285 Pos2::new(ax + arrow_w * 0.5, active_rect.min.y),
286 ];
287 }
288 MacPopoverArrowEdge::Bottom => {
289 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
290 arrow_points = [
291 Pos2::new(ax - arrow_w * 0.5, active_rect.max.y),
292 Pos2::new(ax, active_rect.max.y + arrow_h),
293 Pos2::new(ax + arrow_w * 0.5, active_rect.max.y),
294 ];
295 }
296 MacPopoverArrowEdge::Left => {
297 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
298 arrow_points = [
299 Pos2::new(active_rect.min.x, ay - arrow_w * 0.5),
300 Pos2::new(active_rect.min.x - arrow_h, ay),
301 Pos2::new(active_rect.min.x, ay + arrow_w * 0.5),
302 ];
303 }
304 MacPopoverArrowEdge::Right => {
305 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
306 arrow_points = [
307 Pos2::new(active_rect.max.x, ay - arrow_w * 0.5),
308 Pos2::new(active_rect.max.x + arrow_h, ay),
309 Pos2::new(active_rect.max.x, ay + arrow_w * 0.5),
310 ];
311 }
312 MacPopoverArrowEdge::None => {}
313 }
314
315 if self.arrow_edge != MacPopoverArrowEdge::None {
316 painter.add(egui::Shape::convex_polygon(
317 arrow_points.to_vec(),
318 bg_color,
319 Stroke::NONE,
320 ));
321 painter.line_segment([arrow_points[0], arrow_points[1]], stroke);
323 painter.line_segment([arrow_points[1], arrow_points[2]], stroke);
324 painter.line_segment(
326 [arrow_points[0], arrow_points[2]],
327 Stroke::new(border_width * 2.0, bg_color),
328 );
329 }
330 }
331
332 let mut child_ui = ui.new_child(
333 egui::UiBuilder::new()
334 .max_rect(active_rect.shrink(corner_radius.min(metrics.inner_padding.points())))
335 .layout(egui::Layout::top_down(egui::Align::Min)),
336 );
337 let ret = add_contents(&mut child_ui);
338 let resp = ui.interact(
339 active_rect,
340 ui.scope_id().with("mac_popover"),
341 egui::Sense::click_and_drag(),
342 );
343 (resp, ret)
344 }
345}