tui_realm_textarea/
lib.rs

1//! # tui-realm-textarea
2//!
3//! [tui-realm-textarea](https://github.com/veeso/tui-realm-textarea) is a
4//! [tui-realm](https://github.com/veeso/tui-realm) implementation of a textarea component.
5//! The tree engine is based on [Orange-trees](https://docs.rs/orange-trees/).
6//!
7//! ## Get Started
8//!
9//! ### Adding `tui-realm-textarea` as dependency
10//!
11//! ```toml
12//! tui-realm-textarea = "2"
13//! ```
14//!
15//! Or if you don't use **Crossterm**, define the backend as you would do with tui-realm:
16//!
17//! ```toml
18//! tui-realm-textarea = { version = "2", default-features = false, features = [ "termion" ] }
19//! ```
20//!
21//! #### Features ⚙️
22
23//! These features can be enabled in tui-realm-textarea:
24//!
25//! - `clipboard` enables system clipboard support
26//! - `search` enables the string search in the textarea
27//!
28//! ## Component API
29//!
30//! **Commands**:
31//!
32//! | Cmd                                            | Result         | Behaviour                               |
33//! |------------------------------------------------|----------------|-----------------------------------------|
34//! | `Custom($TEXTAREA_CMD_NEWLINE)`                | `None`         | Insert newline                          |
35//! | `Custom($TEXTAREA_CMD_DEL_LINE_BY_END)`        | `None`         | Delete line by end to current position  |
36//! | `Custom($TEXTAREA_CMD_DEL_LINE_BY_HEAD)`       | `None`         | Delete line by head to current position |
37//! | `Custom($TEXTAREA_CMD_DEL_WORD)`               | `None`         | Delete the current word                 |
38//! | `Custom($TEXTAREA_CMD_DEL_NEXT_WORD)`          | `None`         | Delete the next word                    |
39//! | `Custom($TEXTAREA_CMD_MOVE_WORD_FORWARD)`      | `None`         | Move to the next word                   |
40//! | `Custom($TEXTAREA_CMD_MOVE_WORD_BACK)`         | `None`         | Move to the previous word               |
41//! | `Custom($TEXTAREA_CMD_MOVE_PARAGRAPH_BACK)`    | `None`         | Move to the previous paragraph          |
42//! | `Custom($TEXTAREA_CMD_MOVE_PARAGRAPH_FORWARD)` | `None`         | Move to the next paragraph              |
43//! | `Custom($TEXTAREA_CMD_MOVE_TOP)`               | `None`         | Move to the beginning of the file       |
44//! | `Custom($TEXTAREA_CMD_MOVE_BOTTOM)`            | `None`         | Move to the end of the file             |
45//! | `Custom($TEXTAREA_CMD_UNDO)`                   | `None`         | Undo last change                        |
46//! | `Custom($TEXTAREA_CMD_REDO)`                   | `None`         | Redo last change                        |
47//! | `Custom($TEXTAREA_CMD_PASTE)`                  | `None`         | Paste the current content of the buffer |
48//! | `Custom($TEXTAREA_CMD_SEARCH_BACK)`            | `None`         | Go to the previous search match         |
49//! | `Custom($TEXTAREA_CMD_SEARCH_FORWARD)`         | `None`         | Go to the next search match             |
50//! | `Cancel`                                       | `None`         | Delete next char                        |
51//! | `Delete`                                       | `None`         | Delete previous char                    |
52//! | `GoTo(Begin)`                                  | `None`         | Go to the head of the line              |
53//! | `GoTo(End)`                                    | `None`         | Go to the end of the line               |
54//! | `Move(Down)`                                   | `None`         | Move to the line below                  |
55//! | `Move(Up)`                                     | `None`         | Move to the line above                  |
56//! | `Move(Left)`                                   | `None`         | Move cursor to the left                 |
57//! | `Move(Right)`                                  | `None`         | Move cursor to the right                |
58//! | `Scroll(Up)`                                   | `None`         | Move by scroll_step lines up            |
59//! | `Scroll(Down)`                                 | `None`         | Move by scroll_step lines down          |
60//! | `Type(ch)`                                     | `None`         | Type a char in the editor               |
61//! | `Submit`                                       | `Submit`       | Get current lines                       |
62//!
63//! > ❗ Paste command is supported only if the `clipboard` feature is enabled
64//!
65//! **State**: the state returned is a `Vec(String)` containing the lines in the text area.
66//!
67//! **Properties**:
68//!
69//! - `Borders(Borders)`: set borders properties for component
70//! - `Custom($TREE_IDENT_SIZE, Size)`: Set space to render for each each depth level
71//! - `Custom($TEXTAREA_MAX_HISTORY, Payload(One(Usize)))`: Set the history steps to record
72//! - `Custom($TEXTAREA_CURSOR_STYLE, Style)`: Set the cursor style
73//! - `Custom($TEXTAREA_CURSOR_LINE_STYLE, Style)`: Set the current line style
74//! - `Custom($TEXTAREA_FOOTER_FMT, Payload(Tup2(Str, Style)))`: Set the format and the style for the footer bar
75//! - `Custom($TEXTAREA_LINE_NUMBER_STYLE, Style)`: set the style for the line number
76//! - `Custom($TEXTAREA_STATUS_FMT, Payload(Tup2(Str, Style)))`: Set the format and the style for the status bar
77//! - `Custom($TEXTAREA_SEARCH_PATTERN, String`: Set search pattern
78//! - `Custom($TEXTAREA_SEARCH_STYLE, Style`: Set search style
79//! - `Custom($TEXTAREA_SINGLE_LINE, Style`: Act as single-line input
80//! - `Style(Style)`: Set the general style for the textarea
81//! - `Custom($TEXTAREA_TAB_SIZE, Size)`: Set the tab size to display
82//! - `FocusStyle(Style)`: inactive style
83//! - `ScrollStep(Length)`: Defines the maximum amount of rows to scroll
84//! - `Title(Title)`: Set box title
85//!
86//! ### Footer and status format
87//!
88//! The status and footer bars support a special syntax. The following keys can be inserted into the string:
89//!
90//! - `{ROW}`: current row
91//! - `{COL}`: current column
92//!
93//! ## Example
94//!
95//! ```rust
96//! use std::{fs, io::{self, BufRead}};
97//! use tuirealm::{
98//!     application::PollStrategy,
99//!     command::{Cmd, CmdResult, Direction, Position},
100//!     event::{Event, Key, KeyEvent, KeyModifiers},
101//!     props::{Alignment, AttrValue, Attribute, BorderType, Borders, Color, Style, TextModifiers},
102//!     terminal::TerminalBridge,
103//!     Application, Component, EventListenerCfg, MockComponent, NoUserEvent, State, StateValue,
104//!     Update,
105//! };
106//! use tui_realm_textarea::TextArea;
107//!
108//! let textarea = match fs::File::open("README.md") {
109//!     Ok(reader) => TextArea::new(
110//!         io::BufReader::new(reader)
111//!             .lines()
112//!             .map(|l| l.unwrap())
113//!             .collect::<_>(),
114//!     ),
115//!     Err(_) => TextArea::default(),
116//! };
117//! let component = textarea
118//!     .borders(
119//!         Borders::default()
120//!             .color(Color::LightYellow)
121//!             .modifiers(BorderType::Double),
122//!     )
123//!     .cursor_line_style(Style::default())
124//!     .cursor_style(Style::default().add_modifier(TextModifiers::REVERSED))
125//!     .footer_bar("Press <ESC> to quit", Style::default())
126//!     .line_number_style(
127//!         Style::default()
128//!             .fg(Color::LightBlue)
129//!             .add_modifier(TextModifiers::ITALIC),
130//!     )
131//!     .max_histories(64)
132//!     .scroll_step(4)
133//!     .status_bar(
134//!         "README.md Ln {ROW}, Col {COL}",
135//!         Style::default().add_modifier(TextModifiers::REVERSED),
136//!     )
137//!     .tab_length(4)
138//!     .title("Editing README.md", Alignment::Left);
139//! ```
140//!
141
142#![doc(html_playground_url = "https://play.rust-lang.org")]
143
144// -- internal
145mod fmt;
146use fmt::LineFmt;
147
148// deps
149
150#[macro_use]
151extern crate lazy_regex;
152
153#[cfg(feature = "clipboard")]
154use cli_clipboard::{ClipboardContext, ClipboardProvider};
155use tui_textarea::{CursorMove, TextArea as TextAreaWidget};
156use tuirealm::command::{Cmd, CmdResult, Direction, Position};
157use tuirealm::props::{
158    Alignment, AttrValue, Attribute, Borders, PropPayload, PropValue, Props, Style, TextModifiers,
159};
160use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout, Rect};
161use tuirealm::ratatui::widgets::{Block, Paragraph};
162use tuirealm::{Frame, MockComponent, State, StateValue};
163
164// -- props
165pub const TEXTAREA_CURSOR_LINE_STYLE: &str = "cursor-line-style";
166pub const TEXTAREA_CURSOR_STYLE: &str = "cursor-style";
167pub const TEXTAREA_FOOTER_FMT: &str = "footer-fmt";
168pub const TEXTAREA_LINE_NUMBER_STYLE: &str = "line-number-style";
169pub const TEXTAREA_MAX_HISTORY: &str = "max-history";
170pub const TEXTAREA_STATUS_FMT: &str = "status-fmt";
171pub const TEXTAREA_TAB_SIZE: &str = "tab-size";
172pub const TEXTAREA_HARD_TAB: &str = "hard-tab";
173pub const TEXTAREA_SINGLE_LINE: &str = "single-line";
174#[cfg(feature = "search")]
175pub const TEXTAREA_SEARCH_PATTERN: &str = "search-pattern";
176#[cfg(feature = "search")]
177pub const TEXTAREA_SEARCH_STYLE: &str = "search-style";
178pub const TEXTAREA_LAYOUT_MARGIN: &str = "layout-margin";
179
180// -- cmd
181pub const TEXTAREA_CMD_NEWLINE: &str = "0";
182pub const TEXTAREA_CMD_DEL_LINE_BY_END: &str = "1";
183pub const TEXTAREA_CMD_DEL_LINE_BY_HEAD: &str = "2";
184pub const TEXTAREA_CMD_DEL_WORD: &str = "3";
185pub const TEXTAREA_CMD_DEL_NEXT_WORD: &str = "4";
186pub const TEXTAREA_CMD_MOVE_WORD_FORWARD: &str = "5";
187pub const TEXTAREA_CMD_MOVE_WORD_BACK: &str = "6";
188pub const TEXTAREA_CMD_MOVE_PARAGRAPH_FORWARD: &str = "7";
189pub const TEXTAREA_CMD_MOVE_PARAGRAPH_BACK: &str = "8";
190pub const TEXTAREA_CMD_MOVE_TOP: &str = "9";
191pub const TEXTAREA_CMD_MOVE_BOTTOM: &str = "a";
192pub const TEXTAREA_CMD_UNDO: &str = "b";
193pub const TEXTAREA_CMD_REDO: &str = "c";
194#[cfg(feature = "clipboard")]
195pub const TEXTAREA_CMD_PASTE: &str = "d";
196#[cfg(feature = "search")]
197pub const TEXTAREA_CMD_SEARCH_FORWARD: &str = "e";
198#[cfg(feature = "search")]
199pub const TEXTAREA_CMD_SEARCH_BACK: &str = "f";
200
201/// textarea tui-realm component
202pub struct TextArea<'a> {
203    props: Props,
204    widget: TextAreaWidget<'a>,
205    /// Status fmt
206    status_fmt: Option<LineFmt>,
207    /// footer fmt
208    footer_fmt: Option<LineFmt>,
209    /// Act as single-line input
210    single_line: bool,
211}
212
213impl<I> From<I> for TextArea<'_>
214where
215    I: IntoIterator,
216    I::Item: Into<String>,
217{
218    fn from(i: I) -> Self {
219        Self::new(i.into_iter().map(|s| s.into()).collect::<Vec<String>>())
220    }
221}
222
223impl Default for TextArea<'_> {
224    fn default() -> Self {
225        Self::new(Vec::default())
226    }
227}
228
229impl<'a> TextArea<'a> {
230    pub fn new(lines: Vec<String>) -> Self {
231        Self {
232            props: Props::default(),
233            widget: TextAreaWidget::new(lines),
234            status_fmt: None,
235            footer_fmt: None,
236            single_line: false,
237        }
238    }
239
240    /// Set another style from default to use when component is inactive
241    pub fn inactive(mut self, s: Style) -> Self {
242        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
243        self
244    }
245
246    /// Set widget border properties
247    pub fn borders(mut self, b: Borders) -> Self {
248        self.attr(Attribute::Borders, AttrValue::Borders(b));
249        self
250    }
251
252    /// Set widget title
253    pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
254        self.attr(
255            Attribute::Title,
256            AttrValue::Title((t.as_ref().to_string(), a)),
257        );
258        self
259    }
260
261    /// Set scroll step for scrolling command
262    pub fn scroll_step(mut self, step: usize) -> Self {
263        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
264        self
265    }
266
267    /// Set how many modifications are remembered for undo/redo. Setting 0 disables undo/redo.
268    pub fn max_histories(mut self, max: usize) -> Self {
269        self.attr(
270            Attribute::Custom(TEXTAREA_MAX_HISTORY),
271            AttrValue::Payload(PropPayload::One(PropValue::Usize(max))),
272        );
273        self
274    }
275
276    /// Set text editor cursor style
277    pub fn cursor_style(mut self, s: Style) -> Self {
278        self.attr(
279            Attribute::Custom(TEXTAREA_CURSOR_STYLE),
280            AttrValue::Style(s),
281        );
282        self
283    }
284
285    /// Set text editor style for selected line
286    pub fn cursor_line_style(mut self, s: Style) -> Self {
287        self.attr(
288            Attribute::Custom(TEXTAREA_CURSOR_LINE_STYLE),
289            AttrValue::Style(s),
290        );
291        self
292    }
293
294    /// Set footer bar fmt and style for the footer bar
295    /// Default: no footer bar is displayed
296    pub fn footer_bar(mut self, fmt: &str, style: Style) -> Self {
297        self.attr(
298            Attribute::Custom(TEXTAREA_FOOTER_FMT),
299            AttrValue::Payload(PropPayload::Tup2((
300                PropValue::Str(fmt.to_string()),
301                PropValue::Style(style),
302            ))),
303        );
304        self
305    }
306
307    /// Set text editor style for line numbers
308    pub fn line_number_style(mut self, s: Style) -> Self {
309        self.attr(
310            Attribute::Custom(TEXTAREA_LINE_NUMBER_STYLE),
311            AttrValue::Style(s),
312        );
313        self
314    }
315
316    /// Set status bar fmt and style for the status bar
317    /// Default: no status bar is displayed
318    pub fn status_bar(mut self, fmt: &str, style: Style) -> Self {
319        self.attr(
320            Attribute::Custom(TEXTAREA_STATUS_FMT),
321            AttrValue::Payload(PropPayload::Tup2((
322                PropValue::Str(fmt.to_string()),
323                PropValue::Style(style),
324            ))),
325        );
326        self
327    }
328
329    /// Set text style for editor
330    pub fn style(mut self, s: Style) -> Self {
331        self.attr(Attribute::Style, AttrValue::Style(s));
332        self
333    }
334
335    /// Set `<TAB>` size
336    pub fn tab_length(mut self, l: u8) -> Self {
337        self.attr(
338            Attribute::Custom(TEXTAREA_TAB_SIZE),
339            AttrValue::Size(l as u16),
340        );
341        self
342    }
343
344    /// Set another style from default to use when component is inactive
345    pub fn hard_tab(mut self, enabled: bool) -> Self {
346        self.attr(
347            Attribute::Custom(TEXTAREA_HARD_TAB),
348            AttrValue::Flag(enabled),
349        );
350        self
351    }
352
353    /// Set single-line behavior
354    pub fn single_line(mut self, single_line: bool) -> Self {
355        self.attr(
356            Attribute::Custom(TEXTAREA_SINGLE_LINE),
357            AttrValue::Flag(single_line),
358        );
359        self
360    }
361
362    #[cfg(feature = "search")]
363    /// Set search style
364    pub fn search_style(mut self, s: Style) -> Self {
365        self.attr(
366            Attribute::Custom(TEXTAREA_SEARCH_STYLE),
367            AttrValue::Style(s),
368        );
369        self
370    }
371
372    /// Set margin of layout
373    pub fn layout_margin(mut self, margin: u16) -> Self {
374        self.attr(
375            Attribute::Custom(TEXTAREA_LAYOUT_MARGIN),
376            AttrValue::Size(margin),
377        );
378        self
379    }
380
381    // -- private
382    fn get_block(&self) -> Option<Block<'a>> {
383        let mut block = Block::default();
384        if let Some(AttrValue::Title((title, alignment))) = self.query(Attribute::Title) {
385            block = block.title(title).title_alignment(alignment);
386        }
387        if let Some(AttrValue::Borders(borders)) = self.query(Attribute::Borders) {
388            let inactive_style = self
389                .query(Attribute::FocusStyle)
390                .unwrap_or_else(|| AttrValue::Style(Style::default()))
391                .unwrap_style();
392            let focus = self
393                .props
394                .get_or(Attribute::Focus, AttrValue::Flag(false))
395                .unwrap_flag();
396
397            return Some(
398                block
399                    .border_style(match focus {
400                        true => borders.style(),
401                        false => inactive_style,
402                    })
403                    .border_type(borders.modifiers)
404                    .borders(borders.sides),
405            );
406        }
407
408        None
409    }
410
411    #[cfg(feature = "clipboard")]
412    fn paste(&mut self) {
413        // get content from context
414        if let Ok(Ok(yank)) = ClipboardContext::new().map(|mut ctx| ctx.get_contents()) {
415            // TODO: It's desired to set and paste yanked text, but pasting new lines as part of the yanked
416            // text is currently not supported by the textarea widget. Therefor, each line is inserted
417            // separately. The disadvantage of this workaround is, that each newly inserted line is a
418            // separate entry in the history and therefor a separate undo step.
419            if self.single_line {
420                self.widget.insert_str(yank);
421            } else {
422                for line in yank.lines() {
423                    self.widget.insert_str(line);
424                    self.widget.insert_newline();
425                }
426            }
427        }
428    }
429}
430
431impl MockComponent for TextArea<'_> {
432    fn view(&mut self, frame: &mut Frame, area: Rect) {
433        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
434            // set block
435            if let Some(block) = self.get_block() {
436                self.widget.set_block(block);
437            }
438            let margin_prop = self
439                .props
440                .get_or(
441                    Attribute::Custom(TEXTAREA_LAYOUT_MARGIN),
442                    AttrValue::Size(1),
443                )
444                .unwrap_size();
445            let margin = if self.get_block().is_some() {
446                margin_prop
447            } else {
448                0
449            };
450            // make chunks
451            let chunks = Layout::default()
452                .direction(LayoutDirection::Vertical)
453                .margin(margin)
454                .constraints(
455                    [
456                        Constraint::Min(1),
457                        Constraint::Length(if self.status_fmt.is_some() { 1 } else { 0 }),
458                        Constraint::Length(if self.footer_fmt.is_some() { 1 } else { 0 }),
459                    ]
460                    .as_ref(),
461                )
462                .split(area);
463
464            // Remove cursor if not in focus
465            let focus = self
466                .props
467                .get_or(Attribute::Focus, AttrValue::Flag(false))
468                .unwrap_flag();
469            if !focus {
470                self.widget.set_cursor_style(Style::reset());
471            } else {
472                let style = self
473                    .props
474                    .get_or(
475                        Attribute::Custom(TEXTAREA_CURSOR_STYLE),
476                        AttrValue::Style(Style::default().add_modifier(TextModifiers::REVERSED)),
477                    )
478                    .unwrap_style();
479                self.widget.set_cursor_style(style);
480            }
481
482            // render widget
483            frame.render_widget(&self.widget, chunks[0]);
484            if let Some(fmt) = self.status_fmt.as_ref() {
485                frame.render_widget(
486                    Paragraph::new(fmt.fmt(&self.widget)).style(fmt.style()),
487                    chunks[1],
488                );
489            }
490            if let Some(fmt) = self.footer_fmt.as_ref() {
491                frame.render_widget(
492                    Paragraph::new(fmt.fmt(&self.widget)).style(fmt.style()),
493                    chunks[2],
494                );
495            }
496        }
497    }
498
499    fn query(&self, attr: Attribute) -> Option<AttrValue> {
500        self.props.get(attr)
501    }
502
503    fn attr(&mut self, attr: Attribute, value: AttrValue) {
504        self.props.set(attr, value.clone());
505        match (attr, value) {
506            (Attribute::Custom(TEXTAREA_CURSOR_STYLE), AttrValue::Style(s)) => {
507                self.widget.set_cursor_style(s);
508            }
509            (Attribute::Custom(TEXTAREA_CURSOR_LINE_STYLE), AttrValue::Style(s)) => {
510                self.widget.set_cursor_line_style(s);
511            }
512            (
513                Attribute::Custom(TEXTAREA_FOOTER_FMT),
514                AttrValue::Payload(PropPayload::Tup2((
515                    PropValue::Str(fmt),
516                    PropValue::Style(style),
517                ))),
518            ) => {
519                self.footer_fmt = Some(LineFmt::new(&fmt, style));
520            }
521            (
522                Attribute::Custom(TEXTAREA_MAX_HISTORY),
523                AttrValue::Payload(PropPayload::One(PropValue::Usize(max))),
524            ) => {
525                self.widget.set_max_histories(max);
526            }
527            (
528                Attribute::Custom(TEXTAREA_STATUS_FMT),
529                AttrValue::Payload(PropPayload::Tup2((
530                    PropValue::Str(fmt),
531                    PropValue::Style(style),
532                ))),
533            ) => {
534                self.status_fmt = Some(LineFmt::new(&fmt, style));
535            }
536            (Attribute::Custom(TEXTAREA_LINE_NUMBER_STYLE), AttrValue::Style(s)) => {
537                self.widget.set_line_number_style(s);
538            }
539            (Attribute::Custom(TEXTAREA_TAB_SIZE), AttrValue::Size(size)) => {
540                self.widget.set_tab_length(size as u8);
541            }
542            (Attribute::Custom(TEXTAREA_HARD_TAB), AttrValue::Flag(enabled)) => {
543                self.widget.set_hard_tab_indent(enabled);
544            }
545            (Attribute::Custom(TEXTAREA_SINGLE_LINE), AttrValue::Flag(single_line)) => {
546                self.single_line = single_line;
547            }
548            #[cfg(feature = "search")]
549            (Attribute::Custom(TEXTAREA_SEARCH_PATTERN), AttrValue::String(pattern)) => {
550                let _ = self.widget.set_search_pattern(pattern);
551            }
552            #[cfg(feature = "search")]
553            (Attribute::Custom(TEXTAREA_SEARCH_STYLE), AttrValue::Style(s)) => {
554                self.widget.set_search_style(s);
555            }
556            (Attribute::Style, AttrValue::Style(s)) => {
557                self.widget.set_style(s);
558            }
559            (_, _) => {
560                if let Some(block) = self.get_block() {
561                    self.widget.set_block(block);
562                }
563            }
564        }
565    }
566
567    fn state(&self) -> State {
568        State::Vec(
569            self.widget
570                .lines()
571                .iter()
572                .map(|x| StateValue::String(x.to_string()))
573                .collect(),
574        )
575    }
576
577    fn perform(&mut self, cmd: Cmd) -> CmdResult {
578        match cmd {
579            Cmd::Cancel => {
580                self.widget.delete_next_char();
581                CmdResult::None
582            }
583            Cmd::Custom(TEXTAREA_CMD_DEL_LINE_BY_END) => {
584                self.widget.delete_line_by_end();
585                CmdResult::None
586            }
587            Cmd::Custom(TEXTAREA_CMD_DEL_LINE_BY_HEAD) => {
588                self.widget.delete_line_by_head();
589                CmdResult::None
590            }
591            Cmd::Custom(TEXTAREA_CMD_DEL_NEXT_WORD) => {
592                self.widget.delete_next_word();
593                CmdResult::None
594            }
595            Cmd::Custom(TEXTAREA_CMD_DEL_WORD) => {
596                self.widget.delete_word();
597                CmdResult::None
598            }
599            Cmd::Custom(TEXTAREA_CMD_MOVE_PARAGRAPH_BACK) => {
600                self.widget.move_cursor(CursorMove::ParagraphBack);
601                CmdResult::None
602            }
603            Cmd::Custom(TEXTAREA_CMD_MOVE_PARAGRAPH_FORWARD) => {
604                self.widget.move_cursor(CursorMove::ParagraphForward);
605                CmdResult::None
606            }
607            Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_BACK) => {
608                self.widget.move_cursor(CursorMove::WordBack);
609                CmdResult::None
610            }
611            Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_FORWARD) => {
612                self.widget.move_cursor(CursorMove::WordForward);
613                CmdResult::None
614            }
615            Cmd::Custom(TEXTAREA_CMD_MOVE_BOTTOM) => {
616                if !self.single_line {
617                    self.widget.move_cursor(CursorMove::Bottom);
618                }
619                CmdResult::None
620            }
621            Cmd::Custom(TEXTAREA_CMD_MOVE_TOP) => {
622                if !self.single_line {
623                    self.widget.move_cursor(CursorMove::Top);
624                }
625                CmdResult::None
626            }
627            #[cfg(feature = "clipboard")]
628            Cmd::Custom(TEXTAREA_CMD_PASTE) => {
629                self.paste();
630                CmdResult::None
631            }
632            Cmd::Custom(TEXTAREA_CMD_REDO) => {
633                self.widget.redo();
634                CmdResult::None
635            }
636            #[cfg(feature = "search")]
637            Cmd::Custom(TEXTAREA_CMD_SEARCH_BACK) => {
638                self.widget.search_back(true);
639                CmdResult::None
640            }
641            #[cfg(feature = "search")]
642            Cmd::Custom(TEXTAREA_CMD_SEARCH_FORWARD) => {
643                self.widget.search_forward(true);
644                CmdResult::None
645            }
646            Cmd::Custom(TEXTAREA_CMD_UNDO) => {
647                self.widget.undo();
648                CmdResult::None
649            }
650            Cmd::Delete => {
651                self.widget.delete_char();
652                CmdResult::None
653            }
654            Cmd::GoTo(Position::Begin) => {
655                self.widget.move_cursor(CursorMove::Head);
656                CmdResult::None
657            }
658            Cmd::GoTo(Position::End) => {
659                self.widget.move_cursor(CursorMove::End);
660                CmdResult::None
661            }
662            Cmd::Move(Direction::Down) => {
663                if !self.single_line {
664                    self.widget.move_cursor(CursorMove::Down);
665                }
666                CmdResult::None
667            }
668            Cmd::Move(Direction::Left) => {
669                self.widget.move_cursor(CursorMove::Back);
670                CmdResult::None
671            }
672            Cmd::Move(Direction::Right) => {
673                self.widget.move_cursor(CursorMove::Forward);
674                CmdResult::None
675            }
676            Cmd::Move(Direction::Up) => {
677                if !self.single_line {
678                    self.widget.move_cursor(CursorMove::Up);
679                }
680                CmdResult::None
681            }
682            Cmd::Scroll(Direction::Down) => {
683                if !self.single_line {
684                    let step = self
685                        .props
686                        .get_or(Attribute::ScrollStep, AttrValue::Length(8))
687                        .unwrap_length();
688                    (0..step).for_each(|_| self.widget.move_cursor(CursorMove::Down));
689                }
690                CmdResult::None
691            }
692            Cmd::Scroll(Direction::Up) => {
693                if !self.single_line {
694                    let step = self
695                        .props
696                        .get_or(Attribute::ScrollStep, AttrValue::Length(8))
697                        .unwrap_length();
698                    (0..step).for_each(|_| self.widget.move_cursor(CursorMove::Up));
699                }
700                CmdResult::None
701            }
702            Cmd::Type('\t') => {
703                self.widget.insert_tab();
704                CmdResult::None
705            }
706            Cmd::Type('\n') | Cmd::Custom(TEXTAREA_CMD_NEWLINE) => {
707                if !self.single_line {
708                    self.widget.insert_newline();
709                }
710                CmdResult::None
711            }
712            Cmd::Type(ch) => {
713                self.widget.insert_char(ch);
714                CmdResult::None
715            }
716            Cmd::Submit => CmdResult::Submit(self.state()),
717            _ => CmdResult::None,
718        }
719    }
720}