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}