1use gpui::prelude::*;
17use gpui::{
18 div, px, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
19 MouseButton, SharedString, Window,
20};
21
22use super::control_metrics;
23use crate::reactive::Signal;
24use crate::theme::{theme, Size};
25
26#[derive(Debug, Clone)]
28pub enum PinInputEvent {
29 Change(String),
31 Complete(String),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37struct PinModel {
38 slots: Vec<Option<char>>,
39 cursor: usize,
40}
41
42impl PinModel {
43 fn new(length: usize) -> Self {
44 PinModel {
45 slots: vec![None; length.max(1)],
46 cursor: 0,
47 }
48 }
49
50 fn len(&self) -> usize {
51 self.slots.len()
52 }
53
54 fn value(&self) -> String {
56 self.slots.iter().flatten().collect()
57 }
58
59 fn is_complete(&self) -> bool {
60 self.slots.iter().all(|slot| slot.is_some())
61 }
62
63 fn insert(&mut self, ch: char) -> bool {
68 if ch.is_whitespace() || ch.is_control() {
69 return false;
70 }
71 let changed = self.slots[self.cursor] != Some(ch);
72 self.slots[self.cursor] = Some(ch);
73 if self.cursor + 1 < self.len() {
74 self.cursor += 1;
75 }
76 changed
77 }
78
79 fn backspace(&mut self) -> bool {
82 if self.slots[self.cursor].is_some() {
83 self.slots[self.cursor] = None;
84 true
85 } else if self.cursor > 0 {
86 self.cursor -= 1;
87 self.slots[self.cursor] = None;
88 true
89 } else {
90 false
91 }
92 }
93
94 fn left(&mut self) {
95 self.cursor = self.cursor.saturating_sub(1);
96 }
97
98 fn right(&mut self) {
99 if self.cursor + 1 < self.len() {
100 self.cursor += 1;
101 }
102 }
103
104 fn paste(&mut self, text: &str) -> bool {
108 let len = self.len();
109 let chars: Vec<char> = text
110 .chars()
111 .filter(|c| !c.is_whitespace() && !c.is_control())
112 .take(len)
113 .collect();
114 if chars.is_empty() {
115 return false;
116 }
117 let before = self.slots.clone();
118 self.slots.fill(None);
119 for (i, ch) in chars.iter().enumerate() {
120 self.slots[i] = Some(*ch);
121 }
122 self.cursor = chars.len().min(len - 1);
123 self.slots != before
124 }
125
126 fn set_value(&mut self, value: &str) {
128 let len = self.len();
129 self.slots.fill(None);
130 for (i, ch) in value.chars().take(len).enumerate() {
131 self.slots[i] = Some(ch);
132 }
133 self.cursor = self
134 .slots
135 .iter()
136 .position(|slot| slot.is_none())
137 .unwrap_or(len - 1);
138 }
139
140 fn resize(&mut self, length: usize) {
141 self.slots.resize(length.max(1), None);
142 self.cursor = self.cursor.min(self.slots.len() - 1);
143 }
144}
145
146pub struct PinInput {
148 model: PinModel,
149 focus: FocusHandle,
150 mask: bool,
151 size: Size,
152 disabled: bool,
153}
154
155impl EventEmitter<PinInputEvent> for PinInput {}
156
157impl PinInput {
158 pub fn new(cx: &mut Context<Self>) -> Self {
159 PinInput {
160 model: PinModel::new(4),
161 focus: cx.focus_handle(),
162 mask: false,
163 size: Size::Sm,
164 disabled: false,
165 }
166 }
167
168 pub fn length(mut self, length: usize) -> Self {
170 self.model.resize(length);
171 self
172 }
173
174 pub fn mask(mut self, mask: bool) -> Self {
176 self.mask = mask;
177 self
178 }
179
180 pub fn size(mut self, size: Size) -> Self {
181 self.size = size;
182 self
183 }
184
185 pub fn disabled(mut self, disabled: bool) -> Self {
186 self.disabled = disabled;
187 self
188 }
189
190 pub fn value(mut self, value: &str) -> Self {
192 self.model.set_value(value);
193 self
194 }
195
196 pub fn focus_handle(&self) -> FocusHandle {
198 self.focus.clone()
199 }
200
201 pub fn text(&self) -> String {
203 self.model.value()
204 }
205
206 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
208 self.model.set_value(value);
209 cx.notify();
210 }
211
212 pub fn bind(entity: &Entity<PinInput>, signal: &Signal<String>, cx: &mut App) {
217 let initial = signal.get(cx);
218 entity.update(cx, |this, cx| {
219 if this.text() != initial {
220 this.set_text(&initial, cx);
221 }
222 });
223 let sink = signal.clone();
224 cx.subscribe(entity, move |_pin, event: &PinInputEvent, cx| {
225 if let PinInputEvent::Change(text) = event {
226 sink.set_if_changed(cx, text.clone());
227 }
228 })
229 .detach();
230 let pin = entity.downgrade();
231 cx.observe(signal.entity(), move |observed, cx| {
232 let value = observed.read(cx).clone();
233 pin.update(cx, |this, cx| {
234 if this.text() != value {
235 this.set_text(&value, cx);
236 }
237 })
238 .ok();
239 })
240 .detach();
241 }
242
243 fn emit_edit(&mut self, cx: &mut Context<Self>) {
245 let value = self.model.value();
246 cx.emit(PinInputEvent::Change(value.clone()));
247 if self.model.is_complete() {
248 cx.emit(PinInputEvent::Complete(value));
249 }
250 cx.notify();
251 cx.stop_propagation();
252 }
253
254 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
255 if self.disabled {
256 return;
257 }
258 let ks = &event.keystroke;
259 let m = &ks.modifiers;
260 match ks.key.as_str() {
261 "left" => {
262 self.model.left();
263 cx.notify();
264 cx.stop_propagation();
265 }
266 "right" => {
267 self.model.right();
268 cx.notify();
269 cx.stop_propagation();
270 }
271 "backspace" => {
272 if self.model.backspace() {
273 self.emit_edit(cx);
274 } else {
275 cx.stop_propagation();
276 }
277 }
278 "v" if m.platform => {
279 if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
280 if self.model.paste(&text) {
281 self.emit_edit(cx);
282 }
283 }
284 cx.stop_propagation();
285 }
286 _ => {
287 if !m.platform && !m.control {
290 if let Some(typed) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
291 let cursor_before = self.model.cursor;
292 let mut changed = false;
293 for ch in typed.chars() {
294 changed |= self.model.insert(ch);
295 }
296 if changed {
297 self.emit_edit(cx);
298 } else if self.model.cursor != cursor_before {
299 cx.notify();
303 cx.stop_propagation();
304 }
305 }
306 }
307 }
308 }
309 }
310}
311
312impl Render for PinInput {
313 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
314 let t = theme(cx);
315 let (height, _pad_x, font) = control_metrics(self.size);
316 let radius = t.radius(t.default_radius);
317 let focused = self.focus.is_focused(window) && !self.disabled;
318
319 let border = t.border().hsla();
320 let active_border = t.primary().hsla();
321 let surface = t.surface().hsla();
322 let text_color = t.text().hsla();
323
324 let cursor = self.model.cursor;
325 let mask = self.mask;
326
327 let mut row = div()
328 .id("guise-pininput")
329 .track_focus(&self.focus)
330 .on_key_down(cx.listener(Self::on_key))
331 .flex()
332 .items_center()
333 .gap(px(8.0));
334
335 for (i, slot) in self.model.slots.iter().enumerate() {
336 let ch = slot.map(|c| if mask { '\u{2022}' } else { c });
337 let active = focused && i == cursor;
338 let mut cell = div()
339 .id(("guise-pin-box", i))
340 .flex()
341 .items_center()
342 .justify_center()
343 .w(px(height))
344 .h(px(height))
345 .rounded(px(radius))
346 .border_1()
347 .border_color(if active { active_border } else { border })
348 .bg(surface)
349 .text_size(px(font))
350 .text_color(text_color)
351 .on_mouse_down(
352 MouseButton::Left,
353 cx.listener(move |this, _ev, window, cx| {
354 window.focus(&this.focus);
355 this.model.cursor = i.min(this.model.len() - 1);
356 cx.notify();
357 }),
358 );
359 if let Some(ch) = ch {
360 cell = cell.child(SharedString::from(ch.to_string()));
361 }
362 row = row.child(cell);
363 }
364
365 if self.disabled {
366 row.opacity(0.6)
367 } else {
368 row
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::PinModel;
376
377 #[test]
378 fn typing_advances_and_stops_at_the_last_box() {
379 let mut pin = PinModel::new(4);
380 assert!(pin.insert('1'));
381 assert!(pin.insert('2'));
382 assert_eq!(pin.value(), "12");
383 assert_eq!(pin.cursor, 2);
384
385 pin.insert('3');
386 pin.insert('4');
387 assert_eq!(pin.cursor, 3, "cursor parks on the last box");
388 assert!(pin.is_complete());
389
390 assert!(pin.insert('9'));
392 assert_eq!(pin.value(), "1239");
393 assert_eq!(pin.cursor, 3);
394 }
395
396 #[test]
397 fn retyping_the_same_char_reports_no_change() {
398 let mut pin = PinModel::new(4);
399 pin.paste("1234");
400 assert!(!pin.insert('4'));
402 assert_eq!(pin.value(), "1234");
403 pin.left();
405 assert!(!pin.insert('3'));
406 assert_eq!(pin.cursor, 3);
407 assert_eq!(pin.value(), "1234");
408 }
409
410 #[test]
411 fn identical_paste_reports_no_change() {
412 let mut pin = PinModel::new(4);
413 assert!(pin.paste("1234"));
414 assert!(!pin.paste("1234"));
415 assert!(pin.paste("129"), "different content still reports a change");
416 assert_eq!(pin.value(), "129");
417 }
418
419 #[test]
420 fn whitespace_and_control_chars_are_rejected() {
421 let mut pin = PinModel::new(4);
422 assert!(!pin.insert(' '));
423 assert!(!pin.insert('\t'));
424 assert!(!pin.insert('\u{7}'));
425 assert_eq!(pin.value(), "");
426 assert_eq!(pin.cursor, 0);
427 }
428
429 #[test]
430 fn backspace_clears_current_then_retreats() {
431 let mut pin = PinModel::new(4);
432 pin.insert('1');
433 pin.insert('2');
434 assert!(pin.backspace());
436 assert_eq!(pin.value(), "1");
437 assert_eq!(pin.cursor, 1);
438 assert!(pin.backspace());
440 assert_eq!(pin.value(), "");
441 assert_eq!(pin.cursor, 0);
442 assert!(!pin.backspace());
444 }
445
446 #[test]
447 fn backspace_clears_a_filled_box_in_place() {
448 let mut pin = PinModel::new(4);
449 pin.paste("1234");
450 assert_eq!(pin.cursor, 3);
451 assert!(pin.backspace());
452 assert_eq!(pin.value(), "123");
453 assert_eq!(pin.cursor, 3, "clears the filled box without retreating");
454 }
455
456 #[test]
457 fn arrows_clamp_to_the_boxes() {
458 let mut pin = PinModel::new(3);
459 pin.left();
460 assert_eq!(pin.cursor, 0);
461 pin.right();
462 pin.right();
463 pin.right();
464 assert_eq!(pin.cursor, 2);
465 pin.left();
466 assert_eq!(pin.cursor, 1);
467 }
468
469 #[test]
470 fn paste_fills_from_the_start_and_skips_whitespace() {
471 let mut pin = PinModel::new(4);
472 pin.insert('9');
473 assert!(pin.paste(" 12 34 56 "));
474 assert_eq!(pin.value(), "1234", "replaces old content, truncates");
475 assert!(pin.is_complete());
476 assert_eq!(pin.cursor, 3);
477 }
478
479 #[test]
480 fn short_paste_leaves_the_cursor_on_the_next_empty_box() {
481 let mut pin = PinModel::new(6);
482 assert!(pin.paste("12"));
483 assert_eq!(pin.value(), "12");
484 assert_eq!(pin.cursor, 2);
485 assert!(!pin.paste(" "), "whitespace-only paste is a no-op");
486 }
487
488 #[test]
489 fn set_value_places_the_cursor_at_the_first_empty_box() {
490 let mut pin = PinModel::new(4);
491 pin.set_value("12");
492 assert_eq!(pin.cursor, 2);
493 pin.set_value("123456");
494 assert_eq!(pin.value(), "1234", "extra characters are dropped");
495 assert_eq!(pin.cursor, 3);
496 pin.set_value("");
497 assert_eq!(pin.value(), "");
498 assert_eq!(pin.cursor, 0);
499 }
500
501 #[test]
502 fn resize_preserves_slots_and_clamps_the_cursor() {
503 let mut pin = PinModel::new(6);
504 pin.paste("123456");
505 pin.resize(3);
506 assert_eq!(pin.value(), "123");
507 assert_eq!(pin.cursor, 2);
508 pin.resize(5);
509 assert_eq!(pin.value(), "123");
510 assert_eq!(pin.len(), 5);
511 pin.resize(0);
513 assert_eq!(pin.len(), 1);
514 assert_eq!(pin.cursor, 0);
515 }
516}