Skip to main content

modalkit_ratatui/
textbox.rs

1//! # Text box
2//!
3//! ## Overview
4//!
5//! This text box provides a view of a shared editing buffer, and, on top of passing operations
6//! through to an [EditBuffer], is capable of doing the following:
7//!
8//! - Toggling wrapped and non-wrapped views of the buffer's
9//! - Scrolling through the buffer's contents
10//! - Rendering line annotations in left and right gutters
11//!
12//! [EditBuffer]: modalkit::editing::buffer::EditBuffer
13//!
14//! ## Example
15//!
16//! ```
17//! use modalkit::editing::application::EmptyInfo;
18//! use modalkit::editing::store::Store;
19//! use modalkit_ratatui::textbox::TextBoxState;
20//!
21//! use ratatui::layout::Rect;
22//!
23//! let mut store = Store::<EmptyInfo>::default();
24//! let buffer = store.load_buffer(String::from("*scratch*"));
25//! let mut tbox = TextBoxState::new(buffer);
26//!
27//! tbox.set_term_info(Rect::new(0, 0, 6, 4));
28//!
29//! tbox.set_text("a\nb\nc\nd\ne\nf\n");
30//! assert_eq!(tbox.get_text(), "a\nb\nc\nd\ne\nf\n");
31//! assert_eq!(tbox.get_lines(), 6);
32//! assert_eq!(tbox.has_lines(4), 4);
33//! assert_eq!(tbox.has_lines(6), 6);
34//! assert_eq!(tbox.has_lines(8), 6);
35//! ```
36use std::convert::TryInto;
37use std::iter::Iterator;
38use std::marker::PhantomData;
39
40use ratatui::{
41    buffer::Buffer,
42    layout::Rect,
43    style::{Modifier, Style},
44    text::Span,
45    widgets::{Block, StatefulWidget, Widget},
46};
47
48use modalkit::actions::*;
49use modalkit::editing::{
50    application::{ApplicationInfo, EmptyInfo},
51    buffer::{CursorGroupId, FollowersInfo, HighlightInfo},
52    completion::CompletionList,
53    context::{EditContext, Resolve},
54    cursor::Cursor,
55    rope::{CharOff, EditRope},
56    store::{SharedBuffer, Store},
57};
58use modalkit::errors::{EditError, EditResult, UIResult};
59use modalkit::prelude::*;
60
61use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
62
63use super::{ScrollActions, TerminalCursor, WindowOps};
64
65/// Line annotation shown in the left gutter.
66pub struct LeftGutterInfo {
67    text: String,
68    style: Style,
69}
70
71impl LeftGutterInfo {
72    /// Create a new instance.
73    pub fn new(text: String, style: Style) -> Self {
74        LeftGutterInfo { text, style }
75    }
76
77    fn render(&self, area: Rect, buf: &mut Buffer) {
78        let _ = buf.set_stringn(area.x, area.y, &self.text, area.width as usize, self.style);
79    }
80}
81
82/// Line annotation shown in the right gutter.
83pub struct RightGutterInfo {
84    text: String,
85    style: Style,
86}
87
88impl RightGutterInfo {
89    /// Create a new instance.
90    pub fn new(text: String, style: Style) -> Self {
91        RightGutterInfo { text, style }
92    }
93
94    fn render(&self, area: Rect, buf: &mut Buffer) {
95        let _ = buf.set_stringn(area.x, area.y, &self.text, area.width as usize, self.style);
96    }
97}
98
99/// Persistent state for [TextBox].
100pub struct TextBoxState<I: ApplicationInfo = EmptyInfo> {
101    buffer: SharedBuffer<I>,
102    group_id: CursorGroupId,
103    readonly: bool,
104
105    viewctx: ViewportContext<Cursor>,
106    term_cursor: (u16, u16),
107}
108
109/// Widget for rendering a multi-line text box.
110pub struct TextBox<'a, I: ApplicationInfo = EmptyInfo> {
111    block: Option<Block<'a>>,
112    prompt: Span<'a>,
113    oneline: bool,
114    style: Style,
115
116    lgutter_width: u16,
117    rgutter_width: u16,
118
119    _pc: PhantomData<I>,
120}
121
122/*
123 * If the cursor has moved outside of the viewport, update the corner of the viewport so that the
124 * cursor is visible onscreen again.
125 */
126fn shift_corner_nowrap(cursor: &Cursor, corner: &mut Cursor, width: usize, height: usize) {
127    if cursor.y < corner.y {
128        corner.set_y(cursor.y);
129    } else if cursor.y >= corner.y + height {
130        corner.set_y(cursor.y - height + 1);
131    }
132
133    if cursor.x < corner.x {
134        corner.set_x(cursor.x);
135    } else if cursor.x >= corner.x + width {
136        corner.set_x(cursor.x - width + 1);
137    }
138}
139
140fn shift_corner_wrap(cursor: &Cursor, corner: &mut Cursor, height: usize) {
141    if cursor.y < corner.y {
142        corner.set_y(cursor.y);
143        corner.set_x(0);
144    } else if cursor.y >= corner.y + height {
145        corner.set_y(cursor.y - height + 1);
146        corner.set_x(0);
147    } else if cursor.y == corner.y && cursor.x < corner.x {
148        corner.set_x(0);
149    }
150}
151
152fn shift_corner_oneline(cursor: &Cursor, corner: &mut Cursor) {
153    if cursor < corner {
154        corner.set_y(cursor.y);
155        corner.set_x(cursor.x);
156    }
157}
158
159fn shift_corner(
160    viewctx: &mut ViewportContext<Cursor>,
161    cursor: &Cursor,
162    width: usize,
163    height: usize,
164) {
165    if viewctx.wrap {
166        shift_corner_wrap(cursor, &mut viewctx.corner, height);
167    } else {
168        shift_corner_nowrap(cursor, &mut viewctx.corner, width, height);
169    }
170}
171
172/*
173 * If the cursor has moved outside of the viewport, move the cursor back within the boundaries of
174 * the viewport, so it is visible onscreen again.
175 */
176fn shift_cursor(cursor: &mut Cursor, corner: &Cursor, width: usize, height: usize) {
177    if cursor.y < corner.y {
178        cursor.set_y(corner.y);
179    } else if cursor.y >= corner.y + height {
180        cursor.set_y(corner.y + height - 1);
181    }
182
183    if cursor.x < corner.x {
184        cursor.set_x(corner.x);
185    } else if cursor.x >= corner.x + width {
186        cursor.set_x(corner.x + width - 1);
187    }
188}
189
190impl<I> TextBoxState<I>
191where
192    I: ApplicationInfo,
193{
194    /// Create state for a new text box.
195    pub fn new(buffer: SharedBuffer<I>) -> Self {
196        let mut viewctx = ViewportContext::default();
197        let group_id = buffer.write().unwrap().create_group();
198
199        viewctx.set_wrap(true);
200
201        TextBoxState {
202            buffer,
203            group_id,
204            readonly: false,
205
206            viewctx,
207            term_cursor: (0, 0),
208        }
209    }
210
211    /// Get a reference to the shared buffer used by this text box.
212    pub fn buffer(&self) -> SharedBuffer<I> {
213        self.buffer.clone()
214    }
215
216    /// Indicates whether the buffer contents are readonly.
217    pub fn is_readonly(&self) -> bool {
218        self.readonly
219    }
220
221    /// Set whether the buffer contents are modifiable through the [Editable] trait.
222    pub fn set_readonly(&mut self, readonly: bool) {
223        self.readonly = readonly;
224    }
225
226    /// Get the contents of the underlying buffer as an [EditRope].
227    pub fn get(&self) -> EditRope {
228        self.buffer.read().unwrap().get().clone()
229    }
230
231    /// Get the contents of the underlying buffer as a [String].
232    pub fn get_text(&self) -> String {
233        self.buffer.read().unwrap().get_text()
234    }
235
236    /// Replace the contents of the text box's underlying buffer.
237    pub fn set_text<T: Into<EditRope>>(&mut self, t: T) {
238        self.buffer.write().unwrap().set_text(t)
239    }
240
241    /// Clear the text box's underlying buffer of its content, and return it.
242    pub fn reset(&mut self) -> EditRope {
243        self.buffer.write().unwrap().reset()
244    }
245
246    /// Clear the text box's underlying buffer of its content, and return it as a [String].
247    pub fn reset_text(&mut self) -> String {
248        self.buffer.write().unwrap().reset_text()
249    }
250
251    /// Create or update a line annotation for the left gutter.
252    pub fn set_left_gutter(&mut self, line: usize, s: String, style: Option<Style>) {
253        let style = style.unwrap_or_default();
254        let info = LeftGutterInfo::new(s, style);
255
256        self.buffer.write().unwrap().set_line_info(line, info);
257    }
258
259    /// Create or update a line annotation for the right gutter.
260    pub fn set_right_gutter(&mut self, line: usize, s: String, style: Option<Style>) {
261        let style = style.unwrap_or_default();
262        let info = RightGutterInfo::new(s, style);
263
264        self.buffer.write().unwrap().set_line_info(line, info);
265    }
266
267    /// Control whether the text box should wrap long lines when displaying them.
268    pub fn set_wrap(&mut self, wrap: bool) {
269        self.viewctx.set_wrap(wrap);
270    }
271
272    /// Inform the text box what its dimensions and placement on the terminal window is.
273    pub fn set_term_info(&mut self, area: Rect) {
274        self.viewctx.dimensions = (area.width as usize, area.height as usize);
275    }
276
277    /// Get the leader cursor for this text box's cursor group.
278    pub fn get_cursor(&mut self) -> Cursor {
279        self.buffer.write().unwrap().get_leader(self.group_id)
280    }
281
282    /// Calculate how many lines are in this text box.
283    pub fn get_lines(&self) -> usize {
284        self.buffer.read().unwrap().get_lines()
285    }
286
287    /// Check whether this text box is capable of displaying `max` lines.
288    ///
289    /// If there are fewer lines available than `max`, this returns the same value as
290    /// [get_lines()](TextBoxState::get_lines).
291    /// Otherwise, this returns `max`.
292    ///
293    /// This method is useful for building additional widgets that want to create a [TextBox] with a
294    /// flexible height up to `max` lines.
295    pub fn has_lines(&self, max: usize) -> usize {
296        if self.viewctx.wrap {
297            let width = self.viewctx.get_width();
298            let mut count = 0;
299
300            if width == 0 {
301                return count;
302            }
303
304            let mut fline = false;
305
306            for line in self.buffer.read().unwrap().lines(0) {
307                count += 1;
308                let mut line_width = 0;
309                for c in line.chars(CharOff::from(0)) {
310                    let c_width = c.width_cjk().unwrap_or(1);
311                    if line_width + c_width > width {
312                        count += 1;
313                        line_width = c_width;
314                    } else {
315                        line_width += c_width;
316                    }
317                }
318                fline |= line_width == width;
319
320                if count >= max {
321                    return max;
322                }
323            }
324
325            if fline {
326                // At least one of our lines is the full area width, so
327                // we bump the count by 1 to move closer to the max, so
328                // that moving the cursor to the line end doesn't move
329                // the viewport corner in annoying ways.
330                count += 1;
331            }
332
333            return count;
334        } else {
335            self.buffer.read().unwrap().get_lines().min(max)
336        }
337    }
338}
339
340macro_rules! c2cgi {
341    ($s: expr, $ctx: expr) => {
342        &($s.group_id, &$s.viewctx, $ctx)
343    };
344}
345
346impl<I> Editable<EditContext, Store<I>, I> for TextBoxState<I>
347where
348    I: ApplicationInfo,
349{
350    fn editor_command(
351        &mut self,
352        act: &EditorAction,
353        ctx: &EditContext,
354        store: &mut Store<I>,
355    ) -> EditResult<EditInfo, I> {
356        if self.readonly && !act.is_readonly(ctx) {
357            Err(EditError::ReadOnly)
358        } else {
359            self.buffer.editor_command(act, c2cgi!(self, ctx), store)
360        }
361    }
362}
363
364impl<I> Jumpable<EditContext, I> for TextBoxState<I>
365where
366    I: ApplicationInfo,
367{
368    fn jump(
369        &mut self,
370        list: PositionList,
371        dir: MoveDir1D,
372        count: usize,
373        ctx: &EditContext,
374    ) -> UIResult<usize, I> {
375        self.buffer.jump(list, dir, count, c2cgi!(self, ctx))
376    }
377}
378
379impl<I> Promptable<EditContext, Store<I>, I> for TextBoxState<I>
380where
381    I: ApplicationInfo,
382{
383    fn prompt(
384        &mut self,
385        _: &PromptAction,
386        _: &EditContext,
387        _: &mut Store<I>,
388    ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
389        Err(EditError::Failure("Not at a prompt".to_string()))
390    }
391}
392
393impl<I> Searchable<EditContext, Store<I>, I> for TextBoxState<I>
394where
395    I: ApplicationInfo,
396{
397    fn search(
398        &mut self,
399        dir: MoveDirMod,
400        count: Count,
401        ctx: &EditContext,
402        store: &mut Store<I>,
403    ) -> UIResult<EditInfo, I> {
404        self.buffer.search(dir, count, c2cgi!(self, ctx), store)
405    }
406}
407
408impl<I> ScrollActions<EditContext, Store<I>, I> for TextBoxState<I>
409where
410    I: ApplicationInfo,
411{
412    fn dirscroll(
413        &mut self,
414        dir: MoveDir2D,
415        size: ScrollSize,
416        count: &Count,
417        ctx: &EditContext,
418        _: &mut Store<I>,
419    ) -> EditResult<EditInfo, I> {
420        let count = ctx.resolve(count);
421
422        let height = self.viewctx.dimensions.1;
423        let rows = match size {
424            ScrollSize::Cell => count,
425            ScrollSize::HalfPage => count.saturating_mul(height) / 2,
426            ScrollSize::Page => count.saturating_mul(height),
427        };
428
429        let width = self.viewctx.dimensions.0;
430        let cols = match size {
431            ScrollSize::Cell => count,
432            ScrollSize::HalfPage => count.saturating_mul(width) / 2,
433            ScrollSize::Page => count.saturating_mul(width),
434        };
435
436        match (dir, self.viewctx.wrap) {
437            (MoveDir2D::Up, _) => self.viewctx.corner.up(rows),
438            (MoveDir2D::Down, _) => self.viewctx.corner.down(rows),
439            (MoveDir2D::Left, false) => self.viewctx.corner.left(cols),
440            (MoveDir2D::Right, false) => self.viewctx.corner.right(cols),
441            (MoveDir2D::Left | MoveDir2D::Right, true) => (),
442        };
443
444        /*
445         * We do a quick dance here: moving the viewport should move the cursor so that it stays
446         * visible on the screen. The cursor should not be shifted past the last line or last
447         * column, though, so we clamp it after shifting it. Since the cursor should never
448         * be off-screen, this also sets a boundary of how far we can move the viewport.
449         */
450        let mut cursor = self.get_cursor();
451        let mut buffer = self.buffer.write().unwrap();
452        shift_cursor(&mut cursor, &self.viewctx.corner, width, height);
453        buffer.clamp(&mut cursor, c2cgi!(self, ctx));
454        shift_corner(&mut self.viewctx, &cursor, width, height);
455        buffer.set_leader(self.group_id, cursor);
456
457        Ok(None)
458    }
459
460    fn cursorpos(
461        &mut self,
462        pos: MovePosition,
463        axis: Axis,
464        _: &EditContext,
465        _: &mut Store<I>,
466    ) -> EditResult<EditInfo, I> {
467        if axis == Axis::Horizontal && self.viewctx.wrap {
468            return Ok(None);
469        }
470
471        let (width, height) = self.viewctx.dimensions;
472        let cursor = self.get_cursor();
473        shift_corner(&mut self.viewctx, &cursor, width, height);
474
475        match (axis, pos) {
476            (Axis::Horizontal, MovePosition::Beginning) => {
477                self.viewctx.corner.set_x(cursor.x);
478            },
479            (Axis::Horizontal, MovePosition::Middle) => {
480                let off = cursor.x.saturating_add(1).saturating_sub(width / 2);
481
482                self.viewctx.corner.set_x(off);
483            },
484            (Axis::Horizontal, MovePosition::End) => {
485                let off = cursor.x.saturating_add(1).saturating_sub(width);
486
487                self.viewctx.corner.set_x(off);
488            },
489            (Axis::Vertical, MovePosition::Beginning) => {
490                self.viewctx.corner.set_y(cursor.y);
491            },
492            (Axis::Vertical, MovePosition::Middle) => {
493                let off = cursor.y.saturating_add(1).saturating_sub(height / 2);
494
495                self.viewctx.corner.set_y(off);
496            },
497            (Axis::Vertical, MovePosition::End) => {
498                let off = cursor.y.saturating_add(1).saturating_sub(height);
499
500                self.viewctx.corner.set_y(off);
501            },
502        }
503
504        Ok(None)
505    }
506
507    fn linepos(
508        &mut self,
509        pos: MovePosition,
510        count: &Count,
511        ctx: &EditContext,
512        _: &mut Store<I>,
513    ) -> EditResult<EditInfo, I> {
514        let mut buffer = self.buffer.write().unwrap();
515        let max = buffer.get_lines();
516        let line = ctx.resolve(count).min(max).saturating_sub(1);
517
518        let height = self.viewctx.get_height();
519
520        buffer.set_leader(self.group_id, Cursor::new(line, 0));
521
522        match pos {
523            MovePosition::Beginning => {
524                self.viewctx.corner.set_y(line);
525            },
526            MovePosition::Middle => {
527                let off = line.saturating_add(1).saturating_sub(height / 2);
528
529                self.viewctx.corner.set_y(off);
530            },
531            MovePosition::End => {
532                let off = line.saturating_add(1).saturating_sub(height);
533
534                self.viewctx.corner.set_y(off);
535            },
536        }
537
538        Ok(None)
539    }
540}
541
542impl<I> Scrollable<EditContext, Store<I>, I> for TextBoxState<I>
543where
544    I: ApplicationInfo,
545{
546    fn scroll(
547        &mut self,
548        style: &ScrollStyle,
549        ctx: &EditContext,
550        store: &mut Store<I>,
551    ) -> EditResult<EditInfo, I> {
552        match style {
553            ScrollStyle::Direction2D(dir, size, count) => {
554                return self.dirscroll(*dir, *size, count, ctx, store);
555            },
556            ScrollStyle::CursorPos(pos, axis) => {
557                return self.cursorpos(*pos, *axis, ctx, store);
558            },
559            ScrollStyle::LinePos(pos, count) => {
560                return self.linepos(*pos, count, ctx, store);
561            },
562        }
563    }
564}
565
566impl<I> TerminalCursor for TextBoxState<I>
567where
568    I: ApplicationInfo,
569{
570    fn get_term_cursor(&self) -> Option<(u16, u16)> {
571        if self.viewctx.get_height() == 0 {
572            return None;
573        }
574
575        self.term_cursor.into()
576    }
577}
578
579impl<I> WindowOps<I> for TextBoxState<I>
580where
581    I: ApplicationInfo,
582{
583    fn dup(&self, _: &mut Store<I>) -> Self {
584        let buffer = self.buffer.clone();
585        let group_id = buffer.write().unwrap().create_group();
586
587        TextBoxState {
588            buffer,
589            group_id,
590            readonly: self.readonly,
591
592            viewctx: self.viewctx.clone(),
593            term_cursor: (0, 0),
594        }
595    }
596
597    fn close(&mut self, _: CloseFlags, _: &mut Store<I>) -> bool {
598        true
599    }
600
601    fn write(&mut self, _: Option<&str>, _: WriteFlags, _: &mut Store<I>) -> UIResult<EditInfo, I> {
602        if self.readonly {
603            return Err(EditError::ReadOnly.into());
604        } else {
605            return Ok(None);
606        }
607    }
608
609    fn draw(&mut self, area: Rect, buf: &mut Buffer, _: bool, _: &mut Store<I>) {
610        TextBox::new().render(area, buf, self);
611    }
612
613    fn get_completions(&self) -> Option<CompletionList> {
614        self.buffer.read().unwrap().get_completions(self.group_id)
615    }
616
617    fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
618        self.buffer.read().unwrap().get_cursor_word(self.group_id, style)
619    }
620
621    fn get_selected_word(&self) -> Option<String> {
622        self.buffer.read().unwrap().get_selected_word(self.group_id)
623    }
624}
625
626impl<'a, I> TextBox<'a, I>
627where
628    I: ApplicationInfo,
629{
630    /// Create a new widget.
631    pub fn new() -> Self {
632        TextBox {
633            block: None,
634            prompt: Span::default(),
635            oneline: false,
636            style: Style::default(),
637
638            lgutter_width: 0,
639            rgutter_width: 0,
640
641            _pc: PhantomData,
642        }
643    }
644
645    /// Set the style to use for rendering text within the [TextBoxState].
646    pub fn style(mut self, style: Style) -> Self {
647        self.style = style;
648        self
649    }
650
651    /// Wrap this text box in a [Block].
652    pub fn block(mut self, block: Block<'a>) -> Self {
653        self.block = Some(block);
654        self
655    }
656
657    /// Force the text to render on a single line. LF and CR will be rendered as ^J and ^M
658    /// respectively.
659    ///
660    /// Any gutters will not be shown.
661    pub fn oneline(mut self) -> Self {
662        self.oneline = true;
663        self
664    }
665
666    /// Display a prompt in the top left of the text box when focused.
667    pub fn prompt(mut self, prompt: impl Into<Span<'a>>) -> Self {
668        self.prompt = prompt.into();
669        self
670    }
671
672    /// Set the width of the left gutter.
673    pub fn left_gutter(mut self, lw: u16) -> Self {
674        self.lgutter_width = lw;
675        self
676    }
677
678    /// Set the width of the right gutter.
679    pub fn right_gutter(mut self, rw: u16) -> Self {
680        self.rgutter_width = rw;
681        self
682    }
683
684    #[inline]
685    fn _highlight_followers(
686        &self,
687        line: usize,
688        start: usize,
689        end: usize,
690        (x, y): (u16, u16),
691        followers: &FollowersInfo,
692        buf: &mut Buffer,
693    ) {
694        let hlstyled = self.style.add_modifier(Modifier::REVERSED);
695        let cs = (line, start);
696        let ce = (line, end);
697
698        for follower in followers.query(cs..ce) {
699            let fx = x + (follower.value.x - start) as u16;
700            let fa = Rect::new(fx, y, 1, 1);
701            buf.set_style(fa, hlstyled);
702        }
703    }
704
705    #[inline]
706    fn _set_style(&self, start: usize, h1: usize, h2: usize, (x, y): (u16, u16), buf: &mut Buffer) {
707        let tx: u16 = x + (h1 - start) as u16;
708        let selwidth: u16 = (h2 - h1 + 1).try_into().unwrap();
709
710        let hlstyled = self.style.add_modifier(Modifier::REVERSED);
711        let selarea = Rect::new(tx, y, selwidth, 1);
712
713        buf.set_style(selarea, hlstyled);
714    }
715
716    #[inline]
717    fn _highlight_line(
718        &self,
719        line: usize,
720        start: usize,
721        end: usize,
722        (x, y): (u16, u16),
723        hls: &HighlightInfo,
724        buf: &mut Buffer,
725    ) {
726        for selection in hls.query_point(line) {
727            let (sb, se, shape) = &selection.value;
728
729            let maxcol = end.saturating_sub(1);
730            let range = start..end;
731
732            match shape {
733                TargetShape::CharWise => {
734                    let x1 = if line == sb.y { sb.x.max(start) } else { start };
735                    let x2 = if line == se.y {
736                        se.x.min(maxcol)
737                    } else {
738                        maxcol
739                    };
740
741                    if range.contains(&x1) && range.contains(&x2) {
742                        self._set_style(start, x1, x2, (x, y), buf);
743                    }
744                },
745                TargetShape::LineWise => {
746                    let hlstyled = self.style.add_modifier(Modifier::REVERSED);
747                    let selwidth: u16 = (end - start).try_into().unwrap();
748                    let selarea = Rect::new(x, y, selwidth, 1);
749
750                    buf.set_style(selarea, hlstyled);
751                },
752                TargetShape::BlockWise => {
753                    let lx = sb.x.min(se.x);
754                    let rx = sb.x.max(se.x);
755
756                    let x1 = lx.max(start);
757                    let x2 = rx.min(maxcol);
758
759                    if range.contains(&x1) && range.contains(&x2) {
760                        self._set_style(start, x1, x2, (x, y), buf);
761                    }
762                },
763            }
764        }
765    }
766
767    fn _render_lines_wrap(
768        &mut self,
769        area: Rect,
770        gutters: (Rect, Rect),
771        buf: &mut Buffer,
772        hinfo: HighlightInfo,
773        finfo: FollowersInfo,
774        state: &mut TextBoxState<I>,
775    ) {
776        let bot = area.bottom();
777        let x = area.left();
778        let mut y = area.top();
779
780        let height = area.height as usize;
781        let width = area.width as usize;
782
783        /*
784         * If the cursor has moved off-screen, update the viewport corner.
785         *
786         * There might be several long wrapped lines between the new corner and the cursor
787         * afterwards, though, so we update the corner again after handling wrapping if needed.
788         */
789        let cursor = state.get_cursor();
790        shift_corner_wrap(&cursor, &mut state.viewctx.corner, height);
791
792        let cby = state.viewctx.corner.y;
793        let cbx = state.viewctx.corner.x;
794
795        let text = state.buffer.read().unwrap();
796
797        let mut wrapped = Vec::new();
798        let mut sawcursor = false;
799
800        for (loff, s) in text.lines_at(cby, cbx).enumerate() {
801            if wrapped.len() >= height && sawcursor {
802                break;
803            }
804
805            let base = if loff == 0 { cbx } else { 0 };
806            let line = cby + loff;
807            let mut first = true;
808            let mut char_offset = 0;
809            let s_charlen = s.len();
810
811            while char_offset < s_charlen && (wrapped.len() < height || !sawcursor) {
812                let start_char = char_offset;
813
814                let mut full = false;
815                let mut num_chars = 0;
816                let mut cur_width = 0;
817                for (i, c) in s.chars(CharOff::from(start_char)).enumerate() {
818                    let c_width = c.width_cjk().unwrap_or(1);
819                    if cur_width + c_width > width {
820                        full = true;
821                        break;
822                    }
823                    cur_width += c_width;
824                    num_chars = i + 1;
825                }
826
827                let end_char = start_char + num_chars;
828                let swrapped =
829                    s.slice(CharOff::from(start_char)..CharOff::from(end_char)).to_string();
830                char_offset = end_char;
831
832                let start = base + start_char;
833                let end = base + end_char;
834                let slen = base + s_charlen;
835
836                full |= cur_width == width;
837                let last = end == slen && cursor.x == slen;
838                let cursor_line = line == cursor.y && ((start..end).contains(&cursor.x) || last);
839
840                if cursor_line && full && last {
841                    wrapped.push((line, start, end, swrapped, false, first));
842                    wrapped.push((line, end, end, " ".to_string(), true, first));
843                } else {
844                    wrapped.push((line, start, end, swrapped, cursor_line, first));
845                }
846
847                sawcursor |= cursor_line;
848
849                first = false;
850            }
851
852            if s_charlen == 0 {
853                let cursor_line = line == cursor.y;
854                wrapped.push((line, base, base, s.to_string(), cursor_line, true));
855                sawcursor |= cursor_line;
856            }
857        }
858
859        if wrapped.len() > height {
860            let n = wrapped.len() - height;
861            let _ = wrapped.drain(..n);
862            let (line, start, _, _, _, _) = wrapped.first().unwrap();
863            state.viewctx.corner.set_y(*line);
864            state.viewctx.corner.set_x(*start);
865        }
866
867        for (line, start, end, s, cursor_line, first) in wrapped.into_iter() {
868            if y >= bot {
869                break;
870            }
871
872            if first {
873                let lgutter = text.get_line_info::<LeftGutterInfo>(line);
874                let rgutter = text.get_line_info::<RightGutterInfo>(line);
875
876                if let Some(lgi) = lgutter {
877                    let lga = Rect::new(gutters.0.x, y, gutters.0.width, 0);
878                    lgi.render(lga, buf);
879                }
880
881                if let Some(rgi) = rgutter {
882                    let rga = Rect::new(gutters.1.x, y, gutters.1.width, 0);
883                    rgi.render(rga, buf);
884                }
885            }
886
887            if cursor_line {
888                let coff = s[..s
889                    .char_indices()
890                    .map(|(i, _)| i)
891                    .nth(cursor.x.saturating_sub(start))
892                    .unwrap_or(s.len())]
893                    .width_cjk() as u16;
894
895                state.term_cursor = (x + coff, y);
896            }
897
898            let _ = buf.set_stringn(x, y, s, width, self.style);
899
900            self._highlight_followers(line, start, end, (x, y), &finfo, buf);
901            self._highlight_line(line, start, end, (x, y), &hinfo, buf);
902
903            y += 1;
904        }
905    }
906
907    fn _render_lines_oneline(
908        &mut self,
909        area: Rect,
910        buf: &mut Buffer,
911        hinfo: HighlightInfo,
912        finfo: FollowersInfo,
913        state: &mut TextBoxState<I>,
914    ) {
915        let right = area.right();
916        let mut x = area.left();
917        let y = area.top();
918
919        let width = area.width as usize;
920
921        // If the cursor has moved off-screen, update the viewport corner.
922        let cursor = state.get_cursor();
923        shift_corner_oneline(&cursor, &mut state.viewctx.corner);
924
925        let cby = state.viewctx.corner.y;
926        let cbx = state.viewctx.corner.x;
927
928        let text = state.buffer.read().unwrap();
929
930        let mut joined = Vec::new();
931        let mut sawcursor = false;
932        let mut len = 0;
933        let mut off = cbx;
934
935        for (loff, s) in text.lines_at(cby, cbx).enumerate() {
936            if len >= width && sawcursor {
937                break;
938            }
939
940            let base = if loff == 0 { cbx } else { 0 };
941            let line = cby + loff;
942            let slen = s.len();
943
944            while off < slen && (len <= width || !sawcursor) {
945                let start = off;
946                let end = (start + width).min(slen);
947                let swrapped = s.slice(CharOff::from(start)..CharOff::from(end));
948
949                off = end;
950
951                let start = base + start;
952                let end = base + end;
953                let slen = base + slen;
954
955                let full = end - start == width;
956                let last = end == slen && cursor.x == slen;
957                let cursor_line = line == cursor.y && ((start..end).contains(&cursor.x) || last);
958
959                let wlen = swrapped.len();
960
961                if cursor_line && full && last {
962                    joined.push((line, start, end, swrapped, wlen, false));
963                    joined.push((line, end, end, EditRope::from(" "), 1, true));
964                    len += wlen + 1;
965                } else {
966                    joined.push((line, start, end, swrapped, wlen, cursor_line));
967                    len += wlen;
968                }
969
970                sawcursor |= cursor_line;
971            }
972
973            if slen == 0 {
974                let cursor_line = line == cursor.y;
975                joined.push((line, 0, 0, s, 0, cursor_line));
976                sawcursor |= cursor_line;
977            }
978
979            joined.push((line, slen, slen, EditRope::from("^J"), 2, false));
980            len += 2;
981
982            // Reset for next iteration.
983            off = 0;
984        }
985
986        if !joined.is_empty() {
987            // Remove the last ^J.
988            joined.pop();
989            len -= 2;
990        }
991
992        if len > width {
993            let mut n = 0;
994
995            for (idx, (_, ref mut start, _, ref mut s, slen, ref cursor_line)) in
996                joined.iter_mut().enumerate()
997            {
998                if len <= width {
999                    break;
1000                }
1001
1002                let diff = len - width;
1003                n = idx;
1004
1005                if *cursor_line {
1006                    let into = cursor.x - *start;
1007                    let rm = diff.min(into);
1008                    *s = s.slice(CharOff::from(rm)..);
1009                    *start += rm;
1010                    break;
1011                } else if *slen > diff {
1012                    *s = s.slice(CharOff::from(diff)..);
1013                    *start += diff;
1014                    break;
1015                } else {
1016                    len -= *slen;
1017                    continue;
1018                }
1019            }
1020
1021            let _ = joined.drain(..n);
1022            let (line, start, _, _, _, _) = joined.first().unwrap();
1023            state.viewctx.corner.set_y(*line);
1024            state.viewctx.corner.set_x(*start);
1025        }
1026
1027        state.term_cursor = (x, y);
1028
1029        for (line, start, end, s, _, cursor_line) in joined.into_iter() {
1030            if x >= right {
1031                break;
1032            }
1033
1034            let s = s.to_string();
1035            let w = (right - x) as usize;
1036
1037            if cursor_line {
1038                let coff = s[..s
1039                    .char_indices()
1040                    .map(|(i, _)| i)
1041                    .nth(cursor.x.saturating_sub(start))
1042                    .unwrap_or(s.len())]
1043                    .width_cjk() as u16;
1044
1045                state.term_cursor = (x + coff, y);
1046            }
1047
1048            let (xres, _) = buf.set_stringn(x, y, s, w, self.style);
1049
1050            self._highlight_followers(line, start, end, (x, y), &finfo, buf);
1051            self._highlight_line(line, start, end, (x, y), &hinfo, buf);
1052
1053            x = xres;
1054        }
1055    }
1056
1057    fn _render_lines_nowrap(
1058        &mut self,
1059        area: Rect,
1060        gutters: (Rect, Rect),
1061        buf: &mut Buffer,
1062        hinfo: HighlightInfo,
1063        finfo: FollowersInfo,
1064        state: &mut TextBoxState<I>,
1065    ) {
1066        let bot = area.bottom();
1067        let x = area.left();
1068        let mut y = area.top();
1069
1070        let height = area.height as usize;
1071        let width = area.width as usize;
1072
1073        // If the cursor has moved off-screen, update the viewport corner.
1074        let cursor = state.get_cursor();
1075        shift_corner_nowrap(&cursor, &mut state.viewctx.corner, width, height);
1076
1077        let cby = state.viewctx.corner.y;
1078        let cbx = state.viewctx.corner.x;
1079
1080        let text = state.buffer.read().unwrap();
1081        let mut line = cby;
1082        let mut lines = text.lines(line);
1083
1084        while y < bot {
1085            if let Some(s) = lines.next() {
1086                let lgutter = text.get_line_info::<LeftGutterInfo>(line);
1087                let rgutter = text.get_line_info::<RightGutterInfo>(line);
1088
1089                let slen = s.len();
1090                let start = cbx;
1091                let end = slen;
1092
1093                if let Some(lgi) = lgutter {
1094                    let lga = Rect::new(gutters.0.x, y, gutters.0.width, 0);
1095                    lgi.render(lga, buf);
1096                }
1097
1098                let s = s.slice(CharOff::from(start)..CharOff::from(end)).to_string();
1099
1100                if line == cursor.y && (start..=end).contains(&cursor.x) {
1101                    let coff = s[..s
1102                        .char_indices()
1103                        .map(|(i, _)| i)
1104                        .nth(cursor.x.saturating_sub(start))
1105                        .unwrap_or(s.len())]
1106                        .width_cjk() as u16;
1107
1108                    state.term_cursor = (x + coff, y);
1109                }
1110
1111                if cbx < slen {
1112                    let _ = buf.set_stringn(x, y, s, width, self.style);
1113                }
1114
1115                if let Some(rgi) = rgutter {
1116                    let rga = Rect::new(gutters.1.x, y, gutters.1.width, 0);
1117                    rgi.render(rga, buf);
1118                }
1119
1120                self._highlight_followers(line, start, end, (x, y), &finfo, buf);
1121                self._highlight_line(line, start, end, (x, y), &hinfo, buf);
1122
1123                y += 1;
1124                line += 1;
1125            } else {
1126                break;
1127            }
1128        }
1129    }
1130
1131    #[inline]
1132    fn _selection_intervals(&self, state: &mut TextBoxState<I>) -> HighlightInfo {
1133        state.buffer.write().unwrap().selection_intervals(state.group_id)
1134    }
1135
1136    #[inline]
1137    fn _follower_intervals(&self, state: &mut TextBoxState<I>) -> FollowersInfo {
1138        state.buffer.write().unwrap().follower_intervals(state.group_id)
1139    }
1140
1141    fn _render_lines(&mut self, area: Rect, buf: &mut Buffer, state: &mut TextBoxState<I>) {
1142        let hinfo = self._selection_intervals(state);
1143        let finfo = self._follower_intervals(state);
1144
1145        if self.oneline {
1146            state.set_term_info(area);
1147            self._render_lines_oneline(area, buf, hinfo, finfo, state);
1148            return;
1149        }
1150
1151        let (lgw, rgw) = if area.width <= self.lgutter_width + self.rgutter_width {
1152            (0, 0)
1153        } else {
1154            (self.lgutter_width, self.rgutter_width)
1155        };
1156        let textw = area.width - lgw - rgw;
1157        let lga = Rect::new(area.x, area.y, lgw, area.height);
1158        let texta = Rect::new(area.x + lgw, area.y, textw, area.height);
1159        let rga = Rect::new(area.x + lgw + textw, area.y, rgw, area.height);
1160        let gutters = (lga, rga);
1161
1162        state.set_term_info(texta);
1163
1164        if state.viewctx.wrap {
1165            self._render_lines_wrap(texta, gutters, buf, hinfo, finfo, state);
1166        } else {
1167            self._render_lines_nowrap(texta, gutters, buf, hinfo, finfo, state);
1168        }
1169    }
1170}
1171
1172impl<I> Default for TextBox<'_, I>
1173where
1174    I: ApplicationInfo,
1175{
1176    fn default() -> Self {
1177        TextBox::new()
1178    }
1179}
1180
1181impl<I> StatefulWidget for TextBox<'_, I>
1182where
1183    I: ApplicationInfo,
1184{
1185    type State = TextBoxState<I>;
1186
1187    fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
1188        let area = match self.block.take() {
1189            Some(block) => {
1190                let inner_area = block.inner(area);
1191                block.render(area, buf);
1192                inner_area
1193            },
1194            None => area,
1195        };
1196
1197        let plen = self.prompt.width() as u16;
1198        let gutter = Rect::new(area.x, area.y, plen, area.height);
1199
1200        let text_area =
1201            Rect::new(area.x + plen, area.y, area.width.saturating_sub(plen), area.height);
1202
1203        if text_area.width == 0 || text_area.height == 0 {
1204            return;
1205        }
1206
1207        // First, draw the prompt in the gutter.
1208        let _ = buf.set_span(gutter.left(), gutter.top(), &self.prompt, gutter.width);
1209
1210        // Now draw the text.
1211        self._render_lines(text_area, buf, state);
1212    }
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217    use super::*;
1218    use modalkit::editing::store::Store;
1219    use modalkit::env::vim::VimState;
1220
1221    macro_rules! mv {
1222        ($mt: expr) => {
1223            EditTarget::Motion($mt, Count::Contextual)
1224        };
1225        ($mt: expr, $c: expr) => {
1226            EditTarget::Motion($mt, Count::Exact($c))
1227        };
1228    }
1229
1230    macro_rules! dirscroll {
1231        ($tbox: expr, $d: expr, $s: expr, $c: expr, $ctx: expr, $store: expr) => {
1232            $tbox
1233                .scroll(&ScrollStyle::Direction2D($d, $s, $c), $ctx, &mut $store)
1234                .unwrap()
1235        };
1236    }
1237
1238    macro_rules! cursorpos {
1239        ($tbox: expr, $pos: expr, $axis: expr, $ctx: expr, $store: expr) => {
1240            $tbox
1241                .scroll(&ScrollStyle::CursorPos($pos, $axis), $ctx, &mut $store)
1242                .unwrap()
1243        };
1244    }
1245
1246    macro_rules! linepos {
1247        ($tbox: expr, $pos: expr, $c: expr, $ctx: expr, $store: expr) => {
1248            $tbox.scroll(&ScrollStyle::LinePos($pos, $c), $ctx, &mut $store).unwrap()
1249        };
1250    }
1251
1252    fn mkbox() -> (TextBoxState, Store<EmptyInfo>) {
1253        let mut store = Store::default();
1254        let buffer = store.load_buffer("".to_string());
1255
1256        (TextBoxState::new(buffer), store)
1257    }
1258
1259    fn mkboxstr(s: &str) -> (TextBoxState, EditContext, Store<EmptyInfo>) {
1260        let (mut b, mut store) = mkbox();
1261        let ctx = EditContext::from(VimState::<EmptyInfo>::default());
1262
1263        b.set_text(s);
1264        b.editor_command(&HistoryAction::Checkpoint.into(), &ctx, &mut store)
1265            .unwrap();
1266
1267        return (b, ctx, store);
1268    }
1269
1270    #[test]
1271    fn test_scroll_dir1d() {
1272        let (mut tbox, ctx, mut store) = mkboxstr(
1273            "1234567890\n\
1274            abcdefghij\n\
1275            klmnopqrst\n\
1276            uvwxyz,.<>\n\
1277            -_=+[{]}\\|\n\
1278            !@#$%^&*()\n\
1279            1234567890\n",
1280        );
1281
1282        tbox.set_wrap(false);
1283        tbox.set_term_info(Rect::new(0, 0, 6, 4));
1284
1285        // Scroll by terminal cells
1286        dirscroll!(tbox, MoveDir2D::Down, ScrollSize::Cell, 4.into(), &ctx, store);
1287        assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
1288        assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
1289
1290        dirscroll!(tbox, MoveDir2D::Up, ScrollSize::Cell, 2.into(), &ctx, store);
1291        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
1292        assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
1293
1294        dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Cell, 6.into(), &ctx, store);
1295        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 6));
1296        assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
1297
1298        dirscroll!(tbox, MoveDir2D::Left, ScrollSize::Cell, 2.into(), &ctx, store);
1299        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
1300        assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
1301
1302        // Scroll by half page
1303        dirscroll!(tbox, MoveDir2D::Down, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
1304        assert_eq!(tbox.viewctx.corner, Cursor::new(4, 4));
1305        assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
1306
1307        dirscroll!(tbox, MoveDir2D::Up, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
1308        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
1309        assert_eq!(tbox.get_cursor(), Cursor::new(4, 6));
1310
1311        dirscroll!(tbox, MoveDir2D::Right, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
1312        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 7));
1313        assert_eq!(tbox.get_cursor(), Cursor::new(4, 7));
1314
1315        dirscroll!(tbox, MoveDir2D::Left, ScrollSize::HalfPage, Count::Contextual, &ctx, store);
1316        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
1317        assert_eq!(tbox.get_cursor(), Cursor::new(4, 7));
1318
1319        // Scroll by page
1320        dirscroll!(tbox, MoveDir2D::Down, ScrollSize::Page, Count::Contextual, &ctx, store);
1321        assert_eq!(tbox.viewctx.corner, Cursor::new(6, 4));
1322        assert_eq!(tbox.get_cursor(), Cursor::new(6, 7));
1323
1324        dirscroll!(tbox, MoveDir2D::Up, ScrollSize::Page, Count::Contextual, &ctx, store);
1325        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 4));
1326        assert_eq!(tbox.get_cursor(), Cursor::new(5, 7));
1327
1328        dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
1329        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
1330        assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
1331
1332        dirscroll!(tbox, MoveDir2D::Left, ScrollSize::Page, Count::Contextual, &ctx, store);
1333        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 3));
1334        assert_eq!(tbox.get_cursor(), Cursor::new(5, 8));
1335
1336        // Cannot scroll cursor and viewport past the end of the line.
1337        dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
1338        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
1339        assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
1340
1341        dirscroll!(tbox, MoveDir2D::Right, ScrollSize::Page, Count::Contextual, &ctx, store);
1342        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 9));
1343        assert_eq!(tbox.get_cursor(), Cursor::new(5, 9));
1344    }
1345
1346    #[test]
1347    fn test_scroll_cursorpos() {
1348        let (mut tbox, ctx, mut store) = mkboxstr(
1349            "1234567890\n\
1350            abcdefghij\n\
1351            klmnopqrst\n\
1352            uvwxyz,.<>\n\
1353            -_=+[{]}\\|\n\
1354            !@#$%^&*()\n\
1355            1234567890\n",
1356        );
1357
1358        tbox.set_wrap(false);
1359        tbox.set_term_info(Rect::new(0, 0, 4, 4));
1360
1361        // When the cursor is at the top-left corner, these actions are effectively no-ops.
1362        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1363        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1364
1365        cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
1366        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1367        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1368
1369        cursorpos!(tbox, MovePosition::Middle, Axis::Vertical, &ctx, store);
1370        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1371        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1372
1373        cursorpos!(tbox, MovePosition::End, Axis::Vertical, &ctx, store);
1374        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1375        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1376
1377        cursorpos!(tbox, MovePosition::Beginning, Axis::Horizontal, &ctx, store);
1378        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1379        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1380
1381        cursorpos!(tbox, MovePosition::Middle, Axis::Horizontal, &ctx, store);
1382        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1383        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1384
1385        cursorpos!(tbox, MovePosition::End, Axis::Horizontal, &ctx, store);
1386        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1387        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1388
1389        // Move the cursor to the second column of the fifth line, and vertically position cursor.
1390        let mov = mv!(MoveType::BufferLineOffset, 5);
1391        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1392        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1393        assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
1394
1395        let mov = mv!(MoveType::LineColumnOffset, 2);
1396        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1397        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1398        assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
1399
1400        cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
1401        assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
1402        assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
1403
1404        cursorpos!(tbox, MovePosition::End, Axis::Vertical, &ctx, store);
1405        assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
1406        assert_eq!(tbox.viewctx.corner, Cursor::new(1, 0));
1407
1408        cursorpos!(tbox, MovePosition::Middle, Axis::Vertical, &ctx, store);
1409        assert_eq!(tbox.get_cursor(), Cursor::new(4, 1));
1410        assert_eq!(tbox.viewctx.corner, Cursor::new(3, 0));
1411
1412        // Move the cursor to the fifth column, and horizontally position cursor.
1413        let mov = mv!(MoveType::LineColumnOffset, 5);
1414        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1415        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1416        assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
1417
1418        cursorpos!(tbox, MovePosition::Beginning, Axis::Horizontal, &ctx, store);
1419        assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
1420        assert_eq!(tbox.viewctx.corner, Cursor::new(3, 4));
1421
1422        cursorpos!(tbox, MovePosition::End, Axis::Horizontal, &ctx, store);
1423        assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
1424        assert_eq!(tbox.viewctx.corner, Cursor::new(3, 1));
1425
1426        cursorpos!(tbox, MovePosition::Middle, Axis::Horizontal, &ctx, store);
1427        assert_eq!(tbox.get_cursor(), Cursor::new(4, 4));
1428        assert_eq!(tbox.viewctx.corner, Cursor::new(3, 3));
1429
1430        // Vertically positioning the cursor after a FirstWord.
1431        let mov = MoveType::FirstWord(MoveDir1D::Next);
1432        let act = EditorAction::Edit(EditAction::Motion.into(), mv!(mov, 0));
1433        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1434        cursorpos!(tbox, MovePosition::Beginning, Axis::Vertical, &ctx, store);
1435        assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
1436        assert_eq!(tbox.viewctx.corner, Cursor::new(4, 0));
1437    }
1438
1439    #[test]
1440    fn test_scroll_linepos() {
1441        let (mut tbox, ctx, mut store) = mkboxstr(
1442            "1234567890\n\
1443            abcdefghij\n\
1444            klmnopqrst\n\
1445            uvwxyz,.<>\n\
1446            -_=+[{]}\\|\n\
1447            !@#$%^&*()\n\
1448            1234567890\n",
1449        );
1450
1451        tbox.set_wrap(false);
1452        tbox.set_term_info(Rect::new(0, 0, 4, 4));
1453
1454        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1455        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1456
1457        // Scroll so that the 3rd line at the top of the screen.
1458        linepos!(tbox, MovePosition::Beginning, Count::Exact(3), &ctx, store);
1459        assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
1460        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
1461
1462        // Scroll so that the 7th line is in the middle of the screen.
1463        linepos!(tbox, MovePosition::Middle, Count::Exact(7), &ctx, store);
1464        assert_eq!(tbox.get_cursor(), Cursor::new(6, 0));
1465        assert_eq!(tbox.viewctx.corner, Cursor::new(5, 0));
1466
1467        // The 1st line cannot be in the middle of the screen.
1468        linepos!(tbox, MovePosition::Middle, Count::Exact(1), &ctx, store);
1469        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1470        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1471
1472        // The 1st line cannot be at the bottom of the screen.
1473        linepos!(tbox, MovePosition::End, Count::Exact(1), &ctx, store);
1474        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1475        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1476
1477        // The 2nd line cannot be at the bottom of the screen.
1478        linepos!(tbox, MovePosition::End, Count::Exact(2), &ctx, store);
1479        assert_eq!(tbox.get_cursor(), Cursor::new(1, 0));
1480        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1481
1482        // The 3nd line cannot be at the bottom of the screen.
1483        linepos!(tbox, MovePosition::End, Count::Exact(3), &ctx, store);
1484        assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
1485        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1486
1487        // The 4th line can be at the bottom of the screen.
1488        linepos!(tbox, MovePosition::End, Count::Exact(4), &ctx, store);
1489        assert_eq!(tbox.get_cursor(), Cursor::new(3, 0));
1490        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1491
1492        // The 5th line can be at the bottom of the screen.
1493        linepos!(tbox, MovePosition::End, Count::Exact(5), &ctx, store);
1494        assert_eq!(tbox.get_cursor(), Cursor::new(4, 0));
1495        assert_eq!(tbox.viewctx.corner, Cursor::new(1, 0));
1496    }
1497
1498    #[test]
1499    fn test_reset_text() {
1500        let (mut tbox, ctx, mut store) = mkboxstr("foo\nbar\nbaz");
1501
1502        let mov = mv!(MoveType::BufferLineOffset, 3);
1503        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1504        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1505
1506        assert_eq!(tbox.get_text(), "foo\nbar\nbaz\n");
1507        assert_eq!(tbox.get_cursor(), Cursor::new(2, 0));
1508
1509        assert_eq!(tbox.reset_text(), "foo\nbar\nbaz\n");
1510        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1511
1512        assert_eq!(tbox.get_text(), "\n");
1513        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1514    }
1515
1516    #[test]
1517    fn test_render_nowrap() {
1518        let (mut tbox, ctx, mut store) = mkboxstr("foo\nbar\nbaz\nquux 1 2 3 4 5");
1519
1520        tbox.set_wrap(false);
1521
1522        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
1523        let area = Rect::new(0, 8, 10, 2);
1524
1525        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1526
1527        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1528        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1529        assert_eq!(tbox.get_term_cursor(), (2, 8).into());
1530
1531        // Move the cursor to the fourth line, thereby moving corner.
1532        let mov = mv!(MoveType::BufferLineOffset, 4);
1533        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1534        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1535
1536        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1537
1538        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 0));
1539        assert_eq!(tbox.get_cursor(), Cursor::new(3, 0));
1540        assert_eq!(tbox.get_term_cursor(), (2, 9).into());
1541
1542        // Move the cursor to the end of the fourth line, again moving corner.
1543        let mov = mv!(MoveType::LineColumnOffset, 14);
1544        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1545        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1546
1547        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1548
1549        assert_eq!(tbox.viewctx.corner, Cursor::new(2, 6));
1550        assert_eq!(tbox.get_cursor(), Cursor::new(3, 13));
1551        assert_eq!(tbox.get_term_cursor(), (9, 9).into());
1552
1553        // Now move back to the top-left corner.
1554        let mov = mv!(MoveType::BufferByteOffset, 0);
1555        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1556        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1557
1558        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1559
1560        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1561        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1562        assert_eq!(tbox.get_term_cursor(), (2, 8).into());
1563    }
1564
1565    #[test]
1566    fn test_wide_char_cursor() {
1567        let (mut tbox, ctx, mut store) = mkboxstr("세계를 향한 대화\n");
1568
1569        let area = Rect::new(0, 0, 20, 20);
1570        let mut buffer = Buffer::empty(area);
1571
1572        // Prompt should push everything right by 2 characters.
1573        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1574
1575        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1576        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1577        assert_eq!(tbox.get_term_cursor(), (2, 0).into());
1578
1579        // Move the cursor to be over "대", just before the last character.
1580        let mov = mv!(MoveType::Column(MoveDir1D::Next, false), 7);
1581        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1582        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1583
1584        // Draw again to update our terminal cursor using oneline().
1585        TextBox::new().prompt("> ").oneline().render(area, &mut buffer, &mut tbox);
1586
1587        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1588        assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
1589        assert_eq!(tbox.get_term_cursor(), (14, 0).into());
1590        // Draw again to update our terminal cursor using set_wrap(true).
1591        tbox.set_wrap(true);
1592        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1593
1594        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1595        assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
1596        assert_eq!(tbox.get_term_cursor(), (14, 0).into());
1597
1598        // Draw again to update our terminal cursor using set_wrap(false).
1599        tbox.set_wrap(true);
1600        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1601
1602        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1603        assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
1604        assert_eq!(tbox.get_term_cursor(), (14, 0).into());
1605    }
1606
1607    #[test]
1608    fn test_wide_char_wrap() {
1609        let (mut tbox, ctx, mut store) = mkboxstr("세계를 향한대화\n");
1610
1611        let area = Rect::new(0, 0, 10, 10);
1612        let mut buffer = Buffer::empty(area);
1613
1614        let mut expected = buffer.clone();
1615        expected.set_string(0, 0, "> 세계를 ", Style::new());
1616        expected.set_string(0, 1, "  향한대화", Style::new());
1617
1618        // Prompt should push everything right by 2 characters.
1619        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1620
1621        assert_eq!(buffer, expected);
1622        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1623        assert_eq!(tbox.get_cursor(), Cursor::new(0, 0));
1624        assert_eq!(tbox.get_term_cursor(), (2, 0).into());
1625        assert_eq!(tbox.has_lines(5), 3);
1626
1627        // Move the cursor to be on the " " on the end of the first wrapped line.
1628        let mov = mv!(MoveType::Column(MoveDir1D::Next, false), 3);
1629        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1630        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1631
1632        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1633
1634        assert_eq!(buffer, expected);
1635        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1636        assert_eq!(tbox.get_cursor(), Cursor::new(0, 3));
1637        assert_eq!(tbox.get_term_cursor(), (8, 0).into());
1638
1639        // Move the cursor right to be over "향".
1640        let mov = mv!(MoveType::Column(MoveDir1D::Next, false), 1);
1641        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1642        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1643
1644        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1645
1646        assert_eq!(buffer, expected);
1647        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1648        assert_eq!(tbox.get_cursor(), Cursor::new(0, 4));
1649        assert_eq!(tbox.get_term_cursor(), (2, 1).into());
1650
1651        // Move the cursor right to be at the end.
1652        let mov = mv!(MoveType::Column(MoveDir1D::Next, false), 3);
1653        let act = EditorAction::Edit(EditAction::Motion.into(), mov);
1654        tbox.editor_command(&act, &ctx, &mut store).unwrap();
1655
1656        TextBox::new().prompt("> ").render(area, &mut buffer, &mut tbox);
1657
1658        assert_eq!(buffer, expected);
1659        assert_eq!(tbox.viewctx.corner, Cursor::new(0, 0));
1660        assert_eq!(tbox.get_cursor(), Cursor::new(0, 7));
1661        assert_eq!(tbox.get_term_cursor(), (8, 1).into());
1662    }
1663}