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}