Skip to main content

egui_macview/controls/
help_button.rs

1//! Authentic macOS AppKit Help Button (`NSBezelStyleHelpButton`) and Inline Action Badge (`v0.6.5`).
2
3use crate::controls::MacControlSize;
4use crate::layout::subpixel_snap_rect;
5use crate::symbols::MacSymbol;
6use crate::theme::MacTheme;
7use egui::{Align2, FontId, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
8
9/// Authentic macOS AppKit Circular Help Button (`NSBezelStyleHelpButton`).
10///
11/// # Example
12/// ```rust
13/// use egui_macview::{MacAppearance, MacHelpButton, MacTheme};
14/// # let ctx = egui::Context::default();
15/// # let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
16/// let theme = MacTheme::new(MacAppearance::Aqua);
17/// if MacHelpButton::new().theme(&theme).show(ui).clicked() {}
18/// # });
19/// # out.textures_delta.clear();
20/// ```
21pub struct MacHelpButton<'a> {
22    control_size: MacControlSize,
23    fixed_rect: Option<Rect>,
24    enabled: bool,
25    theme: Option<&'a MacTheme>,
26}
27
28impl<'a> Default for MacHelpButton<'a> {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl<'a> MacHelpButton<'a> {
35    /// Creates a new circular AppKit help button (`?`).
36    pub fn new() -> Self {
37        Self {
38            control_size: MacControlSize::Regular,
39            fixed_rect: None,
40            enabled: true,
41            theme: None,
42        }
43    }
44
45    /// Sets the AppKit control size (`Regular` `20pt`, `Small` `16pt`, `Mini` `13pt`).
46    pub fn control_size(mut self, size: MacControlSize) -> Self {
47        self.control_size = size;
48        self
49    }
50
51    /// Pins the help button to an exact NIB rectangle.
52    pub fn fixed_rect(mut self, rect: Rect) -> Self {
53        self.fixed_rect = Some(rect);
54        self
55    }
56
57    /// Enables or disables interaction.
58    pub fn enabled(mut self, enabled: bool) -> Self {
59        self.enabled = enabled;
60        self
61    }
62
63    /// Injects dynamic [`MacTheme`].
64    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
65        self.theme = Some(theme);
66        self
67    }
68
69    /// Returns the visual circular diameter in points (`20.0pt`, `16.0pt`, or `13.0pt`).
70    pub fn diameter(&self) -> f32 {
71        let theme_cow = MacTheme::resolve_or_default(self.theme);
72        let hm = theme_cow.help_button();
73        match self.control_size {
74            MacControlSize::Regular | MacControlSize::Large => hm.diameter.points(),
75            MacControlSize::Small | MacControlSize::CompactInfobox => hm.small_diameter.points(),
76            MacControlSize::Mini => hm.mini_diameter.points(),
77        }
78    }
79
80    /// Returns the `?` glyph font size in points (`12.0pt` Regular, `10.0pt` Small, `9.0pt` Mini).
81    pub fn font_size(&self) -> f32 {
82        let theme_cow = MacTheme::resolve_or_default(self.theme);
83        let hm = theme_cow.help_button();
84        match self.control_size {
85            MacControlSize::Regular | MacControlSize::Large => hm.font_size.points(),
86            MacControlSize::Small | MacControlSize::CompactInfobox => hm.small_font_size.points(),
87            MacControlSize::Mini => hm.mini_font_size.points(),
88        }
89    }
90
91    /// Renders the help button into `ui`.
92    pub fn show(self, ui: &mut Ui) -> Response {
93        ui.add(self)
94    }
95}
96
97impl<'a> Widget for MacHelpButton<'a> {
98    fn ui(self, ui: &mut Ui) -> Response {
99        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
100        let theme: &MacTheme = &theme_cow;
101        let hm = theme.help_button();
102
103        let enabled = self.enabled && ui.is_enabled();
104        let diam = self.diameter();
105        let border_w = hm.border_width.points();
106        let shadow_dy = hm.shadow_offset_y.points();
107        let font_sz = self.font_size();
108        let sense = if enabled { Sense::click() } else { Sense::hover() };
109        let ppp = ui.ctx().pixels_per_point();
110        let (rect, response) = if let Some(fixed) = self.fixed_rect {
111            let snapped = subpixel_snap_rect(fixed, ppp);
112            let id = ui.next_auto_id().with("mac_help_btn_fixed");
113            ui.skip_ahead_auto_ids(1);
114            (snapped, ui.interact(snapped, id, sense))
115        } else {
116            let (r, resp) = ui.allocate_exact_size(Vec2::splat(diam), sense);
117            (subpixel_snap_rect(r, ppp), resp)
118        };
119
120        let alpha = if enabled { 1.0 } else { theme.disabled_opacity() };
121        let painter = ui.painter_at(rect);
122        let visual_rect = theme.clamp_nib_visual_rect(rect, diam, ppp);
123        let center = visual_rect.center();
124        let visual_diam = diam.min(visual_rect.width()).min(visual_rect.height());
125        let radius = visual_diam * 0.5;
126
127        if enabled && response.has_focus() {
128            theme.paint_focus_ring(&painter, Rect::from_center_size(center, Vec2::splat(visual_diam)), radius);
129        }
130
131        painter.circle_filled(
132            Pos2::new(center.x, center.y + shadow_dy),
133            radius,
134            theme.drop_shadow().gamma_multiply(alpha),
135        );
136
137        let fill = if !enabled {
138            theme.control_background().gamma_multiply(alpha)
139        } else if response.is_pointer_button_down_on() {
140            theme.selected_control_inactive()
141        } else if response.hovered() {
142            theme.control()
143        } else {
144            theme.control_background()
145        };
146
147        painter.circle(center, radius, fill, Stroke::new(border_w, theme.separator().gamma_multiply(alpha)));
148        let text_color = theme.label().gamma_multiply(alpha);
149        let (font_id, has_semibold) = theme.semibold_font_id(ui, font_sz);
150        let galley = theme.layout_tracked_galley(ui, "?", font_id, text_color, f32::INFINITY);
151        let text_pos = Align2::CENTER_CENTER.anchor_size(center, galley.size()).min;
152        painter.galley(text_pos, galley.clone(), text_color);
153        if !has_semibold {
154            let boost = theme.label_metrics().semibold_stroke_boost.points();
155            painter.galley(Pos2::new(text_pos.x + boost, text_pos.y), galley, text_color);
156        }
157        response
158    }
159}
160
161/// Circular `16x16pt` AppKit Inline Action Badge Button (used for inline `+`, `-`, `x`, or symbol actions).
162///
163/// # Example
164/// ```rust
165/// use egui_macview::{MacAppearance, MacInlineActionButton, MacSymbol, MacTheme};
166/// # let ctx = egui::Context::default();
167/// # let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
168/// let theme = MacTheme::new(MacAppearance::Aqua);
169/// if MacInlineActionButton::from_symbol(MacSymbol::Plus).theme(&theme).show(ui).clicked() {}
170/// # });
171/// # out.textures_delta.clear();
172/// ```
173pub struct MacInlineActionButton<'a> {
174    glyph: &'a str,
175    fixed_rect: Option<Rect>,
176    enabled: bool,
177    theme: Option<&'a MacTheme>,
178}
179
180impl<'a> MacInlineActionButton<'a> {
181    /// Creates an inline action badge button displaying `glyph` (such as `"+"`, `"-"`, or `"×"`).
182    pub fn new(glyph: &'a str) -> Self {
183        Self {
184            glyph,
185            fixed_rect: None,
186            enabled: true,
187            theme: None,
188        }
189    }
190
191    /// Creates an inline action badge button displaying a [`MacSymbol`].
192    pub fn from_symbol(symbol: MacSymbol) -> Self {
193        Self::new(symbol.as_str())
194    }
195
196    /// Pins the inline action button to an exact NIB rectangle.
197    pub fn fixed_rect(mut self, rect: Rect) -> Self {
198        self.fixed_rect = Some(rect);
199        self
200    }
201
202    /// Enables or disables interaction.
203    pub fn enabled(mut self, enabled: bool) -> Self {
204        self.enabled = enabled;
205        self
206    }
207
208    /// Injects dynamic [`MacTheme`].
209    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
210        self.theme = Some(theme);
211        self
212    }
213
214    /// Returns the circular badge diameter (`16.0pt`).
215    pub fn diameter(&self) -> f32 {
216        MacTheme::resolve_or_default(self.theme).inline_action_button().diameter.points()
217    }
218
219    /// Renders the inline action button into `ui`.
220    pub fn show(self, ui: &mut Ui) -> Response {
221        ui.add(self)
222    }
223}
224
225impl<'a> Widget for MacInlineActionButton<'a> {
226    fn ui(self, ui: &mut Ui) -> Response {
227        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
228        let theme: &MacTheme = &theme_cow;
229        let im = theme.inline_action_button();
230
231        let enabled = self.enabled && ui.is_enabled();
232        let diam = im.diameter.points();
233        let border_w = im.border_width.points();
234        let icon_sz = im.icon_size.points();
235        let sense = if enabled { Sense::click() } else { Sense::hover() };
236        let ppp = ui.ctx().pixels_per_point();
237        let (rect, response) = if let Some(fixed) = self.fixed_rect {
238            let snapped = subpixel_snap_rect(fixed, ppp);
239            let id = ui.next_auto_id().with("mac_inline_action_fixed");
240            ui.skip_ahead_auto_ids(1);
241            (snapped, ui.interact(snapped, id, sense))
242        } else {
243            let (r, resp) = ui.allocate_exact_size(Vec2::splat(diam), sense);
244            (subpixel_snap_rect(r, ppp), resp)
245        };
246
247        let alpha = if enabled { 1.0 } else { theme.disabled_opacity() };
248        let painter = ui.painter_at(rect);
249        let visual_rect = theme.clamp_nib_visual_rect(rect, diam, ppp);
250        let center = visual_rect.center();
251        let visual_diam = diam.min(visual_rect.width()).min(visual_rect.height());
252        let visual_radius = visual_diam * 0.5;
253
254        if enabled && response.has_focus() {
255            theme.paint_focus_ring(&painter, Rect::from_center_size(center, Vec2::splat(visual_diam)), visual_radius);
256        }
257
258        let fill = if !enabled {
259            theme.control().gamma_multiply(alpha)
260        } else if response.is_pointer_button_down_on() {
261            theme.selected_control_inactive()
262        } else if response.hovered() {
263            theme.control_background()
264        } else {
265            theme.control()
266        };
267
268        painter.circle(center, visual_radius, fill, Stroke::new(border_w, theme.separator().gamma_multiply(alpha)));
269        let surface_bg = theme.active_surface_at(ui, center);
270        let fg = if !enabled {
271            theme.tertiary_label_on(surface_bg)
272        } else if response.hovered() {
273            theme.label_on(surface_bg)
274        } else {
275            theme.secondary_label_on(surface_bg)
276        };
277        let galley = theme.layout_tracked_galley(ui, self.glyph, FontId::proportional(icon_sz), fg, f32::INFINITY);
278        let pos = Align2::CENTER_CENTER.anchor_size(center, galley.size()).min;
279        painter.galley(pos, galley, fg);
280        response
281    }
282}