Skip to main content

egui_macview/controls/
label.rs

1//! Authentic macOS AppKit Non-Editable Label (`NSTextField.labelWithString:`, `v0.6.5`).
2//!
3//! Grounded in macOS Sonoma 14.8.9 AppKit `NSTextField` label metrics and W3C DTCG tokens via [`MacTheme`].
4//!
5//! Provides exact AppKit typography sizing (`13pt` Regular, `11pt` Small, `9pt` Mini),
6//! weight variants (`Regular`, `Medium`, `Semibold`, `Bold`), semantic label colors
7//! (`Primary`, `Secondary`, `Tertiary`), optical tracking, and deterministic
8//! cross-platform baseline positioning (`firstBaselineOffsetFromTop`).
9
10use crate::controls::MacControlSize;
11use crate::layout::{subpixel_snap, subpixel_snap_rect};
12use crate::theme::MacTheme;
13use egui::{Align, Color32, FontId, Pos2, Rect, Response, Sense, Ui, Vec2, Widget, WidgetText};
14
15/// Font weight tiers for [`MacLabel`].
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum MacFontWeight {
18    /// Standard regular weight (`NSFontWeightRegular`).
19    #[default]
20    Regular,
21    /// Medium weight (`NSFontWeightMedium`).
22    Medium,
23    /// Semibold weight (`NSFontWeightSemibold`, used for section headers).
24    Semibold,
25    /// Bold weight (`NSFontWeightBold`).
26    Bold,
27}
28
29/// Semantic AppKit label color tiers for [`MacLabel`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum MacLabelColor {
32    /// Primary label color (`NSColor.labelColor`).
33    #[default]
34    Primary,
35    /// Secondary label color (`NSColor.secondaryLabelColor`).
36    Secondary,
37    /// Tertiary label color (`NSColor.tertiaryLabelColor`).
38    Tertiary,
39}
40
41/// Authentic macOS AppKit Non-Editable Label (`NSTextField.labelWithString:`).
42///
43/// # Example
44/// ```rust
45/// use egui_macview::{
46///     MacAppearance, MacControlSize, MacFontWeight, MacLabel, MacLabelColor, MacTheme,
47/// };
48///
49/// # let ctx = egui::Context::default();
50/// # let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
51/// let theme = MacTheme::new(MacAppearance::Aqua);
52///
53/// MacLabel::new("Output Resolution:")
54///     .control_size(MacControlSize::Regular)
55///     .weight(MacFontWeight::Semibold)
56///     .color(MacLabelColor::Primary)
57///     .theme(&theme)
58///     .show(ui);
59/// # });
60/// # out.textures_delta.clear();
61/// ```
62pub struct MacLabel<'a> {
63    text: WidgetText,
64    control_size: MacControlSize,
65    weight: MacFontWeight,
66    color: MacLabelColor,
67    alignment: Align,
68    fixed_rect: Option<Rect>,
69    theme: Option<&'a MacTheme>,
70}
71
72impl<'a> MacLabel<'a> {
73    /// Creates a new AppKit non-editable label with `text`.
74    pub fn new(text: impl Into<WidgetText>) -> Self {
75        Self {
76            text: text.into(),
77            control_size: MacControlSize::Regular,
78            weight: MacFontWeight::Regular,
79            color: MacLabelColor::Primary,
80            alignment: Align::Min,
81            fixed_rect: None,
82            theme: None,
83        }
84    }
85
86    /// Sets the AppKit control size (`Regular` `13pt`, `Small` `11pt`, `Mini` `9pt`).
87    pub fn control_size(mut self, size: MacControlSize) -> Self {
88        self.control_size = size;
89        self
90    }
91
92    /// Sets the font weight (`Regular`, `Medium`, `Semibold`, `Bold`).
93    pub fn weight(mut self, weight: MacFontWeight) -> Self {
94        self.weight = weight;
95        self
96    }
97
98    /// Sets the semantic label color (`Primary`, `Secondary`, `Tertiary`).
99    pub fn color(mut self, color: MacLabelColor) -> Self {
100        self.color = color;
101        self
102    }
103
104    /// Sets horizontal text alignment (`Align::Min`, `Align::Center`, `Align::Max`).
105    pub fn alignment(mut self, alignment: Align) -> Self {
106        self.alignment = alignment;
107        self
108    }
109
110    /// Pins the label to an exact NIB rectangle.
111    pub fn fixed_rect(mut self, rect: Rect) -> Self {
112        self.fixed_rect = Some(rect);
113        self
114    }
115
116    /// Injects dynamic [`MacTheme`].
117    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
118        self.theme = Some(theme);
119        self
120    }
121
122    /// Resolves the font size in points (`13.0pt`, `11.0pt`, or `9.0pt`).
123    pub fn font_size(&self) -> f32 {
124        let theme_cow = MacTheme::resolve_or_default(self.theme);
125        let lm = theme_cow.label_metrics();
126        match self.control_size {
127            MacControlSize::Regular | MacControlSize::Large => lm.regular_font_size.points(),
128            MacControlSize::Small | MacControlSize::CompactInfobox => lm.small_font_size.points(),
129            MacControlSize::Mini => lm.mini_font_size.points(),
130        }
131    }
132
133    /// Resolves the semantic [`Color32`] from `theme`.
134    pub fn resolved_color(&self, theme: &MacTheme) -> Color32 {
135        match self.color {
136            MacLabelColor::Primary => theme.label(),
137            MacLabelColor::Secondary => theme.secondary_label(),
138            MacLabelColor::Tertiary => theme.tertiary_label(),
139        }
140    }
141
142    /// Renders the label into `ui`.
143    pub fn show(self, ui: &mut Ui) -> Response {
144        ui.add(self)
145    }
146}
147
148impl<'a> Widget for MacLabel<'a> {
149    fn ui(self, ui: &mut Ui) -> Response {
150        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
151        let theme: &MacTheme = &theme_cow;
152        let lm = theme.label_metrics();
153
154        let (font_sz, cell_h, baseline_from_top, ascender) = match self.control_size {
155            MacControlSize::Regular | MacControlSize::Large => (
156                lm.regular_font_size.points(),
157                lm.regular_height.points(),
158                lm.regular_baseline_from_top.points(),
159                lm.regular_ascender.points(),
160            ),
161            MacControlSize::Small | MacControlSize::CompactInfobox => (
162                lm.small_font_size.points(),
163                lm.small_height.points(),
164                lm.small_baseline_from_top.points(),
165                lm.small_ascender.points(),
166            ),
167            MacControlSize::Mini => (
168                lm.mini_font_size.points(),
169                lm.mini_height.points(),
170                lm.mini_baseline_from_top.points(),
171                lm.mini_ascender.points(),
172            ),
173        };
174
175        let text_color = self.resolved_color(theme);
176        let is_heavy = matches!(
177            self.weight,
178            MacFontWeight::Medium | MacFontWeight::Semibold | MacFontWeight::Bold
179        );
180        let (font_id, has_semibold_family) = if is_heavy {
181            theme.semibold_font_id(ui, font_sz)
182        } else {
183            (FontId::proportional(font_sz), false)
184        };
185
186        let wrap_w = self.fixed_rect.map_or(f32::INFINITY, |r| r.width());
187        let galley =
188            theme.layout_tracked_galley(ui, self.text.text(), font_id, text_color, wrap_w);
189
190        let ppp = ui.ctx().pixels_per_point();
191        let (rect, response) = if let Some(fixed) = self.fixed_rect {
192            let snapped = subpixel_snap_rect(fixed, ppp);
193            let id = ui.next_auto_id().with("mac_label_fixed");
194            let resp = ui.interact(snapped, id, Sense::hover());
195            (snapped, resp)
196        } else {
197            let desired = Vec2::new(galley.size().x, cell_h.max(galley.size().y));
198            let (r, resp) = ui.allocate_exact_size(desired, Sense::hover());
199            (subpixel_snap_rect(r, ppp), resp)
200        };
201
202        let painter = ui.painter_at(rect);
203        let text_y = subpixel_snap(rect.top() + (baseline_from_top - ascender), ppp);
204        let text_x = match self.alignment {
205            Align::Min => rect.left(),
206            Align::Center => rect.center().x - galley.size().x * 0.5,
207            Align::Max => (rect.right() - galley.size().x).max(rect.left()),
208        };
209        let pos = Pos2::new(text_x, text_y);
210        painter.galley(pos, galley.clone(), text_color);
211
212        if is_heavy && !has_semibold_family {
213            let boost = match self.weight {
214                MacFontWeight::Bold => lm.bold_stroke_boost.points(),
215                MacFontWeight::Medium | MacFontWeight::Semibold => lm.semibold_stroke_boost.points(),
216                MacFontWeight::Regular => 0.0,
217            };
218            if boost > 0.0 {
219                painter.galley(Pos2::new(pos.x + boost, pos.y), galley, text_color);
220            }
221        }
222
223        response
224    }
225}