Skip to main content

herogpui_core/
enums.rs

1//! Shared enums for HeroGPUI — the v3 prop vocabularies.
2//!
3//! v3 does not use one variant enum everywhere. It uses a small number of
4//! distinct vocabularies, each modelled separately here so an invalid
5//! combination cannot be expressed:
6//!
7//! * [`Variant`] — button emphasis (`primary | secondary | … | danger`)
8//! * [`FieldVariant`] — form-control emphasis (`primary | secondary`)
9//! * [`Prominence`] — container prominence (`transparent | default | …`)
10//! * [`Backdrop`] — overlay scrim style (`opaque | blur | transparent`)
11//! * [`Color`] — semantic color role (`default | accent | … | danger`)
12
13/// Semantic color roles — HeroUI v3.
14///
15/// `Accent` is the brand color (v2 `primary`). `Secondary` as a *color* was
16/// removed in v3; the `secondary` *variant* uses `default`.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum Color {
19    #[default]
20    Default,
21    Accent,
22    Success,
23    Warning,
24    Danger,
25}
26
27impl Color {
28    pub const ALL: [Color; 5] = [
29        Color::Default,
30        Color::Accent,
31        Color::Success,
32        Color::Warning,
33        Color::Danger,
34    ];
35
36    /// The v3 token name of this role.
37    pub fn token(self) -> &'static str {
38        match self {
39            Color::Default => "default",
40            Color::Accent => "accent",
41            Color::Success => "success",
42            Color::Warning => "warning",
43            Color::Danger => "danger",
44        }
45    }
46
47    pub fn label(self) -> &'static str {
48        match self {
49            Color::Default => "Default",
50            Color::Accent => "Accent",
51            Color::Success => "Success",
52            Color::Warning => "Warning",
53            Color::Danger => "Danger",
54        }
55    }
56}
57
58/// Button emphasis variant — `Button`, and the vocabulary `ButtonGroup`
59/// inherits to its direct children (the pinned group context takes
60/// `ButtonProps["variant"]`, every value on this enum).
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
62pub enum Variant {
63    /// Filled with `accent`.
64    #[default]
65    Primary,
66    /// Filled with `default`, accent-tinted label.
67    Secondary,
68    /// Transparent until hovered.
69    Tertiary,
70    /// Bordered, transparent fill.
71    Outline,
72    /// No border, no fill; soft hover only.
73    Ghost,
74    /// Filled with `danger`.
75    Danger,
76    /// `danger` at 15% over the surface, with danger-colored text.
77    DangerSoft,
78}
79
80impl Variant {
81    pub const ALL: [Variant; 7] = [
82        Variant::Primary,
83        Variant::Secondary,
84        Variant::Tertiary,
85        Variant::Outline,
86        Variant::Ghost,
87        Variant::Danger,
88        Variant::DangerSoft,
89    ];
90
91    /// Every Button variant can be inherited by `ButtonGroup` members.
92    pub const GROUP: [Variant; 7] = Self::ALL;
93
94    pub fn label(self) -> &'static str {
95        match self {
96            Variant::Primary => "Primary",
97            Variant::Secondary => "Secondary",
98            Variant::Tertiary => "Tertiary",
99            Variant::Outline => "Outline",
100            Variant::Ghost => "Ghost",
101            Variant::Danger => "Danger",
102            Variant::DangerSoft => "Danger Soft",
103        }
104    }
105}
106
107/// Form-control emphasis — `primary` carries the field shadow, `secondary` is
108/// the flat low-emphasis style for use inside a `Surface`.
109#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
110pub enum FieldVariant {
111    #[default]
112    Primary,
113    Secondary,
114}
115
116impl FieldVariant {
117    pub const ALL: [FieldVariant; 2] = [FieldVariant::Primary, FieldVariant::Secondary];
118
119    pub fn label(self) -> &'static str {
120        match self {
121            FieldVariant::Primary => "Primary",
122            FieldVariant::Secondary => "Secondary",
123        }
124    }
125}
126
127/// Container prominence — `Surface` and `Card`. [`Separator`] uses the same
128/// ladder minus `transparent`.
129///
130/// [`Separator`]: ../herogpui_components/struct.Separator.html
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
132pub enum Prominence {
133    /// No background — for overlays and custom-painted containers.
134    Transparent,
135    /// `bg-surface`
136    #[default]
137    Default,
138    /// `bg-surface-secondary`
139    Secondary,
140    /// `bg-surface-tertiary`
141    Tertiary,
142}
143
144impl Prominence {
145    pub const ALL: [Prominence; 4] = [
146        Prominence::Transparent,
147        Prominence::Default,
148        Prominence::Secondary,
149        Prominence::Tertiary,
150    ];
151
152    pub fn label(self) -> &'static str {
153        match self {
154            Prominence::Transparent => "Transparent",
155            Prominence::Default => "Default",
156            Prominence::Secondary => "Secondary",
157            Prominence::Tertiary => "Tertiary",
158        }
159    }
160}
161
162/// Overlay scrim style — `Modal`, `Drawer` and `AlertDialog`.
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
164pub enum Backdrop {
165    #[default]
166    Opaque,
167    Blur,
168    Transparent,
169}
170
171impl Backdrop {
172    pub const ALL: [Backdrop; 3] = [Backdrop::Opaque, Backdrop::Blur, Backdrop::Transparent];
173
174    pub fn label(self) -> &'static str {
175        match self {
176            Backdrop::Opaque => "Opaque",
177            Backdrop::Blur => "Blur",
178            Backdrop::Transparent => "Transparent",
179        }
180    }
181}
182
183/// The `sm | md | lg` scale used by most components.
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub enum Size {
186    Sm,
187    #[default]
188    Md,
189    Lg,
190}
191
192impl Size {
193    pub const ALL: [Size; 3] = [Size::Sm, Size::Md, Size::Lg];
194
195    /// Control height: sm 32px, md 36px, lg 40px.
196    ///
197    /// These are v3's *desktop* heights. Its sheet is mobile-first — `.button`
198    /// is `h-10 md:h-9`, `.button--sm` is `h-9 md:h-8`, `.button--lg` is
199    /// `h-11 md:h-10` — and a desktop app is past every breakpoint, so the `md`
200    /// value is the one to match. Reading the base value made every control a
201    /// step too tall.
202    pub fn control_height(self) -> gpui::Pixels {
203        match self {
204            Size::Sm => gpui::px(32.0),
205            Size::Md => gpui::px(36.0),
206            Size::Lg => gpui::px(40.0),
207        }
208    }
209
210    /// Icon-only controls are square at the control height.
211    pub fn icon_control_size(self) -> gpui::Pixels {
212        self.control_height()
213    }
214
215    /// The `text-xs`/`text-sm`/`text-base` ladder: sm 12px, md 14px, lg 16px.
216    ///
217    /// Not every sized component steps its type on every rung — `.button` only
218    /// steps at `lg` — so a component reads its own stylesheet rather than
219    /// assuming this one.
220    pub fn text_size(self) -> gpui::Pixels {
221        match self {
222            Size::Sm => gpui::px(12.0),
223            Size::Md => gpui::px(14.0),
224            Size::Lg => gpui::px(16.0),
225        }
226    }
227
228    pub fn label(self) -> &'static str {
229        match self {
230            Size::Sm => "Small",
231            Size::Md => "Medium",
232            Size::Lg => "Large",
233        }
234    }
235}
236
237/// The `xs | sm | md | lg | xl` scale used by `Spinner`, `ColorSwatch` and
238/// `ColorSwatchPicker`.
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
240pub enum SizeXl {
241    Xs,
242    Sm,
243    #[default]
244    Md,
245    Lg,
246    Xl,
247}
248
249impl SizeXl {
250    pub const ALL: [SizeXl; 5] = [SizeXl::Xs, SizeXl::Sm, SizeXl::Md, SizeXl::Lg, SizeXl::Xl];
251
252    /// A colour swatch's edge: `size-4 / 6 / 8 / 9 / 10` from
253    /// `color-swatch.css`, so 16 / 24 / 32 / 36 / 40.
254    ///
255    /// Named for the component on purpose: v3 declares its sizes per sheet, and
256    /// a swatch's `sm` (24px) is not a spinner's (16px, and `Spinner` has its own
257    /// `SpinnerSize` for exactly that reason). The shared `px()` this replaces
258    /// was 16/20/24/32/40 and matched neither sheet.
259    pub fn swatch_px(self) -> gpui::Pixels {
260        match self {
261            SizeXl::Xs => gpui::px(16.0),
262            SizeXl::Sm => gpui::px(24.0),
263            SizeXl::Md => gpui::px(32.0),
264            SizeXl::Lg => gpui::px(36.0),
265            SizeXl::Xl => gpui::px(40.0),
266        }
267    }
268
269    pub fn label(self) -> &'static str {
270        match self {
271            SizeXl::Xs => "Xs",
272            SizeXl::Sm => "Sm",
273            SizeXl::Md => "Md",
274            SizeXl::Lg => "Lg",
275            SizeXl::Xl => "Xl",
276        }
277    }
278}
279
280/// Orientation for separators, toolbars, sliders and groups.
281#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
282pub enum Orientation {
283    #[default]
284    Horizontal,
285    Vertical,
286}
287
288impl Orientation {
289    pub const ALL: [Orientation; 2] = [Orientation::Horizontal, Orientation::Vertical];
290
291    pub fn is_horizontal(self) -> bool {
292        matches!(self, Orientation::Horizontal)
293    }
294
295    pub fn label(self) -> &'static str {
296        match self {
297            Orientation::Horizontal => "Horizontal",
298            Orientation::Vertical => "Vertical",
299        }
300    }
301}
302
303/// How many items a collection lets the user select.
304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
305pub enum SelectionMode {
306    None,
307    #[default]
308    Single,
309    Multiple,
310}
311
312/// How a multi-select collection changes selection when an item is activated.
313///
314/// This is React Stately's `selectionBehavior` prop. `Toggle` preserves the
315/// existing selection and toggles the activated key; `Replace` makes a plain
316/// pointer or keyboard activation the sole selection. Single-selection
317/// collections always retain their own replace/toggle semantics regardless of
318/// this value.
319#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
320pub enum SelectionBehavior {
321    /// Add or remove the activated key while preserving other selected keys.
322    #[default]
323    Toggle,
324    /// Replace the current multi-selection with the activated key.
325    Replace,
326}
327
328impl SelectionBehavior {
329    pub const ALL: [SelectionBehavior; 2] = [SelectionBehavior::Toggle, SelectionBehavior::Replace];
330
331    pub fn label(self) -> &'static str {
332        match self {
333            SelectionBehavior::Toggle => "Toggle",
334            SelectionBehavior::Replace => "Replace",
335        }
336    }
337}
338
339/// `placement` — where a floating panel sits relative to its trigger.
340///
341/// The full React Aria union v3 forwards: both physical spellings
342/// (`"bottom left"`, `"bottom right"`) and logical aliases (`"start"`,
343/// `"end top"`, …) for every side. This port has no RTL mode, so the logical
344/// start/end aliases resolve to the same pixels as their left/right
345/// spellings; the spellings stay distinct values so `ALL` enumerates the
346/// whole 22-value vocabulary a caller can name.
347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
348pub enum Placement {
349    /// `"bottom"` — below the trigger, centred.
350    Bottom,
351    /// `"bottom start"` — below the trigger, flush with its start edge.
352    #[default]
353    BottomStart,
354    /// `"bottom left"` — below the trigger, flush with its left edge; the
355    /// physical spelling of [`Placement::BottomStart`] in this LTR-only port.
356    BottomLeft,
357    /// `"bottom end"` — below the trigger, flush with its end edge.
358    BottomEnd,
359    /// `"bottom right"` — below the trigger, flush with its right edge; the
360    /// physical spelling of [`Placement::BottomEnd`] here.
361    BottomRight,
362    /// `"top"` — above the trigger, centred.
363    Top,
364    /// `"top start"` — above the trigger, flush with its start edge.
365    TopStart,
366    /// `"top left"` — above the trigger, flush with its left edge; the
367    /// physical spelling of [`Placement::TopStart`] here.
368    TopLeft,
369    /// `"top end"` — above the trigger, flush with its end edge.
370    TopEnd,
371    /// `"top right"` — above the trigger, flush with its right edge; the
372    /// physical spelling of [`Placement::TopEnd`] here.
373    TopRight,
374    /// `"left"` — beside the trigger's left edge, vertically centred.
375    Left,
376    /// `"left top"` — beside the left edge, flush with the trigger's top.
377    LeftTop,
378    /// `"left bottom"` — beside the left edge, flush with the trigger's
379    /// bottom.
380    LeftBottom,
381    /// `"right"` — beside the trigger's right edge, vertically centred.
382    Right,
383    /// `"right top"` — beside the right edge, flush with the trigger's top.
384    RightTop,
385    /// `"right bottom"` — beside the right edge, flush with the trigger's
386    /// bottom.
387    RightBottom,
388    /// `"start"` — the logical spelling of [`Placement::Left`] in an
389    /// LTR-only port.
390    Start,
391    /// `"start top"` — the logical spelling of [`Placement::LeftTop`] here.
392    StartTop,
393    /// `"start bottom"` — the logical spelling of [`Placement::LeftBottom`]
394    /// here.
395    StartBottom,
396    /// `"end"` — the logical spelling of [`Placement::Right`] in an
397    /// LTR-only port.
398    End,
399    /// `"end top"` — the logical spelling of [`Placement::RightTop`] here.
400    EndTop,
401    /// `"end bottom"` — the logical spelling of [`Placement::RightBottom`]
402    /// here.
403    EndBottom,
404}
405
406/// How a panel lines up along the trigger's cross axis.
407///
408/// The axis is relative to the side: for the top and bottom placements it is
409/// the trigger's horizontal axis, for the side placements its vertical one.
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum PlacementAlign {
412    Start,
413    Center,
414    End,
415}
416
417impl Placement {
418    pub const ALL: [Placement; 22] = [
419        Placement::Bottom,
420        Placement::BottomStart,
421        Placement::BottomLeft,
422        Placement::BottomEnd,
423        Placement::BottomRight,
424        Placement::Top,
425        Placement::TopStart,
426        Placement::TopLeft,
427        Placement::TopEnd,
428        Placement::TopRight,
429        Placement::Left,
430        Placement::LeftTop,
431        Placement::LeftBottom,
432        Placement::Right,
433        Placement::RightTop,
434        Placement::RightBottom,
435        Placement::Start,
436        Placement::StartTop,
437        Placement::StartBottom,
438        Placement::End,
439        Placement::EndTop,
440        Placement::EndBottom,
441    ];
442
443    pub fn label(self) -> &'static str {
444        match self {
445            Placement::Bottom => "Bottom",
446            Placement::BottomStart => "Bottom start",
447            Placement::BottomLeft => "Bottom left",
448            Placement::BottomEnd => "Bottom end",
449            Placement::BottomRight => "Bottom right",
450            Placement::Top => "Top",
451            Placement::TopStart => "Top start",
452            Placement::TopLeft => "Top left",
453            Placement::TopEnd => "Top end",
454            Placement::TopRight => "Top right",
455            Placement::Left => "Left",
456            Placement::LeftTop => "Left top",
457            Placement::LeftBottom => "Left bottom",
458            Placement::Right => "Right",
459            Placement::RightTop => "Right top",
460            Placement::RightBottom => "Right bottom",
461            Placement::Start => "Start",
462            Placement::StartTop => "Start top",
463            Placement::StartBottom => "Start bottom",
464            Placement::End => "End",
465            Placement::EndTop => "End top",
466            Placement::EndBottom => "End bottom",
467        }
468    }
469
470    /// Whether the panel opens upward.
471    pub fn is_above(self) -> bool {
472        matches!(
473            self,
474            Placement::Top
475                | Placement::TopStart
476                | Placement::TopLeft
477                | Placement::TopEnd
478                | Placement::TopRight
479        )
480    }
481
482    /// Whether the panel sits beside the trigger rather than above or below.
483    pub fn is_side(self) -> bool {
484        matches!(
485            self,
486            Placement::Left
487                | Placement::LeftTop
488                | Placement::LeftBottom
489                | Placement::Right
490                | Placement::RightTop
491                | Placement::RightBottom
492                | Placement::Start
493                | Placement::StartTop
494                | Placement::StartBottom
495                | Placement::End
496                | Placement::EndTop
497                | Placement::EndBottom
498        )
499    }
500
501    /// Whether the panel opens on the trigger's start side — the left edge,
502    /// because this port has no RTL mode.
503    pub fn is_start_side(self) -> bool {
504        matches!(
505            self,
506            Placement::Left
507                | Placement::LeftTop
508                | Placement::LeftBottom
509                | Placement::Start
510                | Placement::StartTop
511                | Placement::StartBottom
512        )
513    }
514
515    /// The alignment along the trigger's cross axis: horizontal for the top
516    /// and bottom placements, vertical for the side ones.
517    pub fn align(self) -> PlacementAlign {
518        match self {
519            Placement::BottomStart
520            | Placement::BottomLeft
521            | Placement::TopStart
522            | Placement::TopLeft
523            | Placement::LeftTop
524            | Placement::RightTop
525            | Placement::StartTop
526            | Placement::EndTop => PlacementAlign::Start,
527            Placement::BottomEnd
528            | Placement::BottomRight
529            | Placement::TopEnd
530            | Placement::TopRight
531            | Placement::LeftBottom
532            | Placement::RightBottom
533            | Placement::StartBottom
534            | Placement::EndBottom => PlacementAlign::End,
535            _ => PlacementAlign::Center,
536        }
537    }
538}