Skip to main content

lattice_theme/
element.rs

1//! Theme-element identity + the authoring (reference) style form.
2//!
3//! A *theme element* is a named, semantic styleable role
4//! (`syntax.keyword`, `diff.add.sign`, `markdown.heading.1`). It
5//! carries no color — only identity, an owner, and a default
6//! [`StyleSpec`]. A `StyleSpec` describes how an element is styled
7//! *by reference*: colors name a [`PaletteKey`](crate::PaletteKey)
8//! rather than baking an absolute RGB, and a spec may `inherit`
9//! another element and override specific attributes (emacs
10//! `:inherit`, vim `:hi link`).
11//!
12//! Resolution (`crate::registry`) turns a `StyleSpec` into a
13//! concrete [`Style`](crate::Style) once at theme-build time.
14//!
15//! Design: `docs/dev/architecture/theme-system.md` §3.1–§3.2.
16
17use std::borrow::Cow;
18
19use crate::{Color, FamilyId, Weight};
20
21/// Interned, process-stable index for a registered theme element.
22/// Allocated at registration; the hot-path read is
23/// `resolved.get(id)` — an array index, no string hashing.
24#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct ElementId(pub u32);
26
27impl ElementId {
28    /// Out-of-range sentinel. `ResolvedTheme::get(INVALID)` returns
29    /// `Style::empty()` (out-of-range ids are styleless, never a
30    /// panic), so it is the safe default + not-found fallback for the
31    /// id-capture helpers ([`crate::BuiltinElementIds`]).
32    pub const INVALID: ElementId = ElementId(u32::MAX);
33}
34
35/// A theme element's dotted, hierarchical name (`markdown.heading.1`).
36/// Fallback walks the dotted parents (`markdown.heading.1` →
37/// `markdown.heading` → `markdown`) when the more-specific element is
38/// unstyled by the active theme.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct ElementName(Cow<'static, str>);
41
42impl ElementName {
43    /// Construct from a `&'static str` (the builtin path).
44    pub const fn from_static(s: &'static str) -> Self {
45        ElementName(Cow::Borrowed(s))
46    }
47
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// The dotted parent, if any: `"a.b.c"` → `"a.b"`, `"a"` → `None`.
53    /// Used for hierarchical fallback at resolution time.
54    pub fn parent(&self) -> Option<ElementName> {
55        self.0
56            .rsplit_once('.')
57            .map(|(head, _)| ElementName(Cow::Owned(head.to_string())))
58    }
59}
60
61impl From<&'static str> for ElementName {
62    fn from(s: &'static str) -> Self {
63        ElementName::from_static(s)
64    }
65}
66
67impl From<String> for ElementName {
68    fn from(s: String) -> Self {
69        ElementName(Cow::Owned(s))
70    }
71}
72
73impl std::fmt::Display for ElementName {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str(&self.0)
76    }
77}
78
79/// Who owns an element (and thus owns its default styling). Core
80/// elements ship with the editor; modes/plugins register their own.
81///
82/// The mode/plugin id is a string rather than a `ModeId` /
83/// `PluginId` because `lattice-theme` is a leaf crate — it cannot
84/// depend on `lattice-mode`. Callers pass `mode.id().as_str()`.
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub enum ElementOwner {
87    Core,
88    Mode(Cow<'static, str>),
89    Plugin(Cow<'static, str>),
90}
91
92/// A color by reference. `Palette` is the normal path; `Literal` is
93/// the escape hatch for a one-off a palette entry would
94/// over-generalize; `Default` means the terminal/window default
95/// channel.
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub enum ColorRef {
98    Palette(crate::PaletteKey),
99    Literal(Color),
100    Default,
101}
102
103impl From<crate::PaletteKey> for ColorRef {
104    fn from(k: crate::PaletteKey) -> Self {
105        ColorRef::Palette(k)
106    }
107}
108
109/// A bare string is the common case: a palette-key reference. So
110/// `spec.fg("purple")` means "the palette's `mauve`", not a literal —
111/// reference-not-absolute by default (design §2). Use
112/// `ColorRef::Literal(..)` explicitly for a one-off color.
113impl From<&'static str> for ColorRef {
114    fn from(s: &'static str) -> Self {
115        ColorRef::Palette(crate::PaletteKey::from_static(s))
116    }
117}
118
119impl From<Color> for ColorRef {
120    fn from(c: Color) -> Self {
121        ColorRef::Literal(c)
122    }
123}
124
125/// Tri-state modifier set: `Some(true)` sets, `Some(false)` clears,
126/// `None` inherits. Inheritance can therefore *clear* a parent's
127/// bold, not only add — emacs faces distinguish "unspecified" from
128/// "off".
129#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
130pub struct ModifierSet {
131    pub bold: Option<bool>,
132    pub italic: Option<bool>,
133    pub underline: Option<bool>,
134    pub dim: Option<bool>,
135    pub reverse: Option<bool>,
136}
137
138/// How an element is styled, by reference. The form a mode default,
139/// a theme override, or a buffer-local remap is written in.
140/// Resolution (`crate::registry`) produces a concrete
141/// [`Style`](crate::Style).
142#[derive(Debug, Default, Clone, PartialEq)]
143pub struct StyleSpec {
144    /// Inherit another element's resolved style; this spec's set
145    /// fields override. Resolved by walking the chain at build time.
146    pub inherit: Option<ElementName>,
147    pub fg: Option<ColorRef>,
148    pub bg: Option<ColorRef>,
149    pub modifiers: ModifierSet,
150    /// Relative height ratio (emacs `:height` float). Resolution
151    /// quantizes to fixed-point [`FontScale`](crate::FontScale).
152    pub scale: Option<f32>,
153    pub family: Option<FamilyId>,
154    pub weight: Option<Weight>,
155}
156
157impl StyleSpec {
158    /// An empty spec — resolves to `Style::empty()` (or the inherit
159    /// base) with no overrides.
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Inherit another element's resolved style.
165    pub fn inherit(mut self, name: impl Into<ElementName>) -> Self {
166        self.inherit = Some(name.into());
167        self
168    }
169
170    /// Set the foreground by reference (palette key, literal, or
171    /// default). Accepts a `PaletteKey`, a `Color`, or a `ColorRef`.
172    pub fn fg(mut self, fg: impl Into<ColorRef>) -> Self {
173        self.fg = Some(fg.into());
174        self
175    }
176
177    /// Set the background by reference.
178    pub fn bg(mut self, bg: impl Into<ColorRef>) -> Self {
179        self.bg = Some(bg.into());
180        self
181    }
182
183    pub fn bold(mut self) -> Self {
184        self.modifiers.bold = Some(true);
185        self
186    }
187
188    pub fn italic(mut self) -> Self {
189        self.modifiers.italic = Some(true);
190        self
191    }
192
193    pub fn underline(mut self) -> Self {
194        self.modifiers.underline = Some(true);
195        self
196    }
197
198    pub fn dim(mut self) -> Self {
199        self.modifiers.dim = Some(true);
200        self
201    }
202
203    pub fn reverse(mut self) -> Self {
204        self.modifiers.reverse = Some(true);
205        self
206    }
207
208    /// Explicitly clear a modifier the inherit base set.
209    pub fn no_bold(mut self) -> Self {
210        self.modifiers.bold = Some(false);
211        self
212    }
213
214    /// Set the relative height ratio (rich vocabulary).
215    pub fn scale(mut self, ratio: f32) -> Self {
216        self.scale = Some(ratio);
217        self
218    }
219
220    pub fn family(mut self, family: FamilyId) -> Self {
221        self.family = Some(family);
222        self
223    }
224
225    pub fn weight(mut self, weight: Weight) -> Self {
226        self.weight = Some(weight);
227        self
228    }
229}
230
231/// A registered theme element: identity + metadata + the
232/// owner-supplied default (itself a reference-form [`StyleSpec`]).
233#[derive(Debug, Clone)]
234pub struct ThemeElement {
235    pub id: ElementId,
236    pub name: ElementName,
237    pub owner: ElementOwner,
238    pub default: StyleSpec,
239    /// Self-documenting help (`:describe-element`, design §8).
240    pub doc: &'static str,
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn element_name_parent_walks_dotted_segments() {
249        let n = ElementName::from_static("markdown.heading.1");
250        let p = n.parent().expect("has parent");
251        assert_eq!(p.as_str(), "markdown.heading");
252        let pp = p.parent().expect("has grandparent");
253        assert_eq!(pp.as_str(), "markdown");
254        assert_eq!(pp.parent(), None);
255    }
256
257    #[test]
258    fn color_ref_from_conversions() {
259        assert_eq!(
260            ColorRef::from(Color::Rgb(1, 2, 3)),
261            ColorRef::Literal(Color::Rgb(1, 2, 3))
262        );
263    }
264
265    #[test]
266    fn modifier_set_defaults_to_all_inherit() {
267        let m = ModifierSet::default();
268        assert_eq!(m.bold, None);
269        assert_eq!(m.reverse, None);
270    }
271
272    #[test]
273    fn style_spec_builder_sets_tri_state_modifiers() {
274        let s = StyleSpec::new().bold().no_bold();
275        assert_eq!(s.modifiers.bold, Some(false));
276        let s2 = StyleSpec::new().italic();
277        assert_eq!(s2.modifiers.italic, Some(true));
278        assert_eq!(s2.modifiers.bold, None);
279    }
280}