Skip to main content

ui/
symbol.rs

1// symbol -- multi-colour vector symbols (device pictures for diagrams and palettes): vicon's
2// layers with palette roles and stroke widths, parsed once, drawn as vectors or rasterised
3// once per size and palette and blitted
4//
5//   Symbol::parse(art, vb) -> Symbol     art: SVG path layers separated by '|' on a vb[0] x
6//                                        vb[1] grid (tools/vsym.py writes them from SVG files)
7//   sym.paint(g, x, y, scale, &pal)      every layer, the grid's (0, 0) at x, y (the draw
8//                                        context's coordinates, like Dc::fill), scale = px
9//                                        per grid unit
10//   sym.raster(g, w, &pal) -> Surface    the symbol w px wide in a PARGB surface
11//   sym.height(w) -> px                  the height of the symbol drawn w px wide
12//   SymPalette { c: [u32; 10] }          the colours of the roles SYM_INK .. SYM_HI
13//   SymPalette::from_theme(ui, tint)     light, dark or high contrast from the theme's tokens;
14//                                        tint = the category colour 0xRRGGBB (its light value)
15//   SymPalette::light / dark(text, bg, tint), contrast(ui)   the three schemes
16//   pal.powered_off()                    LEDs and screens dark (a device without power)
17//   SymbolCache::new()                   rasters of up to 256 (symbol, width, palette)
18//   cache.draw(g, id, &sym, x, y, w, &pal, opacity)   the symbol w px wide at x, y: the
19//                                        cached raster blitted, wider than 256 px the
20//                                        vectors; opacity 0..255 (power off: 115 = 45 %)
21//   cache.get(g, id, &sym, w, &pal) -> Option<Rc<Surface>>   the raster itself
22//
23// Layer prefixes (vicon.rs's syntax extended, so its art stays valid): F filled (non-zero
24// rule), E filled (even-odd), neither = stroked with round caps and joins; #n palette role
25// 0..9 (default 0, an 'A' layer 4); S<w> stroke width in grid units (default 1.5; at least
26// 0.8 px when drawn).  Layers paint in order.  Roles: 0 ink (outlines), 1 body, 2 body shade,
27// 3 screen, 4 category tint, 5 ports and dark parts, 6 ok, 7 warn, 8 crit (status LEDs),
28// 9 highlight.  Light and dark themes swap the palette only.
29//
30// The cache key is (id, width, palette hash) with the palette compared too; `id` is the
31// caller's number for the symbol.  Rasters are drawn at whole pixels, so x, y of draw are
32// integers; paint takes fractional positions (a zoomed diagram).
33
34use alloc::rc::Rc;
35use alloc::vec::Vec;
36
37use gfx::pathdata::PData;
38use gfx::xform::Matrix;
39use gfx::{Surface, FMT_PARGB};
40use rt::fx::{ceil_i, div255};
41
42use crate::cmd::Gfx;
43use crate::consts::*;
44use crate::theme::color_mix;
45use crate::vicon::layer_head;
46use crate::Ui;
47
48pub const SYM_INK: u8 = 0; // outlines
49pub const SYM_BODY: u8 = 1; // the device's body
50pub const SYM_SHADE: u8 = 2; // a body part in shadow (sides, bezels, recesses)
51pub const SYM_SCREEN: u8 = 3; // displays
52pub const SYM_TINT: u8 = 4; // the category colour (one band per symbol)
53pub const SYM_PORT: u8 = 5; // ports, grilles, dark parts
54pub const SYM_OK: u8 = 6; // status LEDs
55pub const SYM_WARN: u8 = 7;
56pub const SYM_CRIT: u8 = 8;
57pub const SYM_HI: u8 = 9; // highlight (light details)
58pub const SYM_ROLES: usize = 10;
59/// the widest symbol SymbolCache rasterises (px); wider ones are drawn as vectors
60pub const SYM_CACHE_MAX_W: i32 = 256;
61
62const CACHE_N: usize = 256;
63const STROKE_DEFAULT: f32 = 1.5;
64const STROKE_MIN_PX: f32 = 0.8;
65
66struct Layer {
67    pd: PData,
68    role: u8,
69    /// 0 stroked, 1 non-zero fill, 2 even-odd fill
70    fill: u8,
71    /// stroke width in grid units
72    width: f32,
73}
74
75/// a parsed multi-colour symbol (see the module comment)
76pub struct Symbol {
77    layers: Vec<Layer>,
78    /// the grid: width and height in grid units
79    pub vb: [f32; 2],
80}
81
82impl Symbol {
83    /// parse the layers once; layers whose path data does not parse are left out
84    pub fn parse(art: &str, vb: [f32; 2]) -> Symbol {
85        let mut layers = Vec::new();
86        for layer in art.split('|') {
87            let layer = layer.trim();
88            if layer.is_empty() {
89                continue;
90            }
91            let (h, d) = layer_head(layer);
92            let mut pd = PData::new();
93            if pd.parse(d.as_bytes()).0 != 0 {
94                continue;
95            }
96            let role = h.role.unwrap_or(if h.accent { SYM_TINT } else { SYM_INK });
97            layers.push(Layer { pd, role: role.min(SYM_ROLES as u8 - 1), fill: h.fill, width: h.stroke.unwrap_or(STROKE_DEFAULT) });
98        }
99        let vb = [if vb[0] > 0.0 { vb[0] } else { 1.0 }, if vb[1] > 0.0 { vb[1] } else { 1.0 }];
100        Symbol { layers, vb }
101    }
102
103    /// the number of layers that parsed
104    pub fn layers(&self) -> usize {
105        self.layers.len()
106    }
107
108    /// the height in px of the symbol drawn w px wide
109    pub fn height(&self, w: i32) -> i32 {
110        ceil_i(w as f32 * self.vb[1] / self.vb[0]).max(0)
111    }
112
113    /// draw every layer with the grid's (0, 0) at x, y (draw context coordinates) and
114    /// `scale` px per grid unit; the draw colour is kept
115    pub fn paint(&self, g: &mut Gfx, x: f32, y: f32, scale: f32, pal: &SymPalette) {
116        if scale <= 0.0 || !scale.is_finite() {
117            return;
118        }
119        let s = scale as f64;
120        let m = Matrix { a: s, b: 0.0, c: x as f64 + g.dc.ox as f64, d: 0.0, e: s, f: y as f64 + g.dc.oy as f64 };
121        let color = g.dc.get_color();
122        for l in &self.layers {
123            let path = &mut g.text.path;
124            path.reset();
125            path.set_origin(0, 0);
126            if l.pd.emit(path, Some(&m)) != 0 {
127                continue;
128            }
129            g.dc.set_color(pal.c[l.role as usize]);
130            if l.fill != 0 {
131                path.fill(&mut g.text.ras, g.dc, g.sf, if l.fill == 2 { gfx::RULE_EVENODD } else { gfx::RULE_NONZERO });
132            } else {
133                let w = ((l.width * scale).max(STROKE_MIN_PX) * 256.0) as i32;
134                path.stroke(&mut g.text.ras, g.dc, g.sf, w, gfx::JOIN_ROUND | gfx::CAP_ROUND << 4);
135            }
136        }
137        g.dc.set_color(color);
138    }
139
140    /// the symbol w px wide in a new PARGB surface (height(w) px high)
141    pub fn raster(&self, g: &mut Gfx, w: i32, pal: &SymPalette) -> Surface {
142        let mut sf = Surface::new(w.max(0), self.height(w), FMT_PARGB);
143        if sf.px.is_empty() {
144            return sf;
145        }
146        // begin / end keep the target's clip, origin and opacity
147        g.dc.begin(&sf);
148        g.dc.set_opacity(255);
149        {
150            let mut tg = Gfx { dc: &mut *g.dc, sf: &mut sf, text: &mut *g.text };
151            self.paint(&mut tg, 0.0, 0.0, w as f32 / self.vb[0], pal);
152        }
153        g.dc.end();
154        sf
155    }
156}
157
158// fg (straight alpha) over an opaque bg
159fn over(fg: u32, bg: u32) -> u32 {
160    let a = fg >> 24;
161    color_mix(bg | 0xFF00_0000, fg | 0xFF00_0000, ((a * 256 + 127) / 255) as i32)
162}
163
164/// the colours of a symbol's roles (straight 0xAARRGGBB, index = SYM_* role)
165#[derive(Clone, Copy, PartialEq, Eq, Debug)]
166pub struct SymPalette {
167    pub c: [u32; SYM_ROLES],
168}
169
170impl SymPalette {
171    /// the palette of the current theme: high contrast, dark or light; tint = the category
172    /// colour 0xRRGGBB as it looks in the light theme
173    pub fn from_theme(ui: &Ui, tint: u32) -> SymPalette {
174        if ui.high_contrast() {
175            return SymPalette::contrast(ui);
176        }
177        let dark = ui.theme_dark();
178        let bg = over(ui.color(T_BG), if dark { 0xFF00_0000 } else { 0xFFFF_FFFF });
179        let text = over(ui.color(T_TEXT), bg);
180        if dark { SymPalette::dark(text, bg, tint) } else { SymPalette::light(text, bg, tint) }
181    }
182
183    /// the light scheme from the text and window colours (opaque): slate outlines, near-white
184    /// bodies, pale steel screens
185    pub fn light(text: u32, bg: u32, tint: u32) -> SymPalette {
186        let ink = color_mix(text, bg, 40);
187        let body = color_mix(0xFFFF_FFFF, bg, 96);
188        let port = color_mix(ink, body, 40);
189        SymPalette {
190            c: [
191                ink,
192                body,
193                color_mix(body, ink, 36),
194                color_mix(body, 0xFF3D_6A96, 64),
195                tint | 0xFF00_0000,
196                port,
197                0xFF3C_9A5F,
198                0xFFD3_9A2C,
199                0xFFC8_473D,
200                0xFFFF_FFFF,
201            ],
202        }
203    }
204
205    /// the dark scheme: light grey outlines, bodies a step above the window, dark screens,
206    /// the tint lighter and greyer
207    pub fn dark(text: u32, bg: u32, tint: u32) -> SymPalette {
208        let ink = color_mix(text, bg, 64);
209        let body = color_mix(bg, 0xFFFF_FFFF, 36);
210        SymPalette {
211            c: [
212                ink,
213                body,
214                color_mix(body, bg, 128),
215                color_mix(bg, 0xFF3D_6A96, 90),
216                color_mix(tint | 0xFF00_0000, 0xFFC8_CDD3, 56),
217                color_mix(bg, 0xFF00_0000, 64),
218                0xFF52_B072,
219                0xFFE0_A83E,
220                0xFFE0_645A,
221                color_mix(body, ink, 128),
222            ],
223        }
224    }
225
226    /// high contrast: outlines and ports in the text colour, every fill in the window colour,
227    /// the tint band in the highlight (accent) colour, LEDs in the status colours
228    pub fn contrast(ui: &Ui) -> SymPalette {
229        let bg = over(ui.color(T_BG), 0xFF00_0000);
230        let text = over(ui.color(T_TEXT), bg);
231        let tok = |t: u32| over(ui.color(t), bg);
232        SymPalette { c: [text, bg, bg, bg, tok(T_ACCENT), text, tok(T_OK), tok(T_WARN), tok(T_CRIT), bg] }
233    }
234
235    /// a device without power: status LEDs dark, screens off
236    pub fn powered_off(&self) -> SymPalette {
237        let mut p = *self;
238        let off = color_mix(p.c[SYM_SHADE as usize], p.c[SYM_PORT as usize], 96);
239        p.c[SYM_OK as usize] = off;
240        p.c[SYM_WARN as usize] = off;
241        p.c[SYM_CRIT as usize] = off;
242        p.c[SYM_SCREEN as usize] = p.c[SYM_SHADE as usize];
243        p
244    }
245
246    /// FNV-1a of the colours (the cache key)
247    pub fn hash(&self) -> u32 {
248        let mut h: u32 = 0x811C_9DC5;
249        for c in self.c {
250            for b in c.to_le_bytes() {
251                h = (h ^ b as u32).wrapping_mul(0x0100_0193);
252            }
253        }
254        h
255    }
256}
257
258struct Entry {
259    id: u32,
260    w: i32,
261    hash: u32,
262    pal: [u32; SYM_ROLES],
263    sf: Rc<Surface>,
264    used: u32,
265}
266
267/// rasters of symbols per (id, width, palette), the least recently used one dropped
268/// beyond 256
269#[derive(Default)]
270pub struct SymbolCache {
271    list: Vec<Entry>,
272    tick: u32,
273}
274
275impl SymbolCache {
276    pub fn new() -> SymbolCache {
277        SymbolCache::default()
278    }
279
280    /// the number of rasters kept
281    pub fn len(&self) -> usize {
282        self.list.len()
283    }
284
285    pub fn is_empty(&self) -> bool {
286        self.list.is_empty()
287    }
288
289    /// drop every raster
290    pub fn clear(&mut self) {
291        self.list.clear();
292    }
293
294    /// the raster of symbol `id` w px wide in this palette, made on the first call; None for
295    /// widths outside 1..=SYM_CACHE_MAX_W
296    pub fn get(&mut self, g: &mut Gfx, id: u32, sym: &Symbol, w: i32, pal: &SymPalette) -> Option<Rc<Surface>> {
297        if w <= 0 || w > SYM_CACHE_MAX_W {
298            return None;
299        }
300        let hash = pal.hash();
301        self.tick = self.tick.wrapping_add(1);
302        let tick = self.tick;
303        if let Some(e) = self.list.iter_mut().find(|e| e.id == id && e.w == w && e.hash == hash && e.pal == pal.c) {
304            e.used = tick;
305            return Some(e.sf.clone());
306        }
307        let sf = Rc::new(sym.raster(g, w, pal));
308        if self.list.len() >= CACHE_N {
309            let mut old = 0;
310            for (i, e) in self.list.iter().enumerate() {
311                if e.used < self.list[old].used {
312                    old = i;
313                }
314            }
315            self.list.swap_remove(old);
316        }
317        self.list.push(Entry { id, w, hash, pal: pal.c, sf: sf.clone(), used: tick });
318        Some(sf)
319    }
320
321    /// draw symbol `id` w px wide with its top-left corner at x, y (draw context
322    /// coordinates) at opacity 0..255: the cached raster, wider than SYM_CACHE_MAX_W the
323    /// vectors (through a raster when translucent)
324    #[allow(clippy::too_many_arguments)]
325    pub fn draw(&mut self, g: &mut Gfx, id: u32, sym: &Symbol, x: i32, y: i32, w: i32, pal: &SymPalette, opacity: u32) {
326        if w <= 0 || opacity == 0 {
327            return;
328        }
329        let op = opacity.min(255);
330        let sf = if w <= SYM_CACHE_MAX_W {
331            self.get(g, id, sym, w, pal)
332        } else if op == 255 {
333            sym.paint(g, x as f32, y as f32, w as f32 / sym.vb[0], pal);
334            None
335        } else {
336            Some(Rc::new(sym.raster(g, w, pal)))
337        };
338        if let Some(sf) = sf {
339            let op0 = g.dc.opac;
340            g.dc.set_opacity(div255(op * op0));
341            g.dc.blit(g.sf, &sf, x, y);
342            g.dc.set_opacity(op0);
343        }
344    }
345}