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, 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
39impl<'a> MacButtonCell<'a> {
40    /// Creates a new button with label `text`.
41    pub fn new(text: &'a str) -> Self {
42        Self {
43            text,
44            selected: false,
45            enabled: true,
46            theme: None,
47            width: None,
48            height: None,
49            corner_radius: None,
50        }
51    }
52
53    /// Enables or disables the button cell.
54    pub fn enabled(mut self, enabled: bool) -> Self {
55        self.enabled = enabled;
56        self
57    }
58
59    /// Injects dynamic theme to eliminate hardcoded metrics and colors.
60    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
61        self.theme = Some(theme);
62        self
63    }
64
65    /// Sets whether the button is presented in an active/selected state.
66    pub fn selected(mut self, selected: bool) -> Self {
67        self.selected = selected;
68        self
69    }
70
71    /// Overrides button width in points (defaults to content width + padding).
72    pub fn width(mut self, width: f32) -> Self {
73        self.width = Some(width);
74        self
75    }
76
77    /// Overrides button height in points (defaults to token regular button height).
78    pub fn height(mut self, height: f32) -> Self {
79        self.height = Some(height);
80        self
81    }
82
83    /// Overrides corner radius in points (defaults to token corner radius).
84    pub fn corner_radius(mut self, r: f32) -> Self {
85        self.corner_radius = Some(r);
86        self
87    }
88}
89
90impl<'a> Widget for MacButtonCell<'a> {
91    fn ui(self, ui: &mut Ui) -> Response {
92        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
93        let theme: &MacTheme = &theme_cow;
94
95        let metrics = &theme.button().regular;
96        let height = self.height.unwrap_or_else(|| metrics.height.points());
97        let corner_radius = self
98            .corner_radius
99            .unwrap_or_else(|| metrics.corner_radius.points());
100        let padding_h = metrics
101            .padding_horizontal
102            .as_ref()
103            .map(|d| d.points())
104            .unwrap_or_else(|| metrics.font_size.points());
105
106        let font_id = egui::FontId::proportional(metrics.font_size.points());
107        let text_layout =
108            ui.painter()
109                .layout_no_wrap(self.text.to_owned(), font_id, Color32::PLACEHOLDER);
110        let desired_width = self
111            .width
112            .unwrap_or_else(|| text_layout.size().x + padding_h * 2.0);
113
114        let sense = if self.enabled {
115            egui::Sense::click()
116        } else {
117            egui::Sense::hover()
118        };
119        let (rect, response) = ui.allocate_exact_size(Vec2::new(desired_width, height), sense);
120        let painter = ui.painter_at(rect);
121
122        let alpha_factor = if self.enabled {
123            1.0
124        } else {
125            theme.disabled_opacity()
126        };
127
128        // Dynamic AppKit styling (pressed feedback works even when selected == true)
129        let base_bg = if !self.enabled {
130            if self.selected {
131                theme.selected_control_inactive()
132            } else {
133                theme.control_background()
134            }
135        } else if response.is_pointer_button_down_on() {
136            theme.selected_control_inactive()
137        } else if self.selected {
138            theme.selected_control()
139        } else if response.hovered() {
140            theme.control()
141        } else {
142            theme.control_background()
143        };
144        let bg_color = base_bg.gamma_multiply(alpha_factor);
145
146        let border_stroke = Stroke::new(
147            metrics.border_width.points(),
148            theme.separator().gamma_multiply(alpha_factor),
149        );
150
151        painter.rect(
152            rect,
153            CornerRadius::same(corner_radius as u8),
154            bg_color,
155            border_stroke,
156            StrokeKind::Inside,
157        );
158
159        let bg_style = theme.background_style(base_bg);
160        let text_color = bg_style
161            .primary_text_color(theme)
162            .gamma_multiply(alpha_factor);
163
164        let text_pos = egui::Align2::CENTER_CENTER
165            .anchor_size(rect.center(), text_layout.size())
166            .min;
167        painter.galley(text_pos, text_layout, text_color);
168
169        response
170    }
171}