1use super::StateMachine;
5use crate::color::sRGB;
6use crate::editor::Editor;
7use crate::input::{ModifierKeys, MouseButton, MouseState, RawEvent, RawEventKind};
8use crate::layout::{Layout, base, leaf};
9use crate::text::{Change, EditBuffer};
10use crate::{Dispatchable, Error, InputResult, PxRect, SourceID, layout};
11use cosmic_text::{Action, Buffer, Cursor};
12use derive_where::derive_where;
13use enum_variant_type::EnumVariantType;
14use feather_macro::Dispatch;
15use smallvec::SmallVec;
16use std::rc::Rc;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
20
21#[derive(Debug, Dispatch, EnumVariantType, Clone, PartialEq, Eq)]
22#[evt(derive(Clone), module = "mouse_area_event")]
23pub enum TextBoxEvent {
24 Edit(SmallVec<[Change; 1]>),
25}
26
27struct TextBoxState {
28 last_x_offset: Option<f32>, history: Vec<SmallVec<[Change; 1]>>,
31 undo_index: usize,
32 insert_mode: bool,
33 text_count: AtomicUsize,
34 cursor_count: AtomicUsize,
35 focused: bool,
36 editor: Editor,
37 props: Rc<dyn Prop + 'static>,
38 align: Option<cosmic_text::Align>,
39}
40
41impl TextBoxState {
42 fn translate(e: RawEvent) -> RawEvent {
43 match e {
44 RawEvent::Key {
45 device_id,
46 physical_key: PhysicalKey::Code(key),
47 location,
48 down,
49 logical_key,
50 modifiers,
51 } => {
52 let k = match (key, down, modifiers & ModifierKeys::Control as u8 != 0) {
53 (KeyCode::KeyA, true, true) => Key::Named(NamedKey::Select),
54 (KeyCode::KeyC, true, true) => Key::Named(NamedKey::Copy),
55 (KeyCode::KeyX, true, true) => Key::Named(NamedKey::Cut),
56 (KeyCode::KeyV, true, true) => Key::Named(NamedKey::Paste),
57 (KeyCode::KeyZ, true, true) => Key::Named(NamedKey::Undo),
58 (KeyCode::KeyY, true, true) => Key::Named(NamedKey::Redo),
59 _ => logical_key,
60 };
61 RawEvent::Key {
62 device_id,
63 physical_key: PhysicalKey::Code(key),
64 location,
65 down,
66 logical_key: k,
67 modifiers,
68 }
69 }
70 _ => e,
71 }
72 }
73}
74
75impl super::EventRouter for TextBoxState {
76 type Input = RawEvent;
77 type Output = TextBoxEvent;
78
79 fn process(
80 mut this: crate::AccessCell<Self>,
81 input: Self::Input,
82 area: PxRect,
83 _: PxRect,
84 dpi: crate::RelDim,
85 driver: &std::sync::Weak<crate::Driver>,
86 ) -> InputResult<SmallVec<[Self::Output; 1]>> {
87 let obj = this.props.textedit().obj.clone();
88 let buffer = &mut obj.buffer.borrow_mut();
89 let align = this.align;
90 match Self::translate(input) {
91 RawEvent::Focus { acquired, window } => {
92 this.focused = acquired;
93 window.set_ime_allowed(acquired);
94 if acquired {
95 window.set_ime_purpose(winit::window::ImePurpose::Normal);
96 }
98 }
99 RawEvent::Key {
100 down,
101 logical_key,
102 modifiers,
103 ..
104 } => match logical_key {
105 Key::Named(named_key) => {
106 if down && let Some(driver) = driver.upgrade() {
107 let change = match named_key {
108 NamedKey::Enter => this.editor.action(
109 &mut driver.font_system.write(),
110 buffer,
111 Action::Enter,
112 align,
113 ),
114 NamedKey::Tab => this.editor.action(
115 &mut driver.font_system.write(),
116 buffer,
117 if (modifiers & ModifierKeys::Shift as u8) != 0 {
118 Action::Unindent
119 } else {
120 Action::Indent
121 },
122 align,
123 ),
124 NamedKey::Space => this.editor.action(
125 &mut driver.font_system.write(),
126 buffer,
127 Action::Insert(' '),
128 align,
129 ),
130 NamedKey::ArrowLeft
131 | NamedKey::ArrowRight
132 | NamedKey::ArrowDown
133 | NamedKey::ArrowUp
134 | NamedKey::End
135 | NamedKey::Home
136 | NamedKey::PageDown
137 | NamedKey::PageUp => {
138 let ctrl = (modifiers & ModifierKeys::Control as u8) != 0;
139 let shift = (modifiers & ModifierKeys::Shift as u8) != 0;
140 let font_system = &mut driver.font_system.write();
141 if !shift {
142 let line_height = buffer.metrics().line_height;
143 match (named_key, ctrl) {
144 (NamedKey::ArrowUp, true) | (NamedKey::ArrowDown, true) => {
145 this.editor.action(
146 font_system,
147 buffer,
148 Action::Scroll {
149 pixels: if named_key == NamedKey::ArrowUp {
150 -line_height
151 } else {
152 line_height
153 },
154 },
155 align,
156 );
157 return InputResult::Consume(SmallVec::new());
158 }
159 _ => (),
160 }
161
162 if let Some((start, end)) = this.editor.selection_bounds(buffer)
163 {
164 if named_key == NamedKey::ArrowLeft {
165 this.editor.set_cursor(buffer, start);
166 } else if named_key == NamedKey::ArrowRight {
167 this.editor.set_cursor(buffer, end);
168 }
169 }
170 this.editor
171 .action(font_system, buffer, Action::Escape, align);
172 } else if this.editor.selection() == cosmic_text::Selection::None {
173 let cursor = this.editor.cursor();
175 this.editor.set_selection(
176 buffer,
177 cosmic_text::Selection::Normal(cursor),
178 );
179 }
180 this.editor.action(
181 font_system,
182 buffer,
183 Action::Motion(match (named_key, ctrl) {
184 (NamedKey::ArrowLeft, false) => {
185 cosmic_text::Motion::Previous
186 }
187 (NamedKey::ArrowRight, false) => cosmic_text::Motion::Next,
188 (NamedKey::ArrowUp, false) => cosmic_text::Motion::Up,
189 (NamedKey::ArrowDown, false) => cosmic_text::Motion::Down,
190 (NamedKey::Home, false) => cosmic_text::Motion::Home,
191 (NamedKey::End, false) => cosmic_text::Motion::End,
192 (NamedKey::PageUp, false) => cosmic_text::Motion::PageUp,
193 (NamedKey::PageDown, false) => {
194 cosmic_text::Motion::PageDown
195 }
196 (NamedKey::ArrowLeft, true) => {
197 cosmic_text::Motion::PreviousWord
198 }
199 (NamedKey::ArrowRight, true) => {
200 cosmic_text::Motion::NextWord
201 }
202 (NamedKey::Home, true) => cosmic_text::Motion::BufferStart,
203 (NamedKey::End, true) => cosmic_text::Motion::BufferEnd,
204 _ => return InputResult::Consume(SmallVec::new()),
205 }),
206 align,
207 )
208 }
209 NamedKey::Select => {
210 this.editor.set_selection(
212 buffer,
213 cosmic_text::Selection::Normal(Cursor {
214 line: 0,
215 index: 0,
216 affinity: cosmic_text::Affinity::Before,
217 }),
218 );
219 this.editor.action(
220 &mut driver.font_system.write(),
221 buffer,
222 Action::Motion(cosmic_text::Motion::BufferEnd),
223 align,
224 );
225 SmallVec::new()
226 }
227 NamedKey::Backspace => this.editor.action(
228 &mut driver.font_system.write(),
229 buffer,
230 Action::Backspace,
231 align,
232 ),
233 NamedKey::Delete => this.editor.action(
234 &mut driver.font_system.write(),
235 buffer,
236 Action::Delete,
237 align,
238 ),
239 NamedKey::Clear => {
240 let change = this
241 .editor
242 .delete_selection(&mut driver.font_system.write(), buffer)
243 .map(|x| SmallVec::from_buf([x]))
244 .unwrap_or_default();
245 this.editor.shape_as_needed(
246 &mut driver.font_system.write(),
247 buffer,
248 false,
249 );
250 change
251 }
252 NamedKey::EraseEof => {
253 let cursor = this.editor.cursor();
254 this.editor
255 .set_selection(buffer, cosmic_text::Selection::Normal(cursor));
256 this.editor.action(
257 &mut driver.font_system.write(),
258 buffer,
259 Action::Motion(cosmic_text::Motion::BufferEnd),
260 align,
261 );
262 let change = this
263 .editor
264 .delete_selection(&mut driver.font_system.write(), buffer)
265 .map(|x| SmallVec::from_buf([x]))
266 .unwrap_or_default();
267 this.editor.shape_as_needed(
268 &mut driver.font_system.write(),
269 buffer,
270 false,
271 );
272 change
273 }
274 NamedKey::Insert => {
275 this.insert_mode = !this.insert_mode;
276 SmallVec::new()
277 }
278 NamedKey::Cut | NamedKey::Copy => {
279 if modifiers & ModifierKeys::Held as u8 == 0
280 && let Some(s) = this.editor.copy_selection(buffer)
281 && let Ok(mut clipboard) = arboard::Clipboard::new()
282 && clipboard.set_text(&s).is_ok()
283 && named_key == NamedKey::Cut
284 {
285 let font_system = &mut driver.font_system.write();
286 if let Some(c) =
289 this.editor.delete_selection(font_system, buffer)
290 {
291 this.editor.shape_as_needed(font_system, buffer, false);
292 this.append(SmallVec::from_buf([c]))
293 }
294 }
295 SmallVec::new()
296 }
297 NamedKey::Paste => {
298 if let Ok(mut clipboard) = arboard::Clipboard::new()
299 && let Ok(s) = clipboard.get_text()
300 {
301 let c = this.editor.insert_string(
302 &mut driver.font_system.write(),
303 buffer,
304 &s,
305 None,
306 align,
307 );
308 this.editor.shape_as_needed(
309 &mut driver.font_system.write(),
310 buffer,
311 false,
312 );
313 this.append(SmallVec::from_buf([c]))
314 }
315 SmallVec::new()
316 }
317 NamedKey::Redo => {
318 if this.undo_index > 0 {
319 this.undo_index =
320 this.redo(&mut driver.font_system.write(), buffer)
321 }
322 SmallVec::new()
323 }
324 NamedKey::Undo => {
325 if this.undo_index > 0 {
326 this.undo_index =
327 this.undo(&mut driver.font_system.write(), buffer)
328 }
329 SmallVec::new()
330 }
331 _ => return InputResult::Forward(SmallVec::new()),
333 };
334
335 this.append(change);
336 obj.set_selection(
337 EditBuffer::from_cursor(buffer, this.editor.selection_or_cursor()),
338 EditBuffer::from_cursor(buffer, this.editor.cursor()),
339 );
340 return InputResult::Consume(SmallVec::new());
341 }
342 return InputResult::Consume(SmallVec::new());
345 }
346 Key::Character(c) => {
347 if down {
348 if let Some(driver) = driver.upgrade() {
349 let c = this.editor.insert_string(
350 &mut driver.font_system.write(),
351 buffer,
352 &c,
353 None,
354 align,
355 );
356 this.append(SmallVec::from_buf([c]));
357
358 this.editor.shape_as_needed(
359 &mut driver.font_system.write(),
360 buffer,
361 false,
362 );
363 obj.set_selection(
364 EditBuffer::from_cursor(buffer, this.editor.selection_or_cursor()),
365 EditBuffer::from_cursor(buffer, this.editor.cursor()),
366 );
367 }
368 return InputResult::Consume(SmallVec::new());
369 }
370 }
371 _ => (),
372 },
373 RawEvent::MouseMove {
374 pos, all_buttons, ..
375 } => {
376 if let Some(d) = driver.upgrade() {
377 *d.cursor.write() = winit::window::CursorIcon::Text;
378 let p =
379 area.topleft() + this.props.padding().resolve(dpi).topleft().to_vector();
380
381 if (all_buttons & MouseButton::Left as u16) != 0 {
382 this.editor.action(
383 &mut d.font_system.write(),
384 buffer,
385 Action::Drag {
386 x: (pos.x - p.x).round() as i32,
387 y: (pos.y - p.y).round() as i32,
388 },
389 align,
390 );
391 }
392 }
393 obj.set_selection(
394 EditBuffer::from_cursor(buffer, this.editor.selection_or_cursor()),
395 EditBuffer::from_cursor(buffer, this.editor.cursor()),
396 );
397 return InputResult::Consume(SmallVec::new());
398 }
399 RawEvent::Mouse {
400 pos, state, button, ..
401 } => {
402 if let Some(d) = driver.upgrade() {
403 let p =
404 area.topleft() + this.props.padding().resolve(dpi).topleft().to_vector();
405
406 let action = match (state, button) {
407 (MouseState::Down, MouseButton::Left) => Action::Click {
408 x: (pos.x - p.x).round() as i32,
409 y: (pos.y - p.y).round() as i32,
410 },
411 (MouseState::DblClick, MouseButton::Left) => Action::DoubleClick {
412 x: (pos.x - p.x).round() as i32,
413 y: (pos.y - p.y).round() as i32,
414 },
415 _ => return InputResult::Consume(SmallVec::new()),
416 };
417 this.editor
418 .action(&mut d.font_system.write(), buffer, action, align);
419 }
420 obj.set_selection(
421 EditBuffer::from_cursor(buffer, this.editor.selection_or_cursor()),
422 EditBuffer::from_cursor(buffer, this.editor.cursor()),
423 );
424 return InputResult::Consume(SmallVec::new());
425 }
426 RawEvent::MouseScroll { delta, .. } => {
427 if let Some(d) = driver.upgrade() {
428 let line_height = buffer.metrics().line_height;
429 match delta {
430 Ok(dist) => {
431 let mut scroll = buffer.scroll();
432 scroll.vertical += dist.y;
434 buffer.set_scroll(scroll);
435 }
436 Err(dist) => {
437 this.editor.action(
438 &mut d.font_system.write(),
439 buffer,
440 Action::Scroll {
441 pixels: -dist.y * line_height,
442 },
443 align,
444 );
445 }
446 }
447 }
448 }
449 _ => (),
450 }
451 InputResult::Forward(SmallVec::new())
452 }
453}
454
455impl Clone for TextBoxState {
456 fn clone(&self) -> Self {
457 Self {
458 last_x_offset: self.last_x_offset,
459 history: self.history.clone(),
460 undo_index: self.undo_index,
461 insert_mode: self.insert_mode,
462 text_count: self.text_count.load(Ordering::Relaxed).into(),
463 cursor_count: self.cursor_count.load(Ordering::Relaxed).into(),
464 focused: self.focused,
465 editor: self.editor.clone(),
466 props: self.props.clone(),
467 align: self.align,
468 }
469 }
470}
471
472impl PartialEq for TextBoxState {
473 fn eq(&self, other: &Self) -> bool {
474 self.last_x_offset == other.last_x_offset
475 && self.history == other.history
476 && self.undo_index == other.undo_index
477 && self.insert_mode == other.insert_mode
478 && self.text_count.load(Ordering::Relaxed) == other.text_count.load(Ordering::Relaxed)
479 && self.cursor_count.load(Ordering::Relaxed)
480 == other.cursor_count.load(Ordering::Relaxed)
481 && self.editor == other.editor
482 && self.align == other.align
483 && Rc::ptr_eq(&self.props, &other.props)
484 }
485}
486
487pub trait Prop: leaf::Padded + base::TextEdit {}
488
489#[derive_where(Clone)]
490pub struct TextBox<T> {
491 id: Arc<SourceID>,
492 props: Rc<T>,
493 pub font_size: f32,
494 pub line_height: f32,
495 pub font: cosmic_text::FamilyOwned,
496 pub color: sRGB,
497 pub weight: cosmic_text::Weight,
498 pub style: cosmic_text::Style,
499 pub wrap: cosmic_text::Wrap,
500 pub align: Option<cosmic_text::Align>,
501 pub slots: [Option<crate::Slot>; TextBoxEvent::SIZE],
502}
503
504impl TextBoxState {
505 fn redo(&mut self, font_system: &mut cosmic_text::FontSystem, buffer: &mut Buffer) -> usize {
506 if self.undo_index < self.history.len() {
509 self.history[self.undo_index] = self.editor.apply_change(
510 font_system,
511 buffer,
512 &self.history[self.undo_index],
513 self.align,
514 );
515 self.undo_index + 1
516 } else {
517 self.undo_index
518 }
519 }
520
521 fn undo(&mut self, font_system: &mut cosmic_text::FontSystem, buffer: &mut Buffer) -> usize {
522 if self.undo_index > 0 {
523 self.history[self.undo_index - 1] = self.editor.apply_change(
524 font_system,
525 buffer,
526 &self.history[self.undo_index - 1],
527 self.align,
528 );
529 self.undo_index - 1
530 } else {
531 self.undo_index
532 }
533 }
534
535 fn append(&mut self, change: SmallVec<[Change; 1]>) {
536 self.history.truncate(self.undo_index);
537 self.undo_index += 1;
538 self.history.push(change);
539 }
540}
541
542impl<T: Prop + 'static> TextBox<T> {
543 pub fn new(
544 id: Arc<SourceID>,
545 props: T,
546 font_size: f32,
547 line_height: f32,
548 font: cosmic_text::FamilyOwned,
549 color: sRGB,
550 weight: cosmic_text::Weight,
551 style: cosmic_text::Style,
552 wrap: cosmic_text::Wrap,
553 align: Option<cosmic_text::Align>,
554 ) -> Self {
555 Self {
556 id: id.clone(),
557 props: props.into(),
558 font_size,
559 line_height,
560 font,
561 color,
562 weight,
563 style,
564 wrap,
565 align,
566 slots: [None],
567 }
568 }
569}
570
571impl<T: Prop + 'static> crate::StateMachineChild for TextBox<T> {
572 fn id(&self) -> Arc<SourceID> {
573 self.id.clone()
574 }
575
576 fn init(
577 &self,
578 _: &std::sync::Weak<crate::Driver>,
579 ) -> Result<Box<dyn super::StateMachineWrapper>, Error> {
580 let statemachine = StateMachine {
581 state: TextBoxState {
582 editor: Editor::new(),
583 last_x_offset: Default::default(),
584 history: Default::default(),
585 undo_index: Default::default(),
586 insert_mode: Default::default(),
587 text_count: Default::default(),
588 cursor_count: Default::default(),
589 focused: Default::default(),
590 props: self.props.clone(),
591 align: self.align,
592 },
593 input_mask: RawEventKind::Focus as u64
594 | RawEventKind::Mouse as u64
595 | RawEventKind::MouseMove as u64
596 | RawEventKind::MouseScroll as u64
597 | RawEventKind::Touch as u64
598 | RawEventKind::Key as u64,
599 output: self.slots.clone(),
600 changed: true,
601 };
602 Ok(Box::new(statemachine))
603 }
604}
605
606impl<T: Prop + 'static> super::Component for TextBox<T> {
607 type Props = T;
608
609 fn layout(
610 &self,
611 manager: &mut crate::StateManager,
612 driver: &crate::graphics::Driver,
613 window: &Arc<SourceID>,
614 ) -> Box<dyn Layout<T>> {
615 let dpi = manager
616 .get::<super::window::WindowStateMachine>(window)
617 .map(|x| x.state.dpi)
618 .unwrap_or(crate::BASE_DPI);
619 let mut font_system = driver.font_system.write();
620
621 let textstate: &mut StateMachine<TextBoxState, { TextBoxEvent::SIZE }> =
622 manager.get_mut(&self.id).unwrap();
623 let textstate = &mut textstate.state;
624 textstate.props = self.props.clone();
625 textstate.align = self.align;
626
627 if self.props.textedit().obj.reflow.load(Ordering::Acquire)
628 || self.props.textedit().dpi != dpi
629 {
630 let attrs = cosmic_text::Attrs::new()
631 .family(self.font.as_family())
632 .color(self.color.into())
633 .weight(self.weight)
634 .style(self.style);
635 self.props.textedit().obj.flowtext(
636 &mut font_system,
637 self.font_size,
638 self.line_height,
639 self.wrap,
640 self.align,
641 dpi,
642 attrs,
643 );
644 }
645
646 let instance = crate::render::textbox::Instance {
647 text_buffer: self.props.textedit().obj.buffer.clone(),
648 padding: self.props.padding().as_perimeter(dpi),
649 selection: textstate
650 .editor
651 .selection_bounds(&self.props.textedit().obj.buffer.borrow()),
652 color: self.color,
653 cursor_color: if textstate.focused {
654 self.color
655 } else {
656 sRGB::transparent()
657 },
658 cursor: textstate.editor.cursor(),
659 selection_bg: sRGB::new(0.2, 0.2, 0.5, 1.0),
660 selection_color: self.color,
661 scale: 1.0,
662 };
663
664 Box::new(layout::text::Node::<T> {
665 props: self.props.clone(),
666 id: Arc::downgrade(&self.id),
667 renderable: Rc::new(instance),
668 buffer: self.props.textedit().obj.buffer.clone(),
669 realign: self.align.is_some_and(|x| x != cosmic_text::Align::Left),
670 })
671 }
672}