1pub mod card;
7pub mod hud;
8pub use card::MacCard;
9pub use hud::MacFloatingInfoBox;
10
11use crate::theme::MacTheme;
12use egui::{CornerRadius, Id, Pos2, Rect, Response, Stroke, StrokeKind, Ui, Vec2};
13use std::hash::Hash;
14
15#[derive(Default)]
40pub struct MacPanel<'a> {
41 id: Option<Id>,
42 title: Option<String>,
43 theme: Option<&'a MacTheme>,
44 rounding: Option<u8>,
45}
46
47impl<'a> MacPanel<'a> {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn id(mut self, id_source: impl Hash + std::fmt::Debug) -> Self {
55 self.id = Some(Id::NULL.with(id_source));
56 self
57 }
58
59 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
61 self.theme = Some(theme);
62 self
63 }
64
65 pub fn title(mut self, title: impl Into<String>) -> Self {
67 self.title = Some(title.into());
68 self
69 }
70
71 pub fn rounding(mut self, r: u8) -> Self {
73 self.rounding = Some(r);
74 self
75 }
76
77 pub fn show<R>(
79 self,
80 ui: &mut Ui,
81 rect: Rect,
82 add_contents: impl FnOnce(&mut Ui) -> R,
83 ) -> (Response, R) {
84 let fallback_theme;
85 let theme = match self.theme {
86 Some(t) => t,
87 None => {
88 fallback_theme = MacTheme::from_ctx(ui.ctx());
89 &fallback_theme
90 }
91 };
92
93 let panel_id = self.id.unwrap_or_else(|| {
94 ui.scope_id().with((
95 "mac_panel",
96 self.title.as_deref().unwrap_or(""),
97 rect.min.x.to_bits(),
98 rect.min.y.to_bits(),
99 ))
100 });
101 let resp = ui.interact(rect, panel_id, egui::Sense::click_and_drag());
102 if ui.rect_contains_pointer(rect) {
103 ui.input_mut(|i| {
104 i.smooth_scroll_delta = Vec2::ZERO;
105 });
106 }
107
108 let popover_metrics = theme.popover_metrics();
109 let rounding = self
110 .rounding
111 .unwrap_or_else(|| popover_metrics.corner_radius.points() as u8);
112
113 let bg_color = theme.window_background();
114 let border_stroke = Stroke::new(popover_metrics.border_width.points(), theme.separator());
115
116 let painter = ui.painter_at(rect);
117 painter.rect(
118 rect,
119 CornerRadius::same(rounding),
120 bg_color,
121 border_stroke,
122 StrokeKind::Inside,
123 );
124
125 let header_height = popover_metrics.header_height.points();
126
127 let content_rect = if let Some(title) = self.title {
128 let title_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), header_height));
129 painter.line_segment(
130 [
131 Pos2::new(rect.min.x, rect.min.y + header_height),
132 Pos2::new(rect.max.x, rect.min.y + header_height),
133 ],
134 border_stroke,
135 );
136 painter.text(
137 title_rect.center(),
138 egui::Align2::CENTER_CENTER,
139 title,
140 egui::FontId::proportional(popover_metrics.header_font_size.points()),
141 theme.label(),
142 );
143 Rect::from_min_max(Pos2::new(rect.min.x, rect.min.y + header_height), rect.max)
144 } else {
145 rect
146 };
147
148 let mut child_ui = ui.new_child(
149 egui::UiBuilder::new()
150 .max_rect(content_rect)
151 .layout(egui::Layout::top_down(egui::Align::Min)),
152 );
153 let ret = add_contents(&mut child_ui);
154 (resp, ret)
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
160pub enum MacPopoverArrowEdge {
161 #[default]
163 Top,
164 Bottom,
166 Left,
168 Right,
170 None,
172}
173
174pub struct MacPopover<'a> {
199 id: Option<Id>,
200 theme: Option<&'a MacTheme>,
201 arrow_edge: MacPopoverArrowEdge,
202 arrow_offset: Option<f32>,
203 scale: f32,
204 alpha: f32,
205}
206
207impl<'a> Default for MacPopover<'a> {
208 fn default() -> Self {
209 Self {
210 id: None,
211 theme: None,
212 arrow_edge: MacPopoverArrowEdge::Top,
213 arrow_offset: None,
214 scale: 1.0,
215 alpha: 1.0,
216 }
217 }
218}
219
220impl<'a> MacPopover<'a> {
221 pub fn new() -> Self {
223 Self::default()
224 }
225
226 pub fn id(mut self, id_source: impl Hash + std::fmt::Debug) -> Self {
228 self.id = Some(Id::NULL.with(id_source));
229 self
230 }
231
232 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
234 self.theme = Some(theme);
235 self
236 }
237
238 pub fn arrow_edge(mut self, edge: MacPopoverArrowEdge) -> Self {
240 self.arrow_edge = edge;
241 self
242 }
243
244 pub fn arrow_offset(mut self, offset: f32) -> Self {
246 self.arrow_offset = Some(offset);
247 self
248 }
249
250 pub fn scale(mut self, scale: f32) -> Self {
252 self.scale = scale;
253 self
254 }
255
256 pub fn alpha(mut self, alpha: f32) -> Self {
258 self.alpha = alpha;
259 self
260 }
261
262 pub fn show<R>(
264 self,
265 ui: &mut Ui,
266 rect: Rect,
267 add_contents: impl FnOnce(&mut Ui) -> R,
268 ) -> (Response, R) {
269 let fallback_theme;
270 let theme = match self.theme {
271 Some(t) => t,
272 None => {
273 fallback_theme = MacTheme::from_ctx(ui.ctx());
274 &fallback_theme
275 }
276 };
277
278 let metrics = theme.popover_metrics();
279 let corner_radius = metrics.corner_radius.points();
280 let border_width = metrics.border_width.points();
281 let arrow_w = metrics.arrow_base_width.points();
282 let arrow_h = metrics.arrow_height.points();
283
284 let center = rect.center();
286 let scaled_size = rect.size() * self.scale.max(f32::EPSILON);
287 let active_rect = Rect::from_center_size(center, scaled_size);
288
289 let popover_id = self.id.unwrap_or_else(|| {
290 ui.scope_id().with((
291 "mac_popover",
292 rect.min.x.to_bits(),
293 rect.min.y.to_bits(),
294 self.arrow_edge as u8,
295 ))
296 });
297 let resp = ui.interact(active_rect, popover_id, egui::Sense::click_and_drag());
298 if ui.rect_contains_pointer(active_rect) {
299 ui.input_mut(|i| {
300 i.smooth_scroll_delta = Vec2::ZERO;
301 });
302 }
303
304 let bg_color = theme.window_background().gamma_multiply(self.alpha);
305 let border_color = theme.separator().gamma_multiply(self.alpha);
306 let stroke = Stroke::new(border_width, border_color);
307
308 let painter = ui.painter_at(active_rect);
309
310 painter.rect(
312 active_rect,
313 CornerRadius::same(corner_radius as u8),
314 bg_color,
315 stroke,
316 StrokeKind::Inside,
317 );
318
319 if self.arrow_edge != MacPopoverArrowEdge::None {
321 let mut arrow_points = [Pos2::ZERO; 3];
322 match self.arrow_edge {
323 MacPopoverArrowEdge::Top => {
324 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
325 arrow_points = [
326 Pos2::new(ax - arrow_w * 0.5, active_rect.min.y),
327 Pos2::new(ax, active_rect.min.y - arrow_h),
328 Pos2::new(ax + arrow_w * 0.5, active_rect.min.y),
329 ];
330 }
331 MacPopoverArrowEdge::Bottom => {
332 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
333 arrow_points = [
334 Pos2::new(ax - arrow_w * 0.5, active_rect.max.y),
335 Pos2::new(ax, active_rect.max.y + arrow_h),
336 Pos2::new(ax + arrow_w * 0.5, active_rect.max.y),
337 ];
338 }
339 MacPopoverArrowEdge::Left => {
340 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
341 arrow_points = [
342 Pos2::new(active_rect.min.x, ay - arrow_w * 0.5),
343 Pos2::new(active_rect.min.x - arrow_h, ay),
344 Pos2::new(active_rect.min.x, ay + arrow_w * 0.5),
345 ];
346 }
347 MacPopoverArrowEdge::Right => {
348 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
349 arrow_points = [
350 Pos2::new(active_rect.max.x, ay - arrow_w * 0.5),
351 Pos2::new(active_rect.max.x + arrow_h, ay),
352 Pos2::new(active_rect.max.x, ay + arrow_w * 0.5),
353 ];
354 }
355 MacPopoverArrowEdge::None => {}
356 }
357
358 if self.arrow_edge != MacPopoverArrowEdge::None {
359 painter.add(egui::Shape::convex_polygon(
360 arrow_points.to_vec(),
361 bg_color,
362 Stroke::NONE,
363 ));
364 painter.line_segment([arrow_points[0], arrow_points[1]], stroke);
366 painter.line_segment([arrow_points[1], arrow_points[2]], stroke);
367 painter.line_segment(
369 [arrow_points[0], arrow_points[2]],
370 Stroke::new(border_width * 2.0, bg_color),
371 );
372 }
373 }
374
375 let mut child_ui = ui.new_child(
376 egui::UiBuilder::new()
377 .max_rect(active_rect.shrink(corner_radius.min(metrics.inner_padding.points())))
378 .layout(egui::Layout::top_down(egui::Align::Min)),
379 );
380 let ret = add_contents(&mut child_ui);
381 (resp, ret)
382 }
383}