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