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