Skip to main content

fig_schema/
present.rs

1//! Renderer-neutral presentation hints. A frontend maps these to its own
2//! symbols and colours (SwiftUI → SF Symbols + adaptive `Color`; ratatui →
3//! unicode + ANSI). Carried on [`crate::FieldRule`] but never interpreted by
4//! this crate — purely a payload for the embedder's renderer.
5
6/// Presentation hints for one field rule.
7///
8/// `#[non_exhaustive]`: this is the type that grows every time a frontend needs
9/// a new hint, so it is built from [`Presentation::default`] and the chainable
10/// setters below rather than a struct literal. Reading the fields is unchanged.
11///
12/// ```
13/// use fig_schema::{Icon, Presentation, Tint};
14///
15/// let p = Presentation::default()
16///     .title("Audience")
17///     .description("Who may read this")
18///     .icon(Icon::Globe)
19///     .tint(Tint::Positive);
20/// assert_eq!(p.title.as_deref(), Some("Audience"));
21/// ```
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct Presentation {
25    /// A human field label.
26    pub title: Option<String>,
27    /// Help text / section subtitle.
28    pub description: Option<String>,
29    /// A semantic icon.
30    pub icon: Option<Icon>,
31    /// A semantic tint.
32    pub tint: Option<Tint>,
33}
34
35impl Presentation {
36    /// Set the human field label.
37    pub fn title(mut self, title: impl Into<String>) -> Self {
38        self.title = Some(title.into());
39        self
40    }
41
42    /// Set the label from an optional one — for a caller reading a config where
43    /// the title may be absent. `None` leaves it unset.
44    pub fn title_opt(mut self, title: Option<impl Into<String>>) -> Self {
45        self.title = title.map(Into::into);
46        self
47    }
48
49    /// Set the help text.
50    pub fn description(mut self, description: impl Into<String>) -> Self {
51        self.description = Some(description.into());
52        self
53    }
54
55    /// Set the help text from an optional one. `None` leaves it unset.
56    pub fn description_opt(mut self, description: Option<impl Into<String>>) -> Self {
57        self.description = description.map(Into::into);
58        self
59    }
60
61    /// Set the semantic icon. Takes an [`Icon`] or an `Option<Icon>`.
62    pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
63        self.icon = icon.into();
64        self
65    }
66
67    /// Set the semantic tint. Takes a [`Tint`] or an `Option<Tint>`.
68    pub fn tint(mut self, tint: impl Into<Option<Tint>>) -> Self {
69        self.tint = tint.into();
70        self
71    }
72}
73
74/// A semantic icon hint. Frontends map to their own symbol set.
75///
76/// `#[non_exhaustive]`: the set grows as fields do, so a `match` needs a `_`
77/// arm. Constructing a variant is unaffected.
78#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum Icon {
81    Link,
82    Enum,
83    Toggle,
84    Lock,
85    Globe,
86    Clock,
87    Tag,
88    Text,
89    /// An escape hatch naming a frontend-specific symbol.
90    Other(String),
91}
92
93/// A semantic tint hint. Frontends map to theme-adaptive colours.
94///
95/// `#[non_exhaustive]`: a palette grows the same way an icon set does, so a
96/// `match` needs a `_` arm.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum Tint {
100    Accent,
101    Neutral,
102    Positive,
103    Warning,
104    Danger,
105}
106
107impl Tint {
108    /// Every tint, so a frontend can assert its colour mapping is total —
109    /// `for t in Tint::ALL { assert!(my_colour(*t).is_some()) }` — instead of
110    /// keeping a second copy of this list that silently falls behind.
111    ///
112    /// [`Icon`] deliberately has no equivalent, and the asymmetry is the point
113    /// rather than an oversight. Every other `#[non_exhaustive]` enum here is
114    /// safe against a new variant because *this crate never produces one*: it
115    /// defines the vocabulary, and every value in a workspace is constructed by
116    /// the embedder, so a new variant can only reach a `match` through a
117    /// deliberate, reviewed change to the producer. [`Tint`] is the one where
118    /// that gate is contingent — [`parse_vocabulary`](crate::parse_vocabulary)
119    /// does not read a per-term `tint:` today, but [`Term::tint`](crate::Term)
120    /// exists precisely so a vocabulary can say `public` reads green, and the
121    /// natural place to author that is beside the term in the document. The day
122    /// the parser learns that key, a `Tint` arrives from *user data* and the
123    /// gate becomes "someone edited a file". This list has to already exist for
124    /// that change to turn consumer tests red instead of shipping a term that
125    /// silently renders untinted.
126    pub const ALL: &'static [Tint] = &[
127        Tint::Accent,
128        Tint::Neutral,
129        Tint::Positive,
130        Tint::Warning,
131        Tint::Danger,
132    ];
133}