Skip to main content

tern_sdk/ui/
types.rs

1//! Typed prop values: styled text, enumerations (each with an escape hatch
2//! for values newer than the SDK), sizes and actions.
3
4use serde::{Serialize, Serializer};
5
6/// Defines a string enumeration with an `Other` escape hatch.
7macro_rules! string_enum {
8	($(#[$m:meta])* $name:ident { $($(#[$vm:meta])* $var:ident = $s:literal),* $(,)? }) => {
9		$(#[$m])*
10		#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11		pub enum $name {
12			$($(#[$vm])* $var,)*
13			/// A value this SDK doesn't name.
14			Other(String),
15		}
16
17		impl $name {
18			/// The value on the wire.
19			pub fn as_str(&self) -> &str {
20				match self {
21					$(Self::$var => $s,)*
22					Self::Other(s) => s,
23				}
24			}
25		}
26
27		impl From<&str> for $name {
28			fn from(s: &str) -> Self {
29				match s {
30					$($s => Self::$var,)*
31					_ => Self::Other(s.to_owned()),
32				}
33			}
34		}
35
36		impl From<String> for $name {
37			fn from(s: String) -> Self {
38				Self::from(s.as_str())
39			}
40		}
41
42		impl ::std::fmt::Display for $name {
43			fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
44				f.write_str(self.as_str())
45			}
46		}
47
48		impl ::serde::Serialize for $name {
49			fn serialize<S: ::serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
50				s.serialize_str(self.as_str())
51			}
52		}
53	};
54}
55
56pub(crate) use string_enum;
57
58string_enum!(
59	/// The semantic color of a node's chrome.
60	Tone {
61		/// Gray.
62		Neutral = "neutral",
63		/// The accent.
64		Accent = "accent",
65		/// Informational.
66		Info = "info",
67		/// Success.
68		Success = "success",
69		/// Warning.
70		Warning = "warning",
71		/// Error.
72		Error = "error",
73		/// Pending.
74		Pending = "pending",
75		/// Quiet.
76		Muted = "muted",
77		/// The user's own.
78		User = "user",
79	}
80);
81
82string_enum!(
83	/// The state of a card or tool.
84	Status {
85		/// Not started.
86		Pending = "pending",
87		/// In progress.
88		Running = "running",
89		/// Finished.
90		Done = "done",
91		/// Failed.
92		Error = "error",
93		/// Stopped.
94		Cancelled = "cancelled",
95	}
96);
97
98string_enum!(
99	/// The state of an agent.
100	AgentStatus {
101		/// Not started.
102		Pending = "pending",
103		/// In progress.
104		Running = "running",
105		/// Finished.
106		Done = "done",
107		/// Failed.
108		Failed = "failed",
109		/// Stopped.
110		Aborted = "aborted",
111		/// Waiting for work.
112		Idle = "idle",
113		/// Parked.
114		Parked = "parked",
115	}
116);
117
118string_enum!(
119	/// A transient selection mark drawn over a node.
120	Mark {
121		/// An accent bar at the node's left.
122		Pick = "pick",
123		/// Dims the node.
124		Drop = "drop",
125	}
126);
127
128string_enum!(
129	/// Space between children, or a spacer's height.
130	Gap {
131		/// None.
132		None = "none",
133		/// 2px.
134		Xs = "xs",
135		/// 6px.
136		Sm = "sm",
137		/// 10px (12px for a spacer).
138		Md = "md",
139		/// 16px (20px for a spacer).
140		Lg = "lg",
141	}
142);
143
144string_enum!(
145	/// Cross-axis alignment of a row's or column's children.
146	Align {
147		/// Start.
148		Start = "start",
149		/// Center.
150		Center = "center",
151		/// End.
152		End = "end",
153		/// Text baselines.
154		Baseline = "baseline",
155		/// Stretch.
156		Stretch = "stretch",
157	}
158);
159
160string_enum!(
161	/// Main-axis distribution of a row's or column's children.
162	Justify {
163		/// Space between.
164		Between = "between",
165		/// Packed at the end.
166		End = "end",
167	}
168);
169
170string_enum!(
171	/// How text wraps.
172	Wrap {
173		/// Between words.
174		Word = "word",
175		/// Anywhere.
176		Char = "char",
177		/// Never.
178		None = "none",
179	}
180);
181
182string_enum!(
183	/// Where overflowing text is cut.
184	Truncate {
185		/// Keep the start.
186		End = "end",
187		/// Keep the end.
188		Start = "start",
189		/// Keep both ends.
190		Middle = "middle",
191	}
192);
193
194string_enum!(
195	/// Text alignment in a table column.
196	TextAlign {
197		/// Start.
198		Start = "start",
199		/// Center.
200		Center = "center",
201		/// End (numbers).
202		End = "end",
203	}
204);
205
206string_enum!(
207	/// A span effect.
208	Fx {
209		/// A highlight sweeping across.
210		Shimmer = "shimmer",
211		/// Fades in and out.
212		Pulse = "pulse",
213		/// None.
214		None = "none",
215	}
216);
217
218string_enum!(
219	/// A span style token (several join with spaces; palette tokens through
220	/// `Other`).
221	Token {
222		/// Quiet text.
223		Muted = "muted",
224		/// Quieter text.
225		Dim = "dim",
226		/// Weight 600.
227		Strong = "strong",
228		/// Italic.
229		Em = "em",
230		/// The accent color.
231		Accent = "accent",
232		/// Success color.
233		Success = "success",
234		/// Warning color.
235		Warning = "warning",
236		/// Error color.
237		Error = "error",
238		/// Info color.
239		Info = "info",
240		/// An inline code chip.
241		Code = "code",
242		/// Tabular figures.
243		Mono = "mono",
244		/// A file path.
245		Path = "path",
246		/// A keycap.
247		Key = "key",
248		/// Link color.
249		Link = "link",
250		/// A number.
251		Num = "num",
252		/// Inserted text.
253		Ins = "ins",
254		/// Deleted text.
255		Del = "del",
256		/// A highlight.
257		Mark = "mark",
258		/// A misspelled word.
259		Typo = "typo",
260		/// A Nerd Font glyph.
261		Icon = "icon",
262		/// Not drawn.
263		Hide = "hide",
264	}
265);
266
267string_enum!(
268	/// A spinner's indicator.
269	SpinnerStyle {
270		/// Braille dots.
271		Braille = "braille",
272		/// Three pulsing dots.
273		Dots = "dots",
274		/// omp's thinking glyph.
275		Starburst = "starburst",
276		/// A dot circling a ring.
277		Orbit = "orbit",
278	}
279);
280
281string_enum!(
282	/// How a shimmer sweeps.
283	ShimmerMode {
284		/// Left to right, restarting.
285		Classic = "classic",
286		/// Back and forth.
287		Kitt = "kitt",
288	}
289);
290
291string_enum!(
292	/// How an elapsed timer reads.
293	ElapsedFormat {
294		/// `1m 5s`.
295		Short = "short",
296		/// `1:05`.
297		Clock = "clock",
298	}
299);
300
301string_enum!(
302	/// A meter's shape.
303	MeterStyle {
304		/// A rounded track.
305		Bar = "bar",
306		/// A ring.
307		Ring = "ring",
308		/// A grid of squares.
309		Blocks = "blocks",
310	}
311);
312
313string_enum!(
314	/// Small, medium or large.
315	Size {
316		/// Small.
317		Sm = "sm",
318		/// Medium.
319		Md = "md",
320		/// Large.
321		Lg = "lg",
322	}
323);
324
325string_enum!(
326	/// Which chart.
327	ChartKind {
328		/// A heatmap of cells.
329		Heatmap = "heatmap",
330		/// Bars.
331		Bars = "bars",
332		/// A sparkline.
333		Spark = "spark",
334	}
335);
336
337string_enum!(
338	/// A thinking-effort rung.
339	EffortLevel {
340		/// 0/6.
341		Off = "off",
342		/// 1/6.
343		Minimal = "minimal",
344		/// 2/6.
345		Low = "low",
346		/// 3/6.
347		Medium = "medium",
348		/// 4/6.
349		High = "high",
350		/// 5/6.
351		Xhigh = "xhigh",
352		/// Full, on fire.
353		Max = "max",
354	}
355);
356
357string_enum!(
358	/// How a diff lays out.
359	DiffMode {
360		/// One column.
361		Unified = "unified",
362		/// Side by side.
363		Split = "split",
364		/// The user's setting.
365		Auto = "auto",
366	}
367);
368
369string_enum!(
370	/// How a `kv` lays out.
371	KvLayout {
372		/// Aligned rows.
373		Grid = "grid",
374		/// One wrapping line.
375		Inline = "inline",
376	}
377);
378
379string_enum!(
380	/// A text's line-length cap.
381	Measure {
382		/// 80 cells.
383		Prose = "prose",
384	}
385);
386
387string_enum!(
388	/// A card variant.
389	CardVariant {
390		/// No ring, fill or rounding.
391		Bare = "bare",
392	}
393);
394
395string_enum!(
396	/// A picker sheet's size.
397	PickerSize {
398		/// Medium.
399		Md = "md",
400		/// Large.
401		Lg = "lg",
402		/// The whole pane.
403		Screen = "screen",
404	}
405);
406
407string_enum!(
408	/// A picker's row template.
409	PickerLayout {
410		/// Rows.
411		Rows = "rows",
412		/// Cards (detail on its own line).
413		Cards = "cards",
414		/// A node per row.
415		Timeline = "timeline",
416		/// Depth guides and chevrons.
417		Tree = "tree",
418	}
419);
420
421string_enum!(
422	/// Where a picker's preview sits.
423	PickerPreview {
424		/// Beside the list.
425		Side = "side",
426		/// Under the list.
427		Below = "below",
428		/// No preview.
429		None = "none",
430	}
431);
432
433string_enum!(
434	/// Which part of a picker the keys drive.
435	PickerFocus {
436		/// The list.
437		List = "list",
438		/// The scope column.
439		Scopes = "scopes",
440		/// The tab row.
441		Tabs = "tabs",
442		/// The chip strip.
443		Strip = "strip",
444		/// The preview.
445		Preview = "preview",
446	}
447);
448
449string_enum!(
450	/// A picker's state.
451	PickerState {
452		/// Rows shown.
453		Ready = "ready",
454		/// Skeleton rows.
455		Loading = "loading",
456		/// An error card.
457		Error = "error",
458	}
459);
460
461string_enum!(
462	/// How a tool's string target is drawn.
463	TargetKind {
464		/// A shell line.
465		Command = "command",
466		/// A file path.
467		Path = "path",
468		/// A pattern in quotes.
469		Pattern = "pattern",
470		/// A query in quotes.
471		Query = "query",
472		/// Text.
473		Text = "text",
474	}
475);
476
477string_enum!(
478	/// A tool's frame.
479	ToolFrame {
480		/// A ring.
481		Card = "card",
482		/// No ring.
483		Inline = "inline",
484	}
485);
486
487string_enum!(
488	/// A checklist's presentation.
489	ChecklistMode {
490		/// The whole list.
491		Full = "full",
492		/// A pill.
493		Hud = "hud",
494		/// A reminder line.
495		Reminder = "reminder",
496	}
497);
498
499string_enum!(
500	/// A checklist item's state.
501	ItemStatus {
502		/// Not started.
503		Pending = "pending",
504		/// In progress.
505		Active = "active",
506		/// Finished.
507		Done = "done",
508		/// Dropped.
509		Dropped = "dropped",
510		/// Blocked.
511		Blocked = "blocked",
512	}
513);
514
515string_enum!(
516	/// An overlay's size.
517	OverlaySize {
518		/// Small.
519		Sm = "sm",
520		/// Medium.
521		Md = "md",
522		/// Large.
523		Lg = "lg",
524		/// A full sheet.
525		Full = "full",
526	}
527);
528
529string_enum!(
530	/// Which group of a status strip a segment sits in.
531	Side {
532		/// The right group.
533		Right = "right",
534	}
535);
536
537string_enum!(
538	/// A picker column's format.
539	ColumnFormat {
540		/// Text.
541		Text = "text",
542		/// A number.
543		Num = "num",
544		/// A price.
545		Price = "price",
546		/// A meter.
547		Bar = "bar",
548		/// A time.
549		Time = "time",
550		/// A duration.
551		Elapsed = "elapsed",
552		/// Dim text.
553		Dim = "dim",
554	}
555);
556
557string_enum!(
558	/// How a `prefs` choice draws.
559	ChoiceStyle {
560		/// Segments when few and short, else a menu.
561		Auto = "auto",
562		/// Segments.
563		Segmented = "segmented",
564		/// A popup menu.
565		Menu = "menu",
566	}
567);
568
569string_enum!(
570	/// The type of an `el` `input`.
571	InputType {
572		/// A checkbox.
573		Checkbox = "checkbox",
574		/// A radio.
575		Radio = "radio",
576	}
577);
578
579/// One styled run of text: `{t, s?, fx?, href?}`.
580#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
581pub struct Span {
582	/// The text.
583	pub t:    String,
584	/// Space-separated style tokens.
585	#[serde(skip_serializing_if = "Option::is_none")]
586	pub s:    Option<String>,
587	/// An effect.
588	#[serde(skip_serializing_if = "Option::is_none")]
589	pub fx:   Option<Fx>,
590	/// A link.
591	#[serde(skip_serializing_if = "Option::is_none")]
592	pub href: Option<String>,
593}
594
595impl Span {
596	/// A plain span.
597	pub fn new(t: impl Into<String>) -> Self {
598		Self { t: t.into(), ..Self::default() }
599	}
600
601	/// Adds a style token (or several, space-separated).
602	pub fn style(mut self, token: impl Into<Token>) -> Self {
603		let token = token.into();
604		self.s = Some(match self.s.take() {
605			Some(s) if !s.is_empty() => format!("{s} {token}"),
606			_ => token.to_string(),
607		});
608		self
609	}
610
611	/// Sets the effect.
612	pub fn fx(mut self, fx: impl Into<Fx>) -> Self {
613		self.fx = Some(fx.into());
614		self
615	}
616
617	/// Makes the span a link.
618	pub fn href(mut self, href: impl Into<String>) -> Self {
619		self.href = Some(href.into());
620		self
621	}
622}
623
624impl From<&str> for Span {
625	fn from(t: &str) -> Self {
626		Self::new(t)
627	}
628}
629
630impl From<String> for Span {
631	fn from(t: String) -> Self {
632		Self::new(t)
633	}
634}
635
636/// Text given as a plain string or as styled spans.
637#[derive(Clone, Debug, PartialEq, Eq)]
638pub enum Text {
639	/// A plain string.
640	Plain(String),
641	/// Styled spans.
642	Spans(Vec<Span>),
643}
644
645impl Text {
646	/// The text without styles.
647	pub fn plain(&self) -> String {
648		match self {
649			Self::Plain(s) => s.clone(),
650			Self::Spans(spans) => spans.iter().map(|s| s.t.as_str()).collect(),
651		}
652	}
653}
654
655impl Default for Text {
656	fn default() -> Self {
657		Self::Plain(String::new())
658	}
659}
660
661impl Serialize for Text {
662	fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
663		match self {
664			Self::Plain(t) => s.serialize_str(t),
665			Self::Spans(spans) => spans.serialize(s),
666		}
667	}
668}
669
670impl From<&str> for Text {
671	fn from(t: &str) -> Self {
672		Self::Plain(t.to_owned())
673	}
674}
675
676impl From<String> for Text {
677	fn from(t: String) -> Self {
678		Self::Plain(t)
679	}
680}
681
682impl From<&String> for Text {
683	fn from(t: &String) -> Self {
684		Self::Plain(t.clone())
685	}
686}
687
688impl From<Span> for Text {
689	fn from(span: Span) -> Self {
690		Self::Spans(vec![span])
691	}
692}
693
694impl From<Vec<Span>> for Text {
695	fn from(spans: Vec<Span>) -> Self {
696		Self::Spans(spans)
697	}
698}
699
700impl<const N: usize> From<[Span; N]> for Text {
701	fn from(spans: [Span; N]) -> Self {
702		Self::Spans(spans.into())
703	}
704}
705
706/// A size bound: `"<n>ch"`, `"<n>lines"` or a fraction of the parent.
707#[derive(Clone, Copy, Debug, PartialEq)]
708pub enum Extent {
709	/// Character cells.
710	Ch(f64),
711	/// Text lines.
712	Lines(f64),
713	/// A fraction of the parent (`0.5` is half).
714	Fraction(f64),
715}
716
717impl Serialize for Extent {
718	fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
719		match *self {
720			Self::Ch(n) => s.collect_str(&format_args!("{n}ch")),
721			Self::Lines(n) => s.collect_str(&format_args!("{n}lines")),
722			Self::Fraction(f) => s.serialize_f64(f),
723		}
724	}
725}
726
727/// Size bounds `{w?, h?}` for `min` and `max`.
728#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
729pub struct Bound {
730	/// The width bound.
731	#[serde(skip_serializing_if = "Option::is_none")]
732	pub w: Option<Extent>,
733	/// The height bound.
734	#[serde(skip_serializing_if = "Option::is_none")]
735	pub h: Option<Extent>,
736}
737
738impl Bound {
739	/// A width bound.
740	pub const fn w(w: Extent) -> Self {
741		Self { w: Some(w), h: None }
742	}
743
744	/// A height bound.
745	pub const fn h(h: Extent) -> Self {
746		Self { w: None, h: Some(h) }
747	}
748
749	/// Both bounds.
750	pub const fn wh(w: Extent, h: Extent) -> Self {
751		Self { w: Some(w), h: Some(h) }
752	}
753}
754
755/// A flex basis.
756#[derive(Clone, Copy, Debug, PartialEq)]
757pub enum Basis {
758	/// A fraction of the parent.
759	Fraction(f64),
760	/// The node's own size, without shrinking.
761	Content,
762}
763
764impl Serialize for Basis {
765	fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
766		match *self {
767			Self::Fraction(f) => s.serialize_f64(f),
768			Self::Content => s.serialize_str("content"),
769		}
770	}
771}
772
773impl From<f64> for Basis {
774	fn from(f: f64) -> Self {
775		Self::Fraction(f)
776	}
777}
778
779/// A node key: a string, or an integer named in decimal.
780#[derive(Clone, Debug, PartialEq, Eq, Hash)]
781pub enum NodeKey {
782	/// A string key.
783	Str(String),
784	/// An integer key.
785	Int(i64),
786}
787
788impl Serialize for NodeKey {
789	fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
790		match self {
791			Self::Str(k) => s.serialize_str(k),
792			Self::Int(n) => s.serialize_i64(*n),
793		}
794	}
795}
796
797impl From<&str> for NodeKey {
798	fn from(k: &str) -> Self {
799		Self::Str(k.to_owned())
800	}
801}
802
803impl From<String> for NodeKey {
804	fn from(k: String) -> Self {
805		Self::Str(k)
806	}
807}
808
809impl From<&String> for NodeKey {
810	fn from(k: &String) -> Self {
811		Self::Str(k.clone())
812	}
813}
814
815/// Implements `From<integer>` for [`NodeKey`].
816macro_rules! int_key {
817	($($t:ty),*) => {
818		$(impl From<$t> for NodeKey {
819			fn from(n: $t) -> Self {
820				Self::Int(n as i64)
821			}
822		})*
823	};
824}
825
826int_key!(i32, i64, u32, u64, usize);
827
828/// What a pointer does on a node: `{click?, dblclick?, menu?}`.
829#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
830pub struct Actions {
831	/// The click action.
832	#[serde(skip_serializing_if = "Option::is_none")]
833	pub click:    Option<String>,
834	/// The double-click action.
835	#[serde(skip_serializing_if = "Option::is_none")]
836	pub dblclick: Option<String>,
837	/// Context-menu actions.
838	#[serde(skip_serializing_if = "Vec::is_empty")]
839	pub menu:     Vec<String>,
840}
841
842impl Actions {
843	/// No actions.
844	pub fn new() -> Self {
845		Self::default()
846	}
847
848	/// Sets the click action.
849	pub fn click(mut self, act: impl Into<String>) -> Self {
850		self.click = Some(act.into());
851		self
852	}
853
854	/// Sets the double-click action.
855	pub fn dblclick(mut self, act: impl Into<String>) -> Self {
856		self.dblclick = Some(act.into());
857		self
858	}
859
860	/// Adds a context-menu action.
861	pub fn menu(mut self, act: impl Into<String>) -> Self {
862		self.menu.push(act.into());
863		self
864	}
865}