1use xutf::Text;
2
3use crate::{
4 component::{Component, EventCtx, Flow, Hit, HitTag, PaintCtx, Slot, next_slot},
5 context::{Theme, UiContext},
6 frame::{Rect, Style},
7 input::{
8 Key, Mouse, byte_at_column, sanitize_paste, word_left_column, word_right_column,
9 word_rubout_start,
10 },
11 props::{Prop, PropValue, Props},
12 rich::cell_width,
13};
14
15#[derive(Default)]
16struct InputState {
17 text: String,
18 cursor: u16,
19 mask: bool,
20 masked: String,
21}
22
23impl InputState {
24 fn refresh_mask(&mut self) {
25 self.masked.clear();
26 if self.mask {
27 self
28 .masked
29 .reserve(self.text.chars().count().saturating_mul('•'.len_utf8()));
30 self
31 .masked
32 .extend(std::iter::repeat_n('•', self.text.chars().count()));
33 }
34 }
35}
36
37pub struct Input {
39 props: Props,
40 slot: Slot,
41 state: InputState,
42}
43
44impl Input {
45 pub fn new() -> Self {
47 Self { props: Props::new(), slot: next_slot(), state: InputState::default() }
48 }
49
50 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
52 self.props.set(prop, value);
53 self.sync_prop(prop);
54 self
55 }
56
57 pub fn with_str(mut self, prop: Prop, value: &str) -> Self {
59 self.props.set(prop, value);
60 self.sync_prop(prop);
61 self
62 }
63
64 fn sync_prop(&mut self, prop: Prop) {
65 match prop {
66 Prop::Value => {
67 self.state.text = self
68 .props
69 .str_of(Prop::Value)
70 .map(ToString::to_string)
71 .unwrap_or_default();
72 self.state.cursor = cell_width(&self.state.text);
73 self.state.refresh_mask();
74 },
75 Prop::Mask => {
76 self.state.mask = self.props.flag(Prop::Mask);
77 self.state.refresh_mask();
78 },
79 _ => {},
80 }
81 }
82
83 fn edit(&mut self, key: Key) -> bool {
84 match key {
85 Key::Left | Key::Ctrl('b') => self.state.cursor = self.state.cursor.saturating_sub(1),
86 Key::Right | Key::Ctrl('f') => {
87 self.state.cursor = (self.state.cursor + 1).min(cell_width(&self.state.text));
88 },
89 Key::Home => self.state.cursor = 0,
90 Key::End => self.state.cursor = cell_width(&self.state.text),
91 Key::Backspace => {
92 if self.state.cursor > 0 {
93 let end = byte_at_column(&self.state.text, self.state.cursor);
94 let start = self.state.text[..end]
95 .grapheme_indices()
96 .next_back()
97 .map_or(0, |(offset, _)| offset);
98 let removed = cell_width(&self.state.text[start..end]);
99 self.state.text.replace_range(start..end, "");
100 self.state.cursor = self.state.cursor.saturating_sub(removed);
101 }
102 },
103 Key::Delete | Key::Ctrl('d') => {
104 let start = byte_at_column(&self.state.text, self.state.cursor);
105 if start < self.state.text.len() {
106 let grapheme_len = self.state.text[start..]
107 .graphemes()
108 .next()
109 .map_or(0, str::len);
110 self
111 .state
112 .text
113 .replace_range(start..start + grapheme_len, "");
114 }
115 },
116 Key::Space => {
117 let at = byte_at_column(&self.state.text, self.state.cursor);
118 self.state.text.insert(at, ' ');
119 self.state.cursor += 1;
120 },
121 Key::Char(character) => {
122 let at = byte_at_column(&self.state.text, self.state.cursor);
123 self.state.text.insert(at, character);
124 self.state.cursor += cell_width(character.encode_utf8(&mut [0u8; 4]));
125 },
126 Key::Ctrl('a') => self.state.cursor = 0,
127 Key::Ctrl('e') => self.state.cursor = cell_width(&self.state.text),
128 Key::Ctrl('k') => {
129 let at = byte_at_column(&self.state.text, self.state.cursor);
130 self.state.text.truncate(at);
131 },
132 Key::Ctrl('u') => {
133 let at = byte_at_column(&self.state.text, self.state.cursor);
134 self.state.text.replace_range(..at, "");
135 self.state.cursor = 0;
136 },
137 Key::Ctrl('w') => {
138 let end = byte_at_column(&self.state.text, self.state.cursor);
139 let start = word_rubout_start(&self.state.text, end);
140 let removed = cell_width(&self.state.text[start..end]);
141 self.state.text.replace_range(start..end, "");
142 self.state.cursor = self.state.cursor.saturating_sub(removed);
143 },
144 Key::WordLeft => self.state.cursor = word_left_column(&self.state.text, self.state.cursor),
145 Key::WordRight => {
146 self.state.cursor = word_right_column(&self.state.text, self.state.cursor);
147 },
148 Key::WordDelete => {
149 let start = byte_at_column(&self.state.text, self.state.cursor);
150 let end_column = word_right_column(&self.state.text, self.state.cursor);
151 let end = byte_at_column(&self.state.text, end_column);
152 self.state.text.replace_range(start..end, "");
153 },
154 _ => return false,
155 }
156 self.state.refresh_mask();
157 true
158 }
159}
160
161impl Default for Input {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl Component for Input {
168 fn props(&self) -> &Props {
169 &self.props
170 }
171
172 fn props_mut(&mut self) -> &mut Props {
173 &mut self.props
174 }
175
176 fn slot(&self) -> Slot {
177 self.slot
178 }
179
180 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
181 let placeholder = self
182 .props
183 .str_of(Prop::Placeholder)
184 .map_or(0, |placeholder| cell_width(placeholder));
185 (16, placeholder.saturating_add(3).max(30))
186 }
187
188 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
189 1
190 }
191
192 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
193 if rect.y >= pc.clip {
194 return;
195 }
196 pc.hits
197 .push(Hit { rect, slot: self.slot, tag: HitTag::Press });
198 let focused = pc.focus == Some(self.slot);
199 let shown = if self.state.mask {
200 &self.state.masked
201 } else {
202 &self.state.text
203 };
204 let x = pc.frame.put(
205 rect.x,
206 rect.y,
207 pc.ctx.charset.cursor(),
208 if focused {
209 Style::new().fg(pc.ctx.theme.accent)
210 } else {
211 dim(&pc.ctx.theme)
212 },
213 );
214 if shown.is_empty() && !focused {
215 if let Some(placeholder) = self.props.str_of(Prop::Placeholder) {
216 pc.frame
217 .put(x, rect.y, placeholder, dim(&pc.ctx.theme).italic());
218 }
219 return;
220 }
221 let available = rect.width.saturating_sub(3);
222 let total = cell_width(shown);
223 let left = if total > available {
224 self
225 .state
226 .cursor
227 .saturating_sub(available.saturating_sub(8))
228 } else {
229 0
230 };
231 let start = byte_at_column(shown, left);
232 let visible = &shown[start..];
233 if focused {
234 let split = byte_at_column(visible, self.state.cursor.saturating_sub(left));
237 pc.frame.put(x, rect.y, visible, base(&pc.ctx.theme));
238 pc.frame
239 .set_cursor(x.saturating_add(cell_width(&visible[..split])), rect.y);
240 } else {
241 pc.frame.put(x, rect.y, visible, base(&pc.ctx.theme));
242 }
243 }
244
245 fn focusable(&self) -> bool {
246 true
247 }
248
249 fn key(&mut self, _ec: &mut EventCtx<'_>, key: Key) -> Flow {
250 if self.edit(key) {
251 Flow::Consumed
252 } else {
253 Flow::Skip
254 }
255 }
256
257 fn mouse(
258 &mut self,
259 _ec: &mut EventCtx<'_>,
260 tag: HitTag,
261 at: (u16, u16),
262 rect: Rect,
263 mouse: Mouse,
264 ) -> Flow {
265 match mouse {
266 Mouse::Click if tag == HitTag::Press => {
267 let column = at.0.saturating_sub(rect.x.saturating_add(2));
268 self.state.cursor = column.min(cell_width(&self.state.text));
269 Flow::Consumed
270 },
271 Mouse::Click
272 | Mouse::RightClick
273 | Mouse::MiddleClick
274 | Mouse::Move
275 | Mouse::Drag
276 | Mouse::Release
277 | Mouse::WheelUp
278 | Mouse::WheelDown
279 | Mouse::WheelLeft
280 | Mouse::WheelRight => Flow::Skip,
281 }
282 }
283
284 fn paste(&mut self, _ec: &mut EventCtx<'_>, text: &str) -> Flow {
285 let sanitized = sanitize_paste(text);
286 if sanitized.is_empty() {
287 return Flow::Skip;
288 }
289 let paste = sanitized.replace(['\n', '\t'], " ");
290 let at = byte_at_column(&self.state.text, self.state.cursor);
291 self.state.text.insert_str(at, &paste);
292 self.state.cursor = self.state.cursor.saturating_add(cell_width(&paste));
293 self.state.refresh_mask();
294 Flow::Consumed
295 }
296
297 fn value(&self, out: &mut serde_json::Map<String, serde_json::Value>) {
298 if let Some(id) = self.props.id() {
299 out.insert(id.to_string(), serde_json::Value::String(self.state.text.clone()));
300 }
301 }
302}
303
304const fn base(theme: &Theme) -> Style {
305 Style::new().fg(theme.fg)
306}
307const fn dim(theme: &Theme) -> Style {
308 Style::new().fg(theme.muted)
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::{Frame, Size, test_support::frame_row_text};
315
316 fn event_ctx(ctx: &UiContext) -> EventCtx<'_> {
317 EventCtx::new(ctx, 32, 1)
318 }
319
320 #[test]
321 fn key_and_paste_edit_exported_text() {
322 let mut input = Input::new().with(Prop::Id, "name");
323 let ctx = UiContext::default();
324 assert_eq!(input.key(&mut event_ctx(&ctx), Key::Char('a')), Flow::Consumed);
325 assert_eq!(input.key(&mut event_ctx(&ctx), Key::Char('b')), Flow::Consumed);
326 assert_eq!(input.paste(&mut event_ctx(&ctx), " c\td"), Flow::Consumed);
327 let mut values = serde_json::Map::new();
328 input.value(&mut values);
329 assert_eq!(values["name"], serde_json::json!("ab c d"));
330 }
331
332 #[test]
333 fn paint_draws_text_and_press_hit() {
334 let mut input = Input::new().with(Prop::Value, "hello");
335 let ctx = UiContext::default();
336 let mut frame = Frame::new(Size::new(32, 1));
337 let mut hits = Vec::new();
338 let slot = input.slot();
339 let mut wakes = Vec::new();
340 let mut pc = PaintCtx::new(&mut frame, &ctx, &mut hits, &mut wakes);
341 input.paint(&mut pc, Rect::new(0, 0, 32, 1));
342 assert!(frame_row_text(&frame, 0).contains("hello"));
343 assert_eq!(hits[0].slot, slot);
344 }
345}