feather_ui/component/
textbox.rs

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