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}