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 theme_cow = MacTheme::resolve(self.theme, ui.ctx());
85 let theme: &MacTheme = &theme_cow;
86
87 let panel_id = self.id.unwrap_or_else(|| {
88 ui.scope_id().with((
89 "mac_panel",
90 self.title.as_deref().unwrap_or(""),
91 rect.min.x.to_bits(),
92 rect.min.y.to_bits(),
93 ))
94 });
95 let resp = ui.interact(rect, panel_id, egui::Sense::click_and_drag());
96 if ui.rect_contains_pointer(rect) {
97 ui.input_mut(|i| {
98 i.smooth_scroll_delta = Vec2::ZERO;
99 });
100 }
101
102 let popover_metrics = theme.popover_metrics();
103 let rounding = self
104 .rounding
105 .unwrap_or_else(|| popover_metrics.corner_radius.points() as u8);
106
107 let bg_color = theme.window_background();
108 let border_stroke = Stroke::new(popover_metrics.border_width.points(), theme.separator());
109
110 let painter = ui.painter_at(rect);
111 painter.rect(
112 rect,
113 CornerRadius::same(rounding),
114 bg_color,
115 border_stroke,
116 StrokeKind::Inside,
117 );
118
119 let header_height = popover_metrics.header_height.points();
120
121 let content_rect = if let Some(title) = self.title {
122 let title_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), header_height));
123 painter.line_segment(
124 [
125 Pos2::new(rect.min.x, rect.min.y + header_height),
126 Pos2::new(rect.max.x, rect.min.y + header_height),
127 ],
128 border_stroke,
129 );
130 painter.text(
131 title_rect.center(),
132 egui::Align2::CENTER_CENTER,
133 title,
134 egui::FontId::proportional(popover_metrics.header_font_size.points()),
135 theme.label(),
136 );
137 Rect::from_min_max(Pos2::new(rect.min.x, rect.min.y + header_height), rect.max)
138 } else {
139 rect
140 };
141
142 let mut child_ui = ui.new_child(
143 egui::UiBuilder::new()
144 .id_salt((panel_id, "mac_panel_content"))
145 .max_rect(content_rect)
146 .layout(egui::Layout::top_down(egui::Align::Min)),
147 );
148 let ret = add_contents(&mut child_ui);
149 (resp, ret)
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
155pub enum MacPopoverArrowEdge {
156 #[default]
158 Top,
159 Bottom,
161 Left,
163 Right,
165 None,
167}
168
169pub struct MacPopover<'a> {
194 id: Option<Id>,
195 theme: Option<&'a MacTheme>,
196 arrow_edge: MacPopoverArrowEdge,
197 arrow_offset: Option<f32>,
198 scale: f32,
199 alpha: f32,
200}
201
202impl<'a> Default for MacPopover<'a> {
203 fn default() -> Self {
204 Self {
205 id: None,
206 theme: None,
207 arrow_edge: MacPopoverArrowEdge::Top,
208 arrow_offset: None,
209 scale: 1.0,
210 alpha: 1.0,
211 }
212 }
213}
214
215impl<'a> MacPopover<'a> {
216 pub fn new() -> Self {
218 Self::default()
219 }
220
221 pub fn id(mut self, id_source: impl Hash + std::fmt::Debug) -> Self {
223 self.id = Some(Id::NULL.with(id_source));
224 self
225 }
226
227 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
229 self.theme = Some(theme);
230 self
231 }
232
233 pub fn arrow_edge(mut self, edge: MacPopoverArrowEdge) -> Self {
235 self.arrow_edge = edge;
236 self
237 }
238
239 pub fn arrow_offset(mut self, offset: f32) -> Self {
241 self.arrow_offset = Some(offset);
242 self
243 }
244
245 pub fn scale(mut self, scale: f32) -> Self {
247 self.scale = scale;
248 self
249 }
250
251 pub fn alpha(mut self, alpha: f32) -> Self {
253 self.alpha = alpha;
254 self
255 }
256
257 pub fn show<R>(
259 self,
260 ui: &mut Ui,
261 rect: Rect,
262 add_contents: impl FnOnce(&mut Ui) -> R,
263 ) -> (Response, R) {
264 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
265 let theme: &MacTheme = &theme_cow;
266
267 let metrics = theme.popover_metrics();
268 let corner_radius = metrics.corner_radius.points();
269 let border_width = metrics.border_width.points();
270 let arrow_w = metrics.arrow_base_width.points();
271 let arrow_h = metrics.arrow_height.points();
272
273 let center = rect.center();
275 let scaled_size = rect.size() * self.scale.max(f32::EPSILON);
276 let active_rect = Rect::from_center_size(center, scaled_size);
277
278 let popover_id = self.id.unwrap_or_else(|| {
279 ui.scope_id().with((
280 "mac_popover",
281 rect.min.x.to_bits(),
282 rect.min.y.to_bits(),
283 self.arrow_edge as u8,
284 ))
285 });
286 let resp = ui.interact(active_rect, popover_id, egui::Sense::click_and_drag());
287 if ui.rect_contains_pointer(active_rect) {
288 ui.input_mut(|i| {
289 i.smooth_scroll_delta = Vec2::ZERO;
290 });
291 }
292
293 let bg_color = theme.window_background().gamma_multiply(self.alpha);
294 let border_color = theme.separator().gamma_multiply(self.alpha);
295 let stroke = Stroke::new(border_width, border_color);
296
297 let painter = ui.painter_at(active_rect);
298
299 painter.rect(
301 active_rect,
302 CornerRadius::same(corner_radius as u8),
303 bg_color,
304 stroke,
305 StrokeKind::Inside,
306 );
307
308 if self.arrow_edge != MacPopoverArrowEdge::None {
310 let mut arrow_points = [Pos2::ZERO; 3];
311 match self.arrow_edge {
312 MacPopoverArrowEdge::Top => {
313 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
314 arrow_points = [
315 Pos2::new(ax - arrow_w * 0.5, active_rect.min.y),
316 Pos2::new(ax, active_rect.min.y - arrow_h),
317 Pos2::new(ax + arrow_w * 0.5, active_rect.min.y),
318 ];
319 }
320 MacPopoverArrowEdge::Bottom => {
321 let ax = self.arrow_offset.unwrap_or(active_rect.center().x);
322 arrow_points = [
323 Pos2::new(ax - arrow_w * 0.5, active_rect.max.y),
324 Pos2::new(ax, active_rect.max.y + arrow_h),
325 Pos2::new(ax + arrow_w * 0.5, active_rect.max.y),
326 ];
327 }
328 MacPopoverArrowEdge::Left => {
329 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
330 arrow_points = [
331 Pos2::new(active_rect.min.x, ay - arrow_w * 0.5),
332 Pos2::new(active_rect.min.x - arrow_h, ay),
333 Pos2::new(active_rect.min.x, ay + arrow_w * 0.5),
334 ];
335 }
336 MacPopoverArrowEdge::Right => {
337 let ay = self.arrow_offset.unwrap_or(active_rect.center().y);
338 arrow_points = [
339 Pos2::new(active_rect.max.x, ay - arrow_w * 0.5),
340 Pos2::new(active_rect.max.x + arrow_h, ay),
341 Pos2::new(active_rect.max.x, ay + arrow_w * 0.5),
342 ];
343 }
344 MacPopoverArrowEdge::None => {}
345 }
346
347 if self.arrow_edge != MacPopoverArrowEdge::None {
348 painter.add(egui::Shape::convex_polygon(
349 arrow_points.to_vec(),
350 bg_color,
351 Stroke::NONE,
352 ));
353 painter.line_segment([arrow_points[0], arrow_points[1]], stroke);
355 painter.line_segment([arrow_points[1], arrow_points[2]], stroke);
356 painter.line_segment(
358 [arrow_points[0], arrow_points[2]],
359 Stroke::new(border_width * 2.0, bg_color),
360 );
361 }
362 }
363
364 let mut child_ui = ui.new_child(
365 egui::UiBuilder::new()
366 .id_salt((popover_id, "mac_popover_content"))
367 .max_rect(active_rect.shrink(corner_radius.min(metrics.inner_padding.points())))
368 .layout(egui::Layout::top_down(egui::Align::Min)),
369 );
370 let ret = add_contents(&mut child_ui);
371 (resp, ret)
372 }
373}