kas_core/text/
string.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Text processing
7//!
8//! The functionality here is deliberately a quick hack to get things working.
9//! Hopefully it can be replaced with a real mark-up processor without too
10//! much API breakage.
11
12use crate::cast::Conv;
13use crate::event::Key;
14use crate::text::format::{FontToken, FormattableText};
15use crate::text::{Effect, EffectFlags};
16
17/// An access key string
18///
19/// This is a label which supports highlighting of access keys (sometimes called
20/// "mnemonics").
21///
22/// Drawing this text using the inherent [`FormattableText`] implementation will
23/// not underline the access key. To do that, use the effect tokens returned by
24/// [`Self::key`].
25///
26/// Markup: `&&` translates to `&`; `&x` for any `x` translates to `x` and
27/// identifies `x` as an "access key"; this may be drawn underlined and
28/// may support keyboard access via e.g. `Alt+X`
29#[derive(Clone, Debug, Default, PartialEq, Eq)]
30pub struct AccessString {
31    text: String,
32    key: Option<(Key, [Effect; 2])>,
33}
34
35impl AccessString {
36    /// Parse a `&str`
37    ///
38    /// Since we require `'static` for references and don't yet have
39    /// specialisation, this parser always allocates. Prefer to use `from`.
40    fn parse(mut s: &str) -> Self {
41        let mut text = String::with_capacity(s.len());
42        let mut key = None;
43
44        while let Some(mut i) = s.find('&') {
45            text.push_str(&s[..i]);
46            i += "&".len();
47            s = &s[i..];
48
49            match s.chars().next() {
50                None => {
51                    // Ending with '&' is an error, but we can ignore it
52                    s = &s[0..0];
53                    break;
54                }
55                Some(c) if key.is_none() => {
56                    let start = u32::conv(text.len());
57                    text.push(c);
58
59                    let mut kbuf = [0u8; 4];
60                    let k = c.to_ascii_lowercase().encode_utf8(&mut kbuf);
61                    let k = Key::Character(k.into());
62
63                    let e0 = Effect {
64                        start,
65                        e: 0,
66                        flags: EffectFlags::UNDERLINE,
67                    };
68
69                    let i = c.len_utf8();
70                    s = &s[i..];
71
72                    let e1 = Effect {
73                        start: start + u32::conv(i),
74                        e: 0,
75                        flags: EffectFlags::empty(),
76                    };
77
78                    key = Some((k, [e0, e1]));
79                }
80                Some(c) => {
81                    text.push(c);
82                    let i = c.len_utf8();
83                    s = &s[i..];
84                }
85            }
86        }
87
88        text.push_str(s);
89        AccessString { text, key }
90    }
91
92    /// Get the key bindings and associated effects, if any
93    pub fn key(&self) -> Option<&(Key, [Effect; 2])> {
94        self.key.as_ref()
95    }
96
97    /// Get the text
98    pub fn text(&self) -> &str {
99        &self.text
100    }
101}
102
103impl FormattableText for AccessString {
104    type FontTokenIter<'a> = std::iter::Empty<FontToken>;
105
106    #[inline]
107    fn as_str(&self) -> &str {
108        &self.text
109    }
110
111    #[inline]
112    fn font_tokens(&self, _: f32) -> Self::FontTokenIter<'_> {
113        std::iter::empty()
114    }
115
116    fn effect_tokens(&self) -> &[Effect] {
117        &[]
118    }
119}
120
121impl From<String> for AccessString {
122    fn from(text: String) -> Self {
123        if text.as_bytes().contains(&b'&') {
124            Self::parse(&text)
125        } else {
126            // fast path: we can use the raw input
127            AccessString {
128                text,
129                ..Default::default()
130            }
131        }
132    }
133}
134
135impl From<&str> for AccessString {
136    fn from(input: &str) -> Self {
137        Self::parse(input)
138    }
139}
140
141impl<T: Into<AccessString> + Copy> From<&T> for AccessString {
142    fn from(input: &T) -> Self {
143        (*input).into()
144    }
145}