Skip to main content

termgrid_core/
registry.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use unicode_width::UnicodeWidthStr;
5
6/// The Unicode baseline assumed by BBSstalgia's default profiles.
7///
8/// This crate is designed so that *width policy is explicit* via [`RenderProfile`].
9/// The baseline here documents the project's target when shipping built-in
10/// example profiles.
11pub const UNICODE_BASELINE: (u8, u8, u8) = (11, 0, 0);
12
13/// Per-glyph metadata captured in a render profile.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct GlyphInfo {
16    /// The *policy* display width for this glyph or grapheme cluster.
17    ///
18    /// Typical values are 1 or 2.
19    pub width: u8,
20}
21
22/// A serializable render profile.
23///
24/// This is the data you ship and version-control. The engine (or any consumer)
25/// loads it to create a `GlyphRegistry`.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct RenderProfile {
28    pub id: String,
29    pub version: u32,
30    pub glyphs: BTreeMap<String, GlyphInfo>,
31}
32
33/// Runtime registry used for width lookups.
34#[derive(Debug, Clone)]
35pub struct GlyphRegistry {
36    profile: RenderProfile,
37}
38
39impl GlyphRegistry {
40    /// Create a runtime registry from a serialized profile.
41    pub fn new(profile: RenderProfile) -> Self {
42        Self { profile }
43    }
44
45    /// Returns the underlying profile (useful for debugging and version checks).
46    pub fn profile(&self) -> &RenderProfile {
47        &self.profile
48    }
49
50    /// Returns the policy width for the given grapheme cluster.
51    ///
52    /// - If the grapheme appears in the profile, that width wins.
53    /// - Otherwise, we fall back to a conservative Unicode-width estimate.
54    pub fn width(&self, grapheme: &str) -> u8 {
55        if let Some(info) = self.profile.glyphs.get(grapheme) {
56            return info.width.clamp(1, 2);
57        }
58        // Fallback: do the best we can. This is *not* authoritative.
59        let w = UnicodeWidthStr::width(grapheme);
60        let w = if w == 0 { 1 } else { w };
61        (w.min(2)) as u8
62    }
63}
64
65impl RenderProfile {
66    pub fn empty(id: impl Into<String>, version: u32) -> Self {
67        Self {
68            id: id.into(),
69            version,
70            glyphs: BTreeMap::new(),
71        }
72    }
73
74    /// Built-in example profile tuned for BBSstalgia + xterm.js.
75    ///
76    /// This is intentionally small and exists to provide a safe starting point.
77    /// Real deployments should version-control their own profile JSON.
78    ///
79    /// The returned profile matches `testdata/profile_example.json`.
80    pub fn bbsstalgia_xtermjs_unicode11_example() -> Self {
81        let mut p = RenderProfile::empty("bbsstalgia-xtermjs-unicode11", 1);
82        p.set_width("🙂", 2);
83        p.set_width("⚙️", 2);
84        p.set_width("🧠", 2);
85        // HEART SUIT is commonly rendered as narrow unless paired with VS-16.
86        // We include it as an example of an explicit override.
87        p.set_width("❤", 1);
88        p
89    }
90}
91
92impl RenderProfile {
93    /// Set or replace the width policy for a glyph/grapheme.
94    pub fn set_width(&mut self, glyph: impl Into<String>, width: u8) {
95        let w = width.clamp(1, 2);
96        self.glyphs.insert(glyph.into(), GlyphInfo { width: w });
97    }
98
99    /// Merge another profile's glyph table into this one (other wins on conflicts).
100    ///
101    /// This is intentionally a simple data operation. Profile identity/versioning
102    /// is left to the caller.
103    pub fn merge_glyphs_from(&mut self, other: &RenderProfile) {
104        for (g, info) in &other.glyphs {
105            self.glyphs.insert(g.clone(), info.clone());
106        }
107    }
108}