Skip to main content

pfx_input/glyph/
keycap.rs

1use pfx_text::{Anchor, Error, Face, Placed, Placement, Span, TextEngine};
2
3use super::{BOX, Glyph, glyph};
4use crate::device::{Control, Key};
5use crate::family::Family;
6use crate::layout::KeyLabels;
7
8pub const KEYCAP_WIDTHS: [u16; 6] = [48, 64, 80, 96, 128, 160];
9pub const INSET: f32 = 10.0;
10pub const LETTER_EM: f32 = 26.0;
11pub const WORD_EM: f32 = 18.0;
12pub const CAP_HEIGHT: f32 = 0.7;
13
14pub fn room(width: u16) -> f32 {
15    f32::from(width) - 2.0 * INSET
16}
17
18pub fn keycap_for(label_width: f32) -> Glyph {
19    Glyph::Keycap(
20        KEYCAP_WIDTHS
21            .into_iter()
22            .find(|width| room(*width) >= label_width)
23            .unwrap_or(widest()),
24    )
25}
26
27pub fn label_em(text: &str) -> f32 {
28    if text.chars().count() == 1 {
29        LETTER_EM
30    } else {
31        WORD_EM
32    }
33}
34
35pub fn estimate(text: &str) -> f32 {
36    text.chars().count() as f32 * 0.6 * label_em(text)
37}
38
39#[derive(Clone, Debug, PartialEq)]
40pub struct KeycapLabel {
41    pub glyph: Glyph,
42    pub text: String,
43    pub size: f32,
44    pub origin: [f32; 2],
45}
46
47impl KeycapLabel {
48    pub fn key(
49        engine: &mut TextEngine,
50        face: &Face,
51        labels: &KeyLabels,
52        key: Key,
53    ) -> Result<Self, Error> {
54        Self::key_with_floor(engine, face, labels, key, 0.0)
55    }
56
57    pub fn key_with_floor(
58        engine: &mut TextEngine,
59        face: &Face,
60        labels: &KeyLabels,
61        key: Key,
62        floor: f32,
63    ) -> Result<Self, Error> {
64        match glyph(Family::Generic, Control::Key(key)) {
65            Glyph::Keycap(_) => Self::fit_with_floor(engine, face, labels.label(key), floor),
66            other => Ok(Self {
67                glyph: other,
68                text: String::new(),
69                size: 0.0,
70                origin: [0.0, 0.0],
71            }),
72        }
73    }
74
75    pub fn fit(engine: &mut TextEngine, face: &Face, text: &str) -> Result<Self, Error> {
76        Self::fit_with_floor(engine, face, text, 0.0)
77    }
78
79    pub fn fit_with_floor(
80        engine: &mut TextEngine,
81        face: &Face,
82        text: &str,
83        floor: f32,
84    ) -> Result<Self, Error> {
85        if !floor.is_finite() || floor < 0.0 {
86            return Err(Error::InvalidStyle);
87        }
88        let em = label_em(text);
89        let span = span(text, face, em, [0.0; 4]);
90        let block = engine.layout_spans(&[span], None, 1.0, Anchor::Start)?;
91        let (width, baseline) = (block.width, block.baseline);
92        let Glyph::Keycap(fitted) = keycap_for(width) else {
93            return Err(Error::InvalidStyle);
94        };
95        let mut cap = fitted;
96        let mut shrink = if width > room(cap) {
97            room(cap) / width
98        } else {
99            1.0
100        };
101        if em * shrink < floor {
102            shrink = floor.min(em) / em;
103            cap = widened(width * shrink).max(fitted);
104        }
105        let glyph = Glyph::Keycap(cap);
106        let size = em * shrink;
107        Ok(Self {
108            glyph,
109            text: text.to_owned(),
110            size,
111            origin: [
112                (f32::from(cap) - width * shrink) / 2.0,
113                BOX / 2.0 + CAP_HEIGHT * size / 2.0 - baseline * shrink,
114            ],
115        })
116    }
117
118    pub fn place(
119        &self,
120        engine: &mut TextEngine,
121        face: &Face,
122        color: [f32; 4],
123        origin: [f32; 2],
124        height: f32,
125        placement: &Placement,
126    ) -> Result<Placed, Error> {
127        if self.text.is_empty() {
128            return Ok(Placed::default());
129        }
130        let k = height / BOX;
131        let span = span(&self.text, face, self.size * k, color);
132        engine.place_spans(
133            &[span],
134            None,
135            Anchor::Start,
136            &Placement {
137                origin: [
138                    origin[0] + self.origin[0] * k,
139                    origin[1] + self.origin[1] * k,
140                ],
141                ..*placement
142            },
143        )
144    }
145}
146
147pub fn widened(label_width: f32) -> u16 {
148    (label_width + 2.0 * INSET)
149        .ceil()
150        .clamp(0.0, f32::from(u16::MAX)) as u16
151}
152
153pub fn widest() -> u16 {
154    KEYCAP_WIDTHS[KEYCAP_WIDTHS.len() - 1]
155}
156
157fn span(text: &str, face: &Face, size: f32, color: [f32; 4]) -> Span {
158    Span {
159        text: text.to_owned(),
160        face: Face {
161            size,
162            line: face.line / face.size * size,
163            ..face.clone()
164        },
165        color,
166    }
167}