feather_ui/component/
textbox.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
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, 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>, // 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    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                    //window.set_ime_cursor_area(position, size);
96                }
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                                    // if a selection doesn't exist, make one.
172                                    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                                // Represents a Select All operation
209                                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                                    // Only delete the text for a cut command if the operation succeeds
285                                    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                            // Do not capture key events we don't recognize
329                            _ => 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                    // Always capture the key event if we recognize it even if we don't do anything with it
340                    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                            //TODO: align to layout lines
428                            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        // 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
502        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}