Skip to main content

egui_macview/controls/
button_cell.rs

1//! Authentic macOS AppKit Push Button / Tool Button.
2//!
3//! Grounded in macOS HIG and W3C DTCG tokens via [`MacTheme`].
4//! Fully data-driven: zero hardcoded colors or dimensions.
5
6use crate::theme::MacTheme;
7use egui::{Color32, CornerRadius, Response, Stroke, StrokeKind, Ui, Vec2, Widget};
8
9/// Authentic macOS AppKit Push Button / Tool Button (`NSButtonCell` equivalent).
10///
11/// Features authentic theme-driven background, border, selection states,
12/// typography metrics with Apple HIG optical tracking, and hover feedback.
13///
14/// # Example
15///
16/// ```rust
17/// use egui_macview::{MacButtonCell, MacTheme};
18///
19/// # let ctx = egui::Context::default();
20/// # let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
21/// let theme = MacTheme::default();
22///
23/// if ui.add(MacButtonCell::new("Save Changes").theme(&theme)).clicked() {
24///     // Handle save
25/// }
26/// # });
27/// # output.textures_delta.clear();
28/// ```
29pub struct MacButtonCell<'a> {
30    text: &'a str,
31    selected: bool,
32    enabled: bool,
33    theme: Option<&'a MacTheme>,
34    width: Option<f32>,
35    height: Option<f32>,
36    corner_radius: Option<f32>,
37}
38
39/// Convenience alias for [`MacButtonCell`].
40pub type MacButton<'a> = MacButtonCell<'a>;
41
42impl<'a> MacButtonCell<'a> {
43    /// Creates a new button with label `text`.
44    pub fn new(text: &'a str) -> Self {
45        Self {
46            text,
47            selected: false,
48            enabled: true,
49            theme: None,
50            width: None,
51            height: None,
52            corner_radius: None,
53        }
54    }
55
56    /// Enables or disables the button cell.
57    pub fn enabled(mut self, enabled: bool) -> Self {
58        self.enabled = enabled;
59        self
60    }
61
62    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
63    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
64        self.theme = Some(theme);
65        self
66    }
67
68    /// Sets whether the button is presented in an active/selected state.
69    pub fn selected(mut self, selected: bool) -> Self {
70        self.selected = selected;
71        self
72    }
73
74    /// Overrides button width in points (defaults to content width + padding).
75    pub fn width(mut self, width: f32) -> Self {
76        self.width = Some(width);
77        self
78    }
79
80    /// Overrides button height in points (defaults to token regular button height).
81    pub fn height(mut self, height: f32) -> Self {
82        self.height = Some(height);
83        self
84    }
85
86    /// Overrides corner radius in points (defaults to token corner radius).
87    pub fn corner_radius(mut self, r: f32) -> Self {
88        self.corner_radius = Some(r);
89        self
90    }
91}
92
93impl<'a> Widget for MacButtonCell<'a> {
94    fn ui(self, ui: &mut Ui) -> Response {
95        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
96        let theme: &MacTheme = &theme_cow;
97
98        let metrics = &theme.button().regular;
99        let height = self.height.unwrap_or_else(|| metrics.height.points());
100        let corner_radius = self
101            .corner_radius
102            .unwrap_or_else(|| metrics.corner_radius.points());
103        let padding_h = metrics
104            .padding_horizontal
105            .as_ref()
106            .map(|d| d.points())
107            .unwrap_or_else(|| metrics.font_size.points());
108
109        let font_id = egui::FontId::proportional(metrics.font_size.points());
110        let text_layout = theme.layout_tracked_galley(
111            ui,
112            self.text,
113            font_id,
114            Color32::PLACEHOLDER,
115            f32::INFINITY,
116        );
117        let desired_width = self
118            .width
119            .unwrap_or_else(|| text_layout.size().x + padding_h * 2.0);
120
121        let sense = if self.enabled {
122            egui::Sense::click()
123        } else {
124            egui::Sense::hover()
125        };
126        let (rect, response) = ui.allocate_exact_size(Vec2::new(desired_width, height), sense);
127        let painter = ui.painter_at(rect);
128
129        let alpha_factor = if self.enabled {
130            1.0
131        } else {
132            theme.disabled_opacity()
133        };
134
135        // Dynamic AppKit styling (pressed feedback works even when selected == true)
136        let base_bg = if !self.enabled {
137            if self.selected {
138                theme.selected_control_inactive()
139            } else {
140                theme.control_background()
141            }
142        } else if response.is_pointer_button_down_on() {
143            theme.selected_control_inactive()
144        } else if self.selected {
145            theme.selected_control()
146        } else if response.hovered() {
147            theme.control()
148        } else {
149            theme.control_background()
150        };
151        let bg_color = base_bg.gamma_multiply(alpha_factor);
152
153        let border_stroke = Stroke::new(
154            metrics.border_width.points(),
155            theme.separator().gamma_multiply(alpha_factor),
156        );
157
158        painter.rect(
159            rect,
160            CornerRadius::same(corner_radius as u8),
161            bg_color,
162            border_stroke,
163            StrokeKind::Inside,
164        );
165
166        let bg_style = theme.background_style(base_bg);
167        let text_color = bg_style
168            .primary_text_color(theme)
169            .gamma_multiply(alpha_factor);
170
171        let text_pos = egui::Align2::CENTER_CENTER
172            .anchor_size(rect.center(), text_layout.size())
173            .min;
174        painter.galley(text_pos, text_layout, text_color);
175
176        response
177    }
178}