Skip to main content

egui_macview/surfaces/
mod.rs

1//! macOS AppKit Panel, Popover & Card Surfaces.
2//!
3//! Grounded in macOS HIG and W3C DTCG tokens via [`MacTheme`].
4//! Fully data-driven: zero hardcoded colors or dimensions.
5
6pub mod card;
7pub mod hud;
8pub mod popover;
9pub use card::MacCard;
10pub use hud::MacFloatingInfoBox;
11pub use popover::{MacPopover, MacPopoverArrowEdge, MacPopoverSurface};
12
13use crate::theme::MacTheme;
14use egui::{CornerRadius, Id, Pos2, Rect, Response, Stroke, StrokeKind, Ui, Vec2};
15use std::hash::Hash;
16
17/// Authentic macOS NSPanel floating surface container.
18///
19/// Features authentic window background fill, border hairline strokes, optional
20/// title bar with centered title text, and customizable corner rounding.
21///
22/// # Example
23///
24/// ```rust
25/// use egui_macview::{MacPanel, MacTheme};
26///
27/// # let ctx = egui::Context::default();
28/// # let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
29/// let theme = MacTheme::default();
30/// let panel_rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 150.0));
31///
32/// MacPanel::new()
33///     .theme(&theme)
34///     .title("Inspector")
35///     .show(ui, panel_rect, |panel_ui| {
36///         panel_ui.label("Panel Content");
37///     });
38/// # });
39/// # output.textures_delta.clear();
40/// ```
41#[derive(Default)]
42pub struct MacPanel<'a> {
43    id: Option<Id>,
44    title: Option<String>,
45    theme: Option<&'a MacTheme>,
46    rounding: Option<u8>,
47}
48
49impl<'a> MacPanel<'a> {
50    /// Creates a new floating panel surface.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Sets an explicit widget `Id` for this panel.
56    pub fn id(mut self, id_source: impl Hash + std::fmt::Debug) -> Self {
57        self.id = Some(Id::NULL.with(id_source));
58        self
59    }
60
61    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
62    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
63        self.theme = Some(theme);
64        self
65    }
66
67    /// Sets optional panel title displayed in the header bar.
68    pub fn title(mut self, title: impl Into<String>) -> Self {
69        self.title = Some(title.into());
70        self
71    }
72
73    /// Overrides panel corner rounding in points.
74    pub fn rounding(mut self, r: u8) -> Self {
75        self.rounding = Some(r);
76        self
77    }
78
79    /// Renders the panel within `rect` and invokes `add_contents` for child widgets.
80    pub fn show<R>(
81        self,
82        ui: &mut Ui,
83        rect: Rect,
84        add_contents: impl FnOnce(&mut Ui) -> R,
85    ) -> (Response, R) {
86        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
87        let theme: &MacTheme = &theme_cow;
88
89        let panel_id = self.id.unwrap_or_else(|| {
90            ui.scope_id().with((
91                "mac_panel",
92                self.title.as_deref().unwrap_or(""),
93                rect.min.x.to_bits(),
94                rect.min.y.to_bits(),
95            ))
96        });
97        let resp = ui.interact(rect, panel_id, egui::Sense::click_and_drag());
98        if ui.rect_contains_pointer(rect) {
99            ui.input_mut(|i| {
100                i.smooth_scroll_delta = Vec2::ZERO;
101            });
102        }
103
104        let popover_metrics = theme.popover_metrics();
105        let rounding = self
106            .rounding
107            .unwrap_or_else(|| popover_metrics.corner_radius.points() as u8);
108
109        let bg_color = theme.window_background();
110        let border_stroke = Stroke::new(popover_metrics.border_width.points(), theme.separator());
111
112        let painter = ui.painter_at(rect);
113        painter.rect(
114            rect,
115            CornerRadius::same(rounding),
116            bg_color,
117            border_stroke,
118            StrokeKind::Inside,
119        );
120
121        let header_height = popover_metrics.header_height.points();
122
123        let content_rect = if let Some(title) = self.title {
124            let title_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), header_height));
125            painter.line_segment(
126                [
127                    Pos2::new(rect.min.x, rect.min.y + header_height),
128                    Pos2::new(rect.max.x, rect.min.y + header_height),
129                ],
130                border_stroke,
131            );
132            painter.text(
133                title_rect.center(),
134                egui::Align2::CENTER_CENTER,
135                title,
136                egui::FontId::proportional(popover_metrics.header_font_size.points()),
137                theme.label(),
138            );
139            Rect::from_min_max(Pos2::new(rect.min.x, rect.min.y + header_height), rect.max)
140        } else {
141            rect
142        };
143
144        let mut child_ui = ui.new_child(
145            egui::UiBuilder::new()
146                .id_salt((panel_id, "mac_panel_content"))
147                .max_rect(content_rect)
148                .layout(egui::Layout::top_down(egui::Align::Min)),
149        );
150        let ret = add_contents(&mut child_ui);
151        (resp, ret)
152    }
153}