Skip to main content

qframe/icons/
sample.rs

1//! A row of sample glyphs in one glyph mode, for the user to judge with their own eyes.
2
3use std::borrow::Cow;
4
5use super::GlyphMode;
6use crate::geometry::{Rect, Size};
7use crate::style::CellStyle;
8use crate::text;
9use crate::widget::{MeasureCx, PaintCx, Widget};
10
11/// Cells between two glyphs.
12const GAP: u16 = 2;
13
14/// A few icons drawn in one [`GlyphMode`], whatever mode the application draws in.
15///
16/// No program can learn which font a terminal draws with, so the last word on Nerd Font glyphs
17/// is the user's eye: a setup screen puts a Nerd sample beside a Unicode one and asks which reads
18/// as shapes. A missing Nerd Font shows as boxes or question marks. Place one after installing
19/// the font (see [`nerd_font`](super::nerd_font)) to show whether the terminal picked it up.
20///
21/// ```
22/// use qframe::icons::{GlyphMode, GlyphSample};
23///
24/// let nerd = GlyphSample::new(GlyphMode::Nerd);
25/// let unicode = GlyphSample::new(GlyphMode::Unicode).keys(["folder", "check"]);
26/// # let _ = (nerd, unicode);
27/// ```
28///
29/// The glyphs come from the active icon set, so a theme's icons show as they would be drawn.
30/// They use the `text` colour on no background of their own.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct GlyphSample {
33    mode: GlyphMode,
34    keys: Vec<String>,
35}
36
37impl GlyphSample {
38    /// The icons shown unless [`GlyphSample::keys`] names others: four whose Nerd glyphs lie in
39    /// the private use area, so none of them looks right without a Nerd Font.
40    pub const KEYS: [&str; 4] = ["folder", "check", "search", "settings"];
41
42    /// A sample of [`GlyphSample::KEYS`] in `mode`.
43    #[must_use]
44    pub fn new(mode: GlyphMode) -> Self {
45        Self { mode, keys: Self::KEYS.iter().map(|key| (*key).to_owned()).collect() }
46    }
47
48    /// Shows these icon keys instead, in order. A key the icon set lacks is left out.
49    #[must_use]
50    pub fn keys(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
51        self.keys = keys.into_iter().map(Into::into).collect();
52        self
53    }
54
55    fn glyphs<'a>(&'a self, icons: &'a super::Icons) -> impl Iterator<Item = Cow<'a, str>> + 'a {
56        self.keys.iter().filter_map(|key| icons.glyphs(key)).map(|glyphs| Cow::Borrowed(glyphs.for_mode(self.mode)))
57    }
58}
59
60impl<Msg: 'static> Widget<Msg> for GlyphSample {
61    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
62        let widths: Vec<u16> = self.glyphs(cx.env().icons()).map(|glyph| text::width(&glyph)).collect();
63        if widths.is_empty() {
64            return Size::new(0, 0);
65        }
66        let gaps = u16::try_from(widths.len() - 1).unwrap_or(u16::MAX).saturating_mul(GAP);
67        let width = widths.into_iter().fold(gaps, u16::saturating_add);
68        Size::new(width, 1).min(available)
69    }
70
71    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
72        if area.is_empty() {
73            return;
74        }
75        let glyphs: Vec<String> = self.glyphs(cx.env().icons()).map(Cow::into_owned).collect();
76        let style = CellStyle::fg(cx.color("text"));
77        let right = area.x + i32::from(area.width);
78        let mut x = area.x;
79        for glyph in glyphs {
80            let room = crate::geometry::clamp_u16(right - x);
81            let width = text::width(&glyph);
82            // A glyph is never cut: one that no longer fits ends the row.
83            if width > room {
84                break;
85            }
86            cx.text(x, area.y, &glyph, style, room);
87            x += i32::from(width + GAP);
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::runtime::{App, Command, Harness};
96    use crate::widget::View;
97
98    struct Demo(Vec<GlyphSample>);
99
100    impl App for Demo {
101        type Msg = ();
102        fn update(&mut self, _: ()) -> Command<()> {
103            Command::none()
104        }
105        fn view(&self, ui: &mut View<'_, ()>) {
106            ui.column(|ui| {
107                for sample in &self.0 {
108                    ui.add(sample.clone());
109                }
110            });
111        }
112    }
113
114    #[test]
115    fn draws_each_mode_whatever_the_application_draws_in() {
116        let samples = vec![
117            GlyphSample::new(GlyphMode::Nerd),
118            GlyphSample::new(GlyphMode::Unicode),
119            GlyphSample::new(GlyphMode::Ascii),
120        ];
121        let mut h = Harness::new(Demo(samples), 20, 3);
122        h.set_glyph_mode(GlyphMode::Ascii);
123        assert_eq!(h.screen(), "\u{f07b}  \u{f00c}  \u{f002}  \u{f013}\n■  ✓  ⌕  ▤\n#  v  /  *\n");
124        assert_eq!(h.fg(0, 1), h.env().theme().color("text"));
125    }
126
127    #[test]
128    fn chosen_keys_skip_unknown_ones_and_never_cut_a_glyph() {
129        let sample = GlyphSample::new(GlyphMode::Unicode).keys(["check", "no-such-icon", "folder"]);
130        let h = Harness::new(Demo(vec![sample.clone()]), 20, 1);
131        assert_eq!(h.screen(), "✓  ■\n");
132        let h = Harness::new(Demo(vec![sample]), 3, 1);
133        assert_eq!(h.screen(), "✓\n");
134    }
135}