yeehaw_tui/elements/widgets/
textbox.rs

1use {
2    crate::{
3        elements::menu::{MenuItem, MenuPath, MenuStyle},
4        Keyboard as KB, *,
5    },
6    crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEventKind},
7};
8
9// TODO cache WrChs for efficiency. Also get_wrapped should return a Ref<WrChs>
10
11// TODO better multiline cursor movement
12// retain greater cursor position between lines, ex:
13//    123456789<cursor, starting position>
14//    1234<cursor after moving down>
15//    123456789<cursor, after moving down again>
16
17#[derive(Clone)]
18pub struct TextBox {
19    pub pane: SelectablePane,
20    pub inner: Rc<RefCell<TextBoxInner>>,
21    pub x_scrollbar: Rc<RefCell<Option<HorizontalScrollbar>>>,
22    pub y_scrollbar: Rc<RefCell<Option<VerticalScrollbar>>>,
23    pub line_number_tb: Rc<RefCell<Option<TextBoxInner>>>,
24}
25
26#[yeehaw_derive::impl_pane_basics_from(pane)]
27impl TextBox {
28    const KIND: &'static str = "textbox";
29    pub fn new<S: Into<String>>(init_ctx: &Context, text: S) -> Self {
30        let text = text.into();
31        let s = Size::get_text_size(&text);
32        let pane = SelectablePane::new(init_ctx, Self::KIND)
33            .with_dyn_width(DynVal::new_fixed(s.width as i32))
34            .with_dyn_height(DynVal::new_fixed(s.height as i32))
35            .with_styles(TextBoxInner::STYLE);
36        let inner = TextBoxInner::new(init_ctx, text);
37
38        pane.pane.add_element(Box::new(inner.clone()));
39        let tb = TextBox {
40            pane,
41            inner: Rc::new(RefCell::new(inner)),
42            x_scrollbar: Rc::new(RefCell::new(None)),
43            y_scrollbar: Rc::new(RefCell::new(None)),
44            line_number_tb: Rc::new(RefCell::new(None)),
45        };
46
47        *tb.inner.borrow().current_sty.borrow_mut() = tb.pane.get_current_style();
48
49        let tb_ = tb.clone();
50
51        tb.pane
52            .set_post_hook_for_set_selectability(Box::new(move |_, _| {
53                tb_.post_hook_for_set_selectability();
54            }));
55
56        tb
57    }
58
59    pub fn post_hook_for_set_selectability(&self) {
60        let sel = self.pane.get_selectability();
61        *self.inner.borrow().selectedness.borrow_mut() = sel;
62        *self.inner.borrow().current_sty.borrow_mut() = self.pane.get_current_style();
63        *self.inner.borrow().is_dirty.borrow_mut() = true;
64        if sel != Selectability::Selected {
65            *self.inner.borrow().visual_mode.borrow_mut() = false;
66        }
67    }
68
69    pub fn set_dirty(&self) {
70        *self.inner.borrow().is_dirty.borrow_mut() = true;
71    }
72
73    pub fn with_scrollbars(self, init_ctx: &Context) -> Self {
74        self.set_x_scrollbar_inner(init_ctx, HorizontalSBPositions::Below);
75        self.set_y_scrollbar_inner(init_ctx, VerticalSBPositions::ToTheRight);
76        self
77    }
78
79    pub fn with_left_scrollbar(self, init_ctx: &Context) -> Self {
80        self.set_y_scrollbar_inner(init_ctx, VerticalSBPositions::ToTheLeft);
81        self
82    }
83
84    pub fn with_right_scrollbar(self, init_ctx: &Context) -> Self {
85        self.set_y_scrollbar_inner(init_ctx, VerticalSBPositions::ToTheRight);
86        self
87    }
88
89    fn set_y_scrollbar_inner(&self, init_ctx: &Context, pos: VerticalSBPositions) {
90        let content_height = self.inner.borrow().pane.content_height();
91        let content_size = self.inner.borrow().pane.content_size();
92
93        // accounts for the other scrollbar
94        let inner_start_y = self.inner.borrow().pane.get_dyn_start_y();
95
96        let sb = VerticalScrollbar::new(init_ctx, DynVal::FULL, content_size, content_height)
97            .without_keyboard_events();
98        if self.x_scrollbar.borrow().is_some() {
99            sb.pane.set_end_y(DynVal::FULL.minus_fixed(1));
100        }
101        match pos {
102            VerticalSBPositions::ToTheLeft => {
103                sb.set_at(0.into(), inner_start_y);
104                if let Some(x_sb) = &*self.x_scrollbar.borrow() {
105                    x_sb.pane.set_start_x(1);
106                }
107                self.inner.borrow().pane.set_start_x(1);
108            }
109            VerticalSBPositions::ToTheRight => {
110                sb.set_at(DynVal::FULL.minus_fixed(1), inner_start_y);
111                self.inner
112                    .borrow()
113                    .pane
114                    .set_end_x(DynVal::FULL.minus_fixed(1));
115                if let Some(x_sb) = &*self.x_scrollbar.borrow() {
116                    x_sb.pane.set_end_x(DynVal::FULL.minus_fixed(1));
117                }
118            }
119            VerticalSBPositions::None => {
120                return;
121            }
122        }
123        let size = Size::default();
124        sb.set_scrollable_view_size(size);
125        if let Some(x_sb) = &*self.x_scrollbar.borrow() {
126            x_sb.set_scrollable_view_size(size);
127        }
128
129        // wire the scrollbar to the textbox
130        let pane_ = self.inner.borrow().pane.clone();
131        let is_dirty = self.inner.borrow().is_dirty.clone();
132        let hook = Box::new(move |_, y| {
133            pane_.set_content_y_offset(None, y);
134            is_dirty.replace(true);
135        });
136        *sb.position_changed_hook.borrow_mut() = Some(hook);
137        *self.y_scrollbar.borrow_mut() = Some(sb.clone());
138        self.pane.pane.add_element(Box::new(sb.clone()));
139        self.inner.borrow().y_scrollbar.replace(Some(sb));
140
141        if let VerticalSBPositions::ToTheLeft = pos {
142            self.reset_line_numbers(init_ctx);
143        }
144        self.set_corner_decor(init_ctx);
145        self.reset_sb_sizes(init_ctx);
146    }
147
148    pub fn with_top_scrollbar(self, init_ctx: &Context) -> Self {
149        self.set_x_scrollbar_inner(init_ctx, HorizontalSBPositions::Above);
150        self
151    }
152
153    pub fn with_bottom_scrollbar(self, init_ctx: &Context) -> Self {
154        self.set_x_scrollbar_inner(init_ctx, HorizontalSBPositions::Below);
155        self
156    }
157
158    fn set_x_scrollbar_inner(&self, init_ctx: &Context, pos: HorizontalSBPositions) {
159        let content_width = self.inner.borrow().pane.content_width();
160        let content_size = self.inner.borrow().pane.content_size();
161
162        // accounts for the other scrollbar
163        let inner_start_x = if let Some(ln_tb) = &*self.inner.borrow().line_number_tb.borrow() {
164            ln_tb.pane.get_dyn_start_x()
165        } else {
166            self.inner.borrow().pane.get_dyn_start_x()
167        };
168
169        let sb = HorizontalScrollbar::new(init_ctx, DynVal::FULL, content_size, content_width)
170            .without_keyboard_events();
171        if self.x_scrollbar.borrow().is_some() {
172            sb.pane.set_end_y(DynVal::FULL.minus_fixed(1));
173        }
174        match pos {
175            HorizontalSBPositions::Above => {
176                sb.set_at(inner_start_x, 0.into());
177                self.inner.borrow().pane.set_start_y(1);
178                if let Some(ln_tb) = &*self.inner.borrow().line_number_tb.borrow() {
179                    ln_tb.pane.set_start_y(1);
180                }
181                if let Some(y_sb) = &*self.y_scrollbar.borrow() {
182                    y_sb.pane.set_start_y(1);
183                }
184            }
185            HorizontalSBPositions::Below => {
186                sb.set_at(inner_start_x, DynVal::FULL.minus_fixed(1));
187                self.inner
188                    .borrow()
189                    .pane
190                    .set_end_y(DynVal::FULL.minus_fixed(1));
191                if let Some(ln_tb) = &*self.inner.borrow().line_number_tb.borrow() {
192                    ln_tb.pane.set_end_y(DynVal::FULL.minus_fixed(1));
193                }
194                if let Some(y_sb) = &*self.y_scrollbar.borrow() {
195                    y_sb.pane.set_end_y(DynVal::FULL.minus_fixed(1));
196                }
197            }
198            HorizontalSBPositions::None => {
199                return;
200            }
201        }
202
203        // wire the scrollbar to the textbox
204        let pane_ = self.inner.borrow().pane.clone();
205        let is_dirty = self.inner.borrow().is_dirty.clone();
206        let hook = Box::new(move |_, x| {
207            pane_.set_content_x_offset(None, x);
208            is_dirty.replace(true);
209        });
210        *sb.position_changed_hook.borrow_mut() = Some(hook);
211        *self.x_scrollbar.borrow_mut() = Some(sb.clone());
212        self.pane.pane.add_element(Box::new(sb.clone()));
213        self.inner.borrow().x_scrollbar.replace(Some(sb));
214        self.set_corner_decor(init_ctx);
215        self.reset_sb_sizes(init_ctx);
216    }
217
218    pub fn reset_sb_sizes(&self, _init_ctx: &Context) {
219        let size = Size::default();
220        if let Some(y_sb) = &*self.y_scrollbar.borrow() {
221            y_sb.set_scrollable_view_size(size);
222            *y_sb.scrollable_view_chs.borrow_mut() = DynVal::new_fixed(size.height as i32);
223        }
224        if let Some(x_sb) = &*self.x_scrollbar.borrow() {
225            x_sb.set_scrollable_view_size(size);
226            *x_sb.scrollable_view_chs.borrow_mut() = DynVal::new_fixed(size.width as i32);
227        }
228        self.set_dirty();
229    }
230
231    pub fn with_line_numbers(self, init_ctx: &Context) -> Self {
232        self.set_line_numbers(init_ctx);
233        self
234    }
235
236    pub fn reset_line_numbers(&self, init_ctx: &Context) {
237        let ln_id = self
238            .inner
239            .borrow()
240            .line_number_tb
241            .borrow()
242            .as_ref()
243            .map(|ln_tb| ln_tb.pane.id());
244        if let Some(ln_id) = ln_id {
245            self.pane.pane.remove_element(&ln_id);
246            self.set_line_numbers(init_ctx);
247        }
248    }
249
250    pub fn set_line_numbers(&self, init_ctx: &Context) {
251        let start_x = self.inner.borrow().pane.get_dyn_start_x();
252        let start_y = self.inner.borrow().pane.get_dyn_start_y();
253        let end_y = self.inner.borrow().pane.get_dyn_end_y();
254
255        // determine the width of the line numbers textbox
256        let (lns, lnw) = self.inner.borrow().get_line_numbers(None);
257
258        // create the line numbers textbox
259        let ln_tb = TextBoxInner::new(init_ctx, lns)
260            .at(start_x.clone(), start_y)
261            .with_dyn_width(DynVal::new_fixed(lnw as i32))
262            .with_no_wordwrap()
263            .non_editable(init_ctx)
264            .with_no_ch_cursor()
265            .non_navigable();
266
267        ln_tb.pane.set_end_y(end_y);
268
269        //*ln_tb.current_sty.borrow_mut() = self.pane.get_current_style();
270        *ln_tb.current_sty.borrow_mut() = TextBoxInner::LINE_NUMBERS_STYLE;
271
272        *ln_tb.selectedness.borrow_mut() = Selectability::Unselectable;
273        self.pane.pane.add_element(Box::new(ln_tb.clone()));
274
275        let new_inner_start_x = start_x.plus_fixed(lnw as i32);
276
277        // reduce the width of the main textbox
278        self.inner.borrow().pane.set_start_x(new_inner_start_x);
279        *self.inner.borrow().line_number_tb.borrow_mut() = Some(ln_tb.clone());
280        self.reset_sb_sizes(init_ctx);
281    }
282
283    pub fn set_corner_decor(&self, init_ctx: &Context) {
284        // add corner decor
285        if let (Some(x_sb), Some(y_sb)) = (&*self.x_scrollbar.borrow(), &*self.y_scrollbar.borrow())
286        {
287            let corner_decor = self.inner.borrow().corner_decor.borrow().clone();
288            let cd = Label::new(init_ctx, &(corner_decor.ch.to_string()))
289                .with_style(corner_decor.style.clone());
290
291            let cd_y = x_sb.pane.get_dyn_start_y();
292            let cd_x = y_sb.pane.get_dyn_start_x();
293            let cd = cd.at(cd_x, cd_y);
294            self.pane.pane.add_element(Box::new(cd));
295        }
296    }
297
298    // TODO create this function eventually
299    // not that important and annoying to calculate if the tb has a line numbers element
300    //pub fn with_no_line_numbers(self) -> Self {
301    //    *self.inner.borrow().line_numbered.borrow_mut() = false;
302    //    self
303    //}
304
305    pub fn with_right_click_menu(self, rcm: Option<RightClickMenu>) -> Self {
306        *self.inner.borrow().right_click_menu.borrow_mut() = rcm;
307        self
308    }
309
310    pub fn with_text_when_empty<S: Into<String>>(self, text: S) -> Self {
311        *self.inner.borrow().text_when_empty.borrow_mut() = text.into();
312        self.set_dirty();
313        self
314    }
315
316    pub fn set_text_when_empty(&self, text: String) {
317        *self.inner.borrow().text_when_empty.borrow_mut() = text;
318        self.set_dirty();
319    }
320
321    pub fn with_text_when_empty_fg(self, fg: Color) -> Self {
322        *self.inner.borrow().text_when_empty_fg.borrow_mut() = fg;
323        self.set_dirty();
324        self
325    }
326
327    pub fn with_styles(self, styles: SelStyles) -> Self {
328        let curr_sty = self.pane.get_current_style();
329        if let Some(ln_tb) = &*self.inner.borrow().line_number_tb.borrow() {
330            *ln_tb.current_sty.borrow_mut() = curr_sty;
331        }
332        *self.inner.borrow().current_sty.borrow_mut() = self.pane.get_current_style();
333        self.pane.set_styles(styles);
334        self.set_dirty();
335        self
336    }
337
338    pub fn with_dyn_width(self, width: DynVal) -> Self {
339        self.pane.set_dyn_width(width);
340        self.set_dirty();
341        self
342    }
343
344    pub fn with_dyn_height(self, height: DynVal) -> Self {
345        self.pane.set_dyn_height(height);
346        self.set_dirty();
347        self
348    }
349
350    pub fn at<D: Into<DynVal>, D2: Into<DynVal>>(self, loc_x: D, loc_y: D2) -> Self {
351        self.pane.set_at(loc_x.into(), loc_y.into());
352        self.set_dirty();
353        self
354    }
355
356    pub fn set_at(&self, loc_x: DynVal, loc_y: DynVal) {
357        self.pane.set_at(loc_x, loc_y);
358        self.set_dirty();
359    }
360
361    pub fn with_ch_cursor(self) -> Self {
362        *self.inner.borrow().ch_cursor.borrow_mut() = true;
363        self.set_dirty();
364        self
365    }
366
367    pub fn with_no_ch_cursor(self) -> Self {
368        *self.inner.borrow().ch_cursor.borrow_mut() = false;
369        self.set_dirty();
370        self
371    }
372
373    pub fn editable(self, init_ctx: &Context) -> Self {
374        self.inner.borrow().set_editable(init_ctx);
375        self.reset_sb_sizes(init_ctx);
376        self
377    }
378
379    pub fn non_editable(self, init_ctx: &Context) -> Self {
380        self.inner.borrow().set_non_editable(init_ctx);
381        self.reset_sb_sizes(init_ctx);
382        self
383    }
384
385    pub fn with_wordwrap(self, init_ctx: &Context) -> Self {
386        *self.inner.borrow().wordwrap.borrow_mut() = true;
387        self.reset_sb_sizes(init_ctx);
388        self
389    }
390
391    pub fn with_no_wordwrap(self, init_ctx: &Context) -> Self {
392        *self.inner.borrow().wordwrap.borrow_mut() = false;
393        self.reset_sb_sizes(init_ctx);
394        self
395    }
396
397    pub fn with_position_style_hook(
398        self, hook: Box<dyn FnMut(Context, usize, Style) -> Style>,
399    ) -> Self {
400        *self.inner.borrow().position_style_hook.borrow_mut() = Some(hook);
401        self.set_dirty();
402        self
403    }
404
405    pub fn set_position_style_hook(
406        &mut self, hook: Box<dyn FnMut(Context, usize, Style) -> Style>,
407    ) {
408        *self.inner.borrow().position_style_hook.borrow_mut() = Some(hook);
409    }
410
411    pub fn with_cursor_changed_hook(self, hook: CursorChangedHook) -> Self {
412        *self.inner.borrow().cursor_changed_hook.borrow_mut() = Some(hook);
413        self
414    }
415
416    pub fn set_cursor_changed_hook(&mut self, hook: CursorChangedHook) {
417        *self.inner.borrow().cursor_changed_hook.borrow_mut() = Some(hook);
418    }
419
420    pub fn with_text_changed_hook(
421        self, hook: Box<dyn FnMut(Context, String) -> EventResponses>,
422    ) -> Self {
423        *self.inner.borrow().text_changed_hook.borrow_mut() = Some(hook);
424        self
425    }
426
427    pub fn set_text_changed_hook(
428        &mut self, hook: Box<dyn FnMut(Context, String) -> EventResponses>,
429    ) {
430        *self.inner.borrow().text_changed_hook.borrow_mut() = Some(hook);
431    }
432
433    pub fn with_cursor_style(self, style: Style) -> Self {
434        *self.inner.borrow().cursor_style.borrow_mut() = style;
435        self.set_dirty();
436        self
437    }
438
439    pub fn with_corner_decor(self, decor: DrawCh) -> Self {
440        *self.inner.borrow().corner_decor.borrow_mut() = decor;
441        self.set_dirty();
442        self
443    }
444
445    pub fn get_text(&self) -> String {
446        self.inner.borrow().get_text()
447    }
448
449    pub fn set_text(&self, text: String) {
450        self.inner.borrow().set_text(text);
451        self.set_dirty();
452    }
453
454    pub fn set_cursor_pos(&self, pos: usize) {
455        self.inner.borrow().set_cursor_pos(pos);
456        self.set_dirty();
457    }
458}
459
460#[yeehaw_derive::impl_element_from(pane)]
461impl Element for TextBox {
462    fn receive_event(&self, ctx: &Context, ev: Event) -> (bool, EventResponses) {
463        if self.pane.get_selectability() == Selectability::Unselectable {
464            return (false, EventResponses::default());
465        }
466        self.pane.receive_event(ctx, ev)
467    }
468}
469
470#[allow(clippy::type_complexity)]
471#[derive(Clone)]
472pub struct TextBoxInner {
473    pub pane: Pane,
474    pub current_sty: Rc<RefCell<Style>>,
475    pub selectedness: Rc<RefCell<Selectability>>,
476
477    /// whether or not the content is dirty and needs updating
478    pub is_dirty: Rc<RefCell<bool>>,
479
480    /// used for redrawing logic
481    pub last_size: Rc<RefCell<Size>>,
482
483    /// the last size given to the sbs
484    pub last_size_for_sbs: Rc<RefCell<Size>>,
485
486    pub text: Rc<RefCell<Vec<char>>>,
487    /// greyed out text when the textbox is empty
488    pub text_when_empty: Rc<RefCell<String>>,
489    pub text_when_empty_fg: Rc<RefCell<Color>>,
490
491    /// whether or not this tb has a ch cursor
492    pub ch_cursor: Rc<RefCell<bool>>,
493    /// whether or not this tb can be edited
494    pub editable: Rc<RefCell<bool>>,
495    /// whether or not there are lefthand line numbers
496    pub wordwrap: Rc<RefCell<bool>>,
497    /// cursor absolute position in the text
498    pub cursor_pos: Rc<RefCell<usize>>,
499    /// whether or not the cursor is visual selecting
500    pub cursor_style: Rc<RefCell<Style>>,
501    /// if the mouse is currently dragging
502    pub visual_mode: Rc<RefCell<bool>>,
503    /// the start position of the visual select
504    pub mouse_dragging: Rc<RefCell<bool>>,
505    pub visual_mode_start_pos: Rc<RefCell<usize>>,
506
507    /// this hook is called each time the text changes (for each letter)
508    pub text_changed_hook: Rc<RefCell<Option<Box<dyn FnMut(Context, String) -> EventResponses>>>>,
509
510    /// When this hook is non-nil each characters style will be determineda via this hook.
511    /// This is intended to be used if the caller of the textbox wishes granular control
512    /// over the text styling.
513    ///                                                              abs_pos, existing
514    pub position_style_hook: Rc<RefCell<Option<Box<dyn FnMut(Context, usize, Style) -> Style>>>>,
515
516    /// this hook is called each time the cursor moves
517    pub cursor_changed_hook: Rc<RefCell<Option<CursorChangedHook>>>,
518
519    pub x_scrollbar: Rc<RefCell<Option<HorizontalScrollbar>>>,
520    pub y_scrollbar: Rc<RefCell<Option<VerticalScrollbar>>>,
521    pub line_number_tb: Rc<RefCell<Option<TextBoxInner>>>,
522
523    /// for when there are two scrollbars
524    pub corner_decor: Rc<RefCell<DrawCh>>,
525    pub right_click_menu: Rc<RefCell<Option<RightClickMenu>>>,
526}
527
528/// The hook is called when the cursor position changes, the hook is passed the absolute position of the cursor.
529pub type CursorChangedHook = Box<dyn FnMut(usize) -> EventResponses>;
530
531#[yeehaw_derive::impl_pane_basics_from(pane)]
532impl TextBoxInner {
533    const KIND: &'static str = "textbox_inner";
534
535    const STYLE: SelStyles = SelStyles {
536        selected_style: Style::new_const(Color::BLACK, Color::WHITE),
537        ready_style: Style::new_const(Color::BLACK, Color::GREY17),
538        unselectable_style: Style::new_const(Color::BLACK, Color::GREY15),
539    };
540
541    const LINE_NUMBERS_STYLE: Style = Style::new_const(Color::BLACK, Color::GREY13);
542
543    const DEFAULT_CURSOR_STYLE: Style = Style::new_const(Color::WHITE, Color::BLUE);
544
545    /// for textboxes which are editable
546    pub fn editable_receivable_events() -> ReceivableEvents {
547        ReceivableEvents(vec![
548            (KeyPossibility::Chars.into()),
549            (KB::KEY_BACKSPACE.into()),
550            (KB::KEY_ENTER.into()),
551            (KB::KEY_SHIFT_ENTER.into()),
552            (KB::KEY_LEFT.into()),
553            (KB::KEY_RIGHT.into()),
554            (KB::KEY_UP.into()),
555            (KB::KEY_DOWN.into()),
556            (KB::KEY_SHIFT_LEFT.into()),
557            (KB::KEY_SHIFT_RIGHT.into()),
558            (KB::KEY_SHIFT_UP.into()),
559            (KB::KEY_SHIFT_DOWN.into()),
560        ])
561    }
562
563    /// non-editable textboxes can still scroll
564    pub fn non_editable_receivable_events() -> ReceivableEvents {
565        ReceivableEvents(vec![
566            (KB::KEY_LEFT.into()),
567            (KB::KEY_RIGHT.into()),
568            (KB::KEY_UP.into()),
569            (KB::KEY_DOWN.into()),
570            (KB::KEY_H.into()),
571            (KB::KEY_J.into()),
572            (KB::KEY_K.into()),
573            (KB::KEY_L.into()),
574        ])
575    }
576
577    pub fn new<S: Into<String>>(ctx: &Context, text: S) -> Self {
578        let text = text.into();
579        let pane = Pane::new(ctx, Self::KIND)
580            .with_dyn_width(DynVal::FULL)
581            .with_dyn_height(DynVal::FULL)
582            .with_focused_receivable_events(Self::editable_receivable_events())
583            .with_focused(true);
584
585        let tb = TextBoxInner {
586            pane,
587            current_sty: Rc::new(RefCell::new(Style::default())),
588            selectedness: Rc::new(RefCell::new(Selectability::Ready)),
589            is_dirty: Rc::new(RefCell::new(true)),
590            last_size: Rc::new(RefCell::new(Size::default())),
591            last_size_for_sbs: Rc::new(RefCell::new(Size::default())),
592            text: Rc::new(RefCell::new(text.chars().collect())),
593            text_when_empty: Rc::new(RefCell::new("enter text...".to_string())),
594            text_when_empty_fg: Rc::new(RefCell::new(Color::GREY6)),
595            wordwrap: Rc::new(RefCell::new(true)),
596            ch_cursor: Rc::new(RefCell::new(true)),
597            editable: Rc::new(RefCell::new(true)),
598            cursor_pos: Rc::new(RefCell::new(0)),
599            cursor_style: Rc::new(RefCell::new(Self::DEFAULT_CURSOR_STYLE)),
600            visual_mode: Rc::new(RefCell::new(false)),
601            mouse_dragging: Rc::new(RefCell::new(false)),
602            visual_mode_start_pos: Rc::new(RefCell::new(0)),
603            text_changed_hook: Rc::new(RefCell::new(None)),
604            position_style_hook: Rc::new(RefCell::new(None)),
605            cursor_changed_hook: Rc::new(RefCell::new(None)),
606            x_scrollbar: Rc::new(RefCell::new(None)),
607            y_scrollbar: Rc::new(RefCell::new(None)),
608            line_number_tb: Rc::new(RefCell::new(None)),
609            corner_decor: Rc::new(RefCell::new(DrawCh::new('⁙', Style::default_const()))),
610            right_click_menu: Rc::new(RefCell::new(None)),
611        };
612        tb.set_editable_right_click_menu(ctx);
613        tb
614    }
615
616    pub fn set_editable_right_click_menu(&self, ctx: &Context) {
617        let (tb1, tb2, tb3) = (self.clone(), self.clone(), self.clone());
618        let rcm = RightClickMenu::new(ctx, MenuStyle::default()).with_menu_items(
619            ctx,
620            vec![
621                MenuItem::new(ctx, MenuPath("Cut".to_string())).with_fn(Some(Box::new(
622                    move |ctx| {
623                        tb1.is_dirty.replace(true);
624                        tb1.cut_to_clipboard(&ctx)
625                    },
626                ))),
627                MenuItem::new(ctx, MenuPath("Copy".to_string())).with_fn(Some(Box::new(
628                    move |_ctx| {
629                        tb2.is_dirty.replace(true);
630                        tb2.copy_to_clipboard();
631                        EventResponses::default()
632                    },
633                ))),
634                MenuItem::new(ctx, MenuPath("Paste".to_string())).with_fn(Some(Box::new(
635                    move |ctx| {
636                        tb3.is_dirty.replace(true);
637                        tb3.paste_from_clipboard(&ctx)
638                    },
639                ))),
640            ],
641        );
642        *self.right_click_menu.borrow_mut() = Some(rcm);
643    }
644
645    pub fn set_non_editable_right_click_menu(&self, ctx: &Context) {
646        let tb = self.clone();
647        let rcm = RightClickMenu::new(ctx, MenuStyle::default()).with_menu_items(
648            ctx,
649            vec![
650                MenuItem::new(ctx, MenuPath("Copy".to_string())).with_fn(Some(Box::new(
651                    move |_ctx| {
652                        tb.is_dirty.replace(true);
653                        tb.copy_to_clipboard();
654                        EventResponses::default()
655                    },
656                ))),
657            ],
658        );
659        *self.right_click_menu.borrow_mut() = Some(rcm);
660    }
661
662    pub fn at<D: Into<DynVal>, D2: Into<DynVal>>(self, loc_x: D, loc_y: D2) -> Self {
663        self.pane.set_at(loc_x.into(), loc_y.into());
664        self
665    }
666
667    pub fn with_no_wordwrap(self) -> Self {
668        *self.wordwrap.borrow_mut() = false;
669        self
670    }
671
672    pub fn with_wordwrap(self) -> Self {
673        *self.wordwrap.borrow_mut() = true;
674        self
675    }
676
677    pub fn with_ch_cursor(self) -> Self {
678        *self.ch_cursor.borrow_mut() = true;
679        self
680    }
681
682    pub fn with_no_ch_cursor(self) -> Self {
683        *self.ch_cursor.borrow_mut() = false;
684        self
685    }
686
687    pub fn set_editable(&self, ctx: &Context) {
688        *self.editable.borrow_mut() = true;
689        self.pane
690            .set_focused_receivable_events(TextBoxInner::editable_receivable_events());
691        self.set_editable_right_click_menu(ctx);
692    }
693
694    pub fn set_non_editable(&self, ctx: &Context) {
695        *self.editable.borrow_mut() = false;
696        self.pane
697            .set_focused_receivable_events(TextBoxInner::non_editable_receivable_events());
698        self.set_non_editable_right_click_menu(ctx);
699    }
700
701    pub fn editable(self, ctx: &Context) -> Self {
702        self.set_editable(ctx);
703        self
704    }
705
706    pub fn non_editable(self, ctx: &Context) -> Self {
707        self.set_non_editable(ctx);
708        self
709    }
710
711    pub fn non_navigable(self) -> Self {
712        self.pane
713            .set_focused_receivable_events(ReceivableEvents(vec![]));
714        self
715    }
716
717    pub fn get_text(&self) -> String {
718        self.text.borrow().iter().collect()
719    }
720
721    pub fn set_text(&self, text: String) {
722        *self.text.borrow_mut() = text.chars().collect();
723        self.is_dirty.replace(true);
724    }
725
726    // ---------------------------------------------------------
727
728    pub fn get_cursor_pos(&self) -> usize {
729        let cur_pos = *self.cursor_pos.borrow();
730        // NOTE the cursor can be placed at the end of the text
731        // hence the position is the length
732        if cur_pos > self.text.borrow().len() {
733            self.text.borrow().len()
734        } else {
735            cur_pos
736        }
737    }
738
739    pub fn get_visual_mode_start_pos(&self) -> usize {
740        let pos = *self.visual_mode_start_pos.borrow();
741        if pos >= self.text.borrow().len() {
742            self.text.borrow().len() - 1
743        } else {
744            pos
745        }
746    }
747
748    pub fn set_cursor_pos(&self, new_abs_pos: usize) -> EventResponses {
749        *self.cursor_pos.borrow_mut() = new_abs_pos;
750        if let Some(hook) = &mut *self.cursor_changed_hook.borrow_mut() {
751            hook(new_abs_pos)
752        } else {
753            EventResponses::default()
754        }
755    }
756
757    pub fn incr_cursor_pos(&self, pos_change: isize) -> EventResponses {
758        let new_pos = (self.get_cursor_pos() as isize + pos_change).max(0) as usize;
759        self.set_cursor_pos(new_pos)
760    }
761
762    /// returns the wrapped characters of the text
763    pub fn get_wrapped(&self, draw_size: Option<Size>) -> WrChs {
764        let width = draw_size
765            .map(|s| s.width)
766            .unwrap_or(self.get_last_size().width) as usize;
767
768        let mut rs = self.text.borrow().clone();
769        rs.push(' '); // add the space for the final possible position
770        let mut chs = vec![];
771        let mut max_x = 0;
772        let (mut x, mut y) = (0, 0); // working x and y position in the textbox
773        for (abs_pos, r) in rs.iter().enumerate() {
774            if *self.wordwrap.borrow() && x == width {
775                y += 1;
776                x = 0;
777                if x > max_x {
778                    max_x = x;
779                }
780                chs.push(WrCh::new('\n', None, x, y));
781            }
782
783            if *r == '\n' {
784                // If ch_cursor without wordwrap, add an extra space to the end of
785                // the line so that the cursor can be placed there. Without this
786                // extra space, placing the cursor at the end of the largest line
787                // will panic.
788                if *self.ch_cursor.borrow() && !*self.wordwrap.borrow() {
789                    if x > max_x {
790                        max_x = x;
791                    }
792                    chs.push(WrCh::new(' ', None, x, y));
793                }
794
795                // the "newline character" exists as an extra space at
796                // the end of the line
797                if x > max_x {
798                    max_x = x;
799                }
800                chs.push(WrCh::new('\n', Some(abs_pos), x, y));
801
802                // move the working position to the beginning of the next line
803                y += 1;
804                x = 0;
805            } else {
806                if x > max_x {
807                    max_x = x;
808                }
809                chs.push(WrCh::new(*r, Some(abs_pos), x, y));
810                x += 1;
811            }
812        }
813        WrChs { chs, max_x }
814    }
815
816    /// returns the formatted line numbers of the textbox
817    /// line numbers are right justified
818    pub fn get_line_numbers(&self, draw_size: Option<Size>) -> (String, usize) {
819        let wr_chs = self.get_wrapped(draw_size);
820
821        // get the max line number
822        let mut max_line_num = 0;
823        for (i, wr_ch) in wr_chs.chs.iter().enumerate() {
824            if (wr_ch.ch == '\n' && wr_ch.abs_pos.is_some()) || i == 0 {
825                max_line_num += 1;
826            }
827        }
828
829        // get the largest amount of digits in the line numbers from the string
830        let line_num_width = max_line_num.to_string().chars().count();
831
832        let mut s = String::new();
833        let mut true_line_num = 1;
834
835        for (i, wr_ch) in wr_chs.chs.iter().enumerate() {
836            // NOTE this i=0 case needs to be seperate from the newline case
837            // for when the first character is a newline we need print two line numbers
838            if i == 0 {
839                s += &format!("{:line_num_width$} ", true_line_num);
840                true_line_num += 1;
841                s += "\n";
842            }
843            if wr_ch.ch == '\n' {
844                if wr_ch.abs_pos.is_some() {
845                    s += &format!("{:line_num_width$} ", true_line_num);
846                    true_line_num += 1;
847                }
848                s += "\n";
849            }
850        }
851
852        (s, line_num_width + 1) // +1 for the extra space after the digits
853    }
854
855    /// NOTE the resp is sent in to potentially modify the offsets from numbers tb
856    pub fn correct_offsets(&self, dr: &DrawRegion, w: &WrChs) -> EventResponse {
857        if *self.ch_cursor.borrow() {
858            let (x, y) = w.cursor_x_and_y(self.get_cursor_pos());
859            let (x, y) = (x.unwrap_or(0), y.unwrap_or(0));
860            self.pane.correct_offsets_to_view_position(dr, x, y);
861        }
862        self.correct_ln_and_sbs(dr)
863    }
864
865    /// correct the line number (if applicable) and scrollbar positions
866    pub fn correct_ln_and_sbs(&self, dr: &DrawRegion) -> EventResponse {
867        let y_offset = self.pane.get_content_y_offset();
868        let x_offset = self.pane.get_content_x_offset();
869
870        let update_size = if *self.last_size_for_sbs.borrow() != dr.size {
871            *self.last_size_for_sbs.borrow_mut() = dr.size;
872            self.is_dirty.replace(true);
873            true
874        } else {
875            false
876        };
877
878        // update the scrollbars/line numbers textbox
879        if let Some(sb) = self.y_scrollbar.borrow().as_ref() {
880            if update_size {
881                sb.set_scrollable_view_size(dr.size);
882                *sb.scrollable_view_chs.borrow_mut() = DynVal::new_fixed(dr.size.height as i32);
883            }
884
885            sb.external_change(y_offset, self.pane.content_height(), dr.size);
886        }
887        let resp = EventResponse::default();
888        if let Some(ln_tb) = self.line_number_tb.borrow().as_ref() {
889            let (lns, lnw) = self.get_line_numbers(Some(dr.size));
890            let last_lnw = ln_tb.pane.get_width(dr);
891            if lnw != last_lnw {
892                let ln_start_x = ln_tb.pane.get_dyn_start_x();
893                let tb_start_x = ln_start_x.plus_fixed(lnw as i32);
894                self.pane.set_start_x(tb_start_x);
895                ln_tb.pane.set_dyn_width(DynVal::new_fixed(lnw as i32))
896            }
897            ln_tb.set_text(lns);
898            ln_tb.pane.set_content_y_offset(Some(dr), y_offset);
899        }
900        if let Some(sb) = self.x_scrollbar.borrow().as_ref() {
901            if update_size {
902                sb.set_scrollable_view_size(dr.size);
903                *sb.scrollable_view_chs.borrow_mut() = DynVal::new_fixed(dr.size.width as i32);
904            }
905            sb.external_change(x_offset, self.pane.content_width(), dr.size);
906        }
907        resp
908    }
909
910    /// get the start and end position of the visually selected text
911    pub fn visual_selection_pos(&self) -> Option<(usize, usize)> {
912        let mut cur_pos = self.get_cursor_pos();
913        if *self.visual_mode.borrow() {
914            let start_pos = self.get_visual_mode_start_pos();
915            if cur_pos >= self.text.borrow().len() {
916                cur_pos = self.text.borrow().len() - 1;
917            }
918            if start_pos < cur_pos {
919                Some((start_pos, cur_pos))
920            } else {
921                Some((cur_pos, start_pos))
922            }
923        } else {
924            if cur_pos >= self.text.borrow().len() {
925                return None;
926            }
927            Some((cur_pos, cur_pos))
928        }
929    }
930
931    pub fn visual_selected_text(&self) -> String {
932        let text = self.text.borrow();
933        let Some((start_pos, end_pos)) = self.visual_selection_pos() else {
934            return String::new();
935        };
936        if !*self.visual_mode.borrow() {
937            return text[start_pos].to_string();
938        }
939        text[start_pos..=end_pos].iter().collect()
940    }
941
942    pub fn delete_visual_selection(&self, ctx: &Context) -> EventResponses {
943        if !*self.visual_mode.borrow() {
944            return EventResponses::default();
945        }
946
947        // delete everything in the visual selection
948        let mut rs = self.text.borrow().clone();
949
950        let Some((start_pos, end_pos)) = self.visual_selection_pos() else {
951            return EventResponses::default();
952        };
953        rs.drain(start_pos..=end_pos);
954        self.set_cursor_pos(start_pos);
955
956        *self.text.borrow_mut() = rs;
957        *self.visual_mode.borrow_mut() = false;
958        let w = self.get_wrapped(None);
959        self.pane.set_content_from_string(w.wrapped_string());
960        self.is_dirty.replace(true);
961        let resps = if let Some(hook) = &mut *self.text_changed_hook.borrow_mut() {
962            hook(ctx.clone(), self.get_text())
963        } else {
964            EventResponses::default()
965        };
966        resps
967    }
968
969    pub fn copy_to_clipboard(&self) {
970        let text = self.visual_selected_text();
971        let Ok(mut cb) = arboard::Clipboard::new() else {
972            log_err!("failed to get clipboard");
973            return;
974        };
975        if let Err(e) = cb.set_text(text) {
976            log_err!("failed to set text to clipboard: {}", e);
977        }
978    }
979
980    pub fn cut_to_clipboard(&self, ctx: &Context) -> EventResponses {
981        self.copy_to_clipboard();
982        self.delete_visual_selection(ctx)
983    }
984
985    pub fn paste_from_clipboard(&self, ctx: &Context) -> EventResponses {
986        let mut resps = self.delete_visual_selection(ctx);
987
988        let Ok(mut cb) = arboard::Clipboard::new() else {
989            log_err!("failed to get clipboard");
990            return EventResponses::default();
991        };
992        let Ok(cliptext) = cb.get_text() else {
993            log_err!("failed to get text from clipboard");
994            return EventResponses::default();
995        };
996        if cliptext.is_empty() {
997            return resps;
998        }
999        let cliprunes = cliptext.chars().collect::<Vec<char>>();
1000        let mut rs = self.text.borrow().clone();
1001        let cur_pos = self.get_cursor_pos();
1002        rs.splice(cur_pos..cur_pos, cliprunes.iter().cloned());
1003        *self.text.borrow_mut() = rs;
1004
1005        self.incr_cursor_pos(cliprunes.len() as isize);
1006        let w = self.get_wrapped(None);
1007        self.pane.set_content_from_string(w.wrapped_string()); // See NOTE-1
1008
1009        self.is_dirty.replace(true);
1010
1011        if let Some(hook) = &mut *self.text_changed_hook.borrow_mut() {
1012            resps.extend(hook(ctx.clone(), self.get_text()));
1013        }
1014        resps
1015    }
1016
1017    pub fn receive_key_event(&self, ctx: &Context, ev: Vec<KeyEvent>) -> (bool, EventResponses) {
1018        if *self.selectedness.borrow() != Selectability::Selected || ev.is_empty() {
1019            return (false, EventResponses::default());
1020        }
1021
1022        self.is_dirty.replace(true);
1023
1024        if !*self.ch_cursor.borrow() {
1025            match true {
1026                _ if ev[0] == KB::KEY_LEFT || ev[0] == KB::KEY_H => {
1027                    self.pane.scroll_left(None);
1028                }
1029                _ if ev[0] == KB::KEY_RIGHT || ev[0] == KB::KEY_L => {
1030                    self.pane.scroll_right(None);
1031                }
1032                _ if ev[0] == KB::KEY_DOWN || ev[0] == KB::KEY_J => {
1033                    self.pane.scroll_down(None);
1034                }
1035                _ if ev[0] == KB::KEY_UP || ev[0] == KB::KEY_K => {
1036                    self.pane.scroll_up(None);
1037                }
1038                _ => {}
1039            }
1040
1041            return (true, EventResponses::default());
1042        }
1043
1044        let mut visual_mode_event = false;
1045        let visual_mode = *self.visual_mode.borrow();
1046        let cursor_pos = self.get_cursor_pos();
1047
1048        let mut resps = EventResponses::default();
1049        match true {
1050            _ if ev[0] == KB::KEY_SHIFT_LEFT => {
1051                visual_mode_event = true;
1052                if !visual_mode {
1053                    *self.visual_mode.borrow_mut() = true;
1054                    *self.visual_mode_start_pos.borrow_mut() = cursor_pos;
1055                }
1056                if cursor_pos > 0 {
1057                    self.incr_cursor_pos(-1);
1058                }
1059            }
1060
1061            _ if ev[0] == KB::KEY_SHIFT_RIGHT => {
1062                visual_mode_event = true;
1063                if !visual_mode {
1064                    *self.visual_mode.borrow_mut() = true;
1065                    *self.visual_mode_start_pos.borrow_mut() = cursor_pos;
1066                }
1067                if cursor_pos < self.text.borrow().len() {
1068                    self.incr_cursor_pos(1);
1069                }
1070            }
1071
1072            _ if ev[0] == KB::KEY_SHIFT_UP => {
1073                visual_mode_event = true;
1074                if !visual_mode {
1075                    *self.visual_mode.borrow_mut() = true;
1076                    *self.visual_mode_start_pos.borrow_mut() = cursor_pos;
1077                }
1078                let w = self.get_wrapped(None);
1079                if let Some(new_pos) = w.get_cursor_above_position(cursor_pos) {
1080                    self.set_cursor_pos(new_pos);
1081                }
1082            }
1083
1084            _ if ev[0] == KB::KEY_SHIFT_DOWN => {
1085                visual_mode_event = true;
1086                if !visual_mode {
1087                    *self.visual_mode.borrow_mut() = true;
1088                    *self.visual_mode_start_pos.borrow_mut() = cursor_pos;
1089                }
1090                let w = self.get_wrapped(None);
1091                if let Some(new_pos) = w.get_cursor_below_position(cursor_pos) {
1092                    self.set_cursor_pos(new_pos);
1093                }
1094            }
1095
1096            _ if ev[0] == KB::KEY_LEFT || (!*self.editable.borrow() && ev[0] == KB::KEY_H) => {
1097                if cursor_pos > 0 && cursor_pos <= self.text.borrow().len() {
1098                    // do not move left if at the beginning of a line
1099                    if self.text.borrow()[cursor_pos - 1] != '\n' {
1100                        self.incr_cursor_pos(-1);
1101                    }
1102                }
1103            }
1104
1105            _ if ev[0] == KB::KEY_RIGHT || (!*self.editable.borrow() && ev[0] == KB::KEY_L) => {
1106                // don't allow moving to the next line
1107                if cursor_pos < self.text.borrow().len() && self.text.borrow()[cursor_pos] != '\n' {
1108                    self.incr_cursor_pos(1);
1109                }
1110            }
1111
1112            _ if ev[0] == KB::KEY_UP || (!*self.editable.borrow() && ev[0] == KB::KEY_K) => {
1113                let w = self.get_wrapped(None);
1114                if let Some(new_pos) = w.get_cursor_above_position(cursor_pos) {
1115                    self.set_cursor_pos(new_pos);
1116                }
1117            }
1118
1119            _ if ev[0] == KB::KEY_DOWN || (!*self.editable.borrow() && ev[0] == KB::KEY_J) => {
1120                let w = self.get_wrapped(None);
1121                if let Some(new_pos) = w.get_cursor_below_position(cursor_pos) {
1122                    self.set_cursor_pos(new_pos);
1123                }
1124            }
1125
1126            _ if *self.editable.borrow() && (ev[0] == KB::KEY_BACKSPACE) => {
1127                if visual_mode {
1128                    resps = self.delete_visual_selection(ctx);
1129                } else if cursor_pos > 0 {
1130                    let mut rs = self.text.borrow().clone();
1131                    rs.remove(cursor_pos - 1);
1132                    self.incr_cursor_pos(-1);
1133                    *self.text.borrow_mut() = rs;
1134                    let w = self.get_wrapped(None);
1135                    self.pane.set_content_from_string(w.wrapped_string()); // See NOTE-1
1136                    if let Some(hook) = &mut *self.text_changed_hook.borrow_mut() {
1137                        resps = hook(ctx.clone(), self.get_text());
1138                    }
1139                }
1140            }
1141
1142            _ if *self.editable.borrow() && ev[0] == KB::KEY_ENTER => {
1143                let mut rs = self.text.borrow().clone();
1144                rs.splice(cursor_pos..cursor_pos, std::iter::once('\n'));
1145                *self.text.borrow_mut() = rs;
1146                self.incr_cursor_pos(1);
1147                let w = self.get_wrapped(None);
1148                self.pane.set_content_from_string(w.wrapped_string()); // See NOTE-1
1149                if let Some(hook) = &mut *self.text_changed_hook.borrow_mut() {
1150                    resps = hook(ctx.clone(), self.get_text());
1151                }
1152            }
1153
1154            _ if *self.editable.borrow() && KeyPossibility::Chars.matches_key(&ev[0]) => {
1155                if let crossterm::event::KeyCode::Char(r) = ev[0].code {
1156                    let mut rs = self.text.borrow().clone();
1157                    rs.insert(cursor_pos, r);
1158                    *self.text.borrow_mut() = rs;
1159                    self.incr_cursor_pos(1);
1160                    let w = self.get_wrapped(None);
1161
1162                    // NOTE-1: must call SetContentFromString to update the content
1163                    // before updating the offset or else the offset amount may not
1164                    // exist in the content and the widget pane will reject the new
1165                    // offset
1166                    self.pane.set_content_from_string(w.wrapped_string());
1167                    if let Some(hook) = &mut *self.text_changed_hook.borrow_mut() {
1168                        resps = hook(ctx.clone(), self.get_text());
1169                    }
1170                }
1171            }
1172
1173            _ => {}
1174        }
1175
1176        if *self.visual_mode.borrow() && !visual_mode_event {
1177            *self.visual_mode.borrow_mut() = false;
1178        }
1179
1180        (true, resps)
1181    }
1182
1183    // used to determine if scroll mouse events should be captured
1184    fn changes_made(
1185        &self, start_cur_pos: usize, start_offset_x: usize, start_offset_y: usize,
1186    ) -> bool {
1187        let end_cur_pos = self.get_cursor_pos();
1188        let end_offset_x = self.pane.get_content_x_offset();
1189        let end_offset_y = self.pane.get_content_y_offset();
1190        if start_cur_pos != end_cur_pos
1191            || start_offset_x != end_offset_x
1192            || start_offset_y != end_offset_y
1193        {
1194            return true;
1195        }
1196        false
1197    }
1198
1199    pub fn receive_mouse_event(&self, _: &Context, ev: MouseEvent) -> (bool, EventResponses) {
1200        // handle right click
1201        if let Some(rcm) = &*self.right_click_menu.borrow() {
1202            if let Some(resps) = rcm.create_menu_if_right_click(&ev) {
1203                return (true, resps);
1204            }
1205        }
1206
1207        if !matches!(ev.kind, MouseEventKind::Moved) {
1208            self.is_dirty.replace(true);
1209        }
1210
1211        let selectedness = *self.selectedness.borrow();
1212        let cursor_pos = self.get_cursor_pos();
1213        let start_offset_x = self.pane.get_content_x_offset();
1214        let start_offset_y = self.pane.get_content_y_offset();
1215        match ev.kind {
1216            MouseEventKind::ScrollDown
1217                if ev.modifiers == KeyModifiers::NONE
1218                    && selectedness == Selectability::Selected =>
1219            {
1220                let w = self.get_wrapped(Some(ev.dr.size));
1221                let Some(new_pos) = w.get_cursor_below_position(cursor_pos) else {
1222                    return (
1223                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1224                        EventResponses::default(),
1225                    );
1226                };
1227                self.set_cursor_pos(new_pos);
1228                return (
1229                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1230                    EventResponses::default(),
1231                );
1232            }
1233            MouseEventKind::ScrollUp
1234                if ev.modifiers == KeyModifiers::NONE
1235                    && selectedness == Selectability::Selected =>
1236            {
1237                let w = self.get_wrapped(Some(ev.dr.size));
1238                let Some(new_pos) = w.get_cursor_above_position(cursor_pos) else {
1239                    return (
1240                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1241                        EventResponses::default(),
1242                    );
1243                };
1244                self.set_cursor_pos(new_pos);
1245                return (
1246                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1247                    EventResponses::default(),
1248                );
1249            }
1250            MouseEventKind::ScrollLeft
1251                if ev.modifiers == KeyModifiers::NONE
1252                    && selectedness == Selectability::Selected =>
1253            {
1254                let w = self.get_wrapped(Some(ev.dr.size));
1255                let Some(new_pos) = w.get_cursor_left_position(cursor_pos) else {
1256                    return (
1257                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1258                        EventResponses::default(),
1259                    );
1260                };
1261                self.set_cursor_pos(new_pos);
1262                return (
1263                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1264                    EventResponses::default(),
1265                );
1266            }
1267            MouseEventKind::ScrollDown
1268                if ev.modifiers == KeyModifiers::SHIFT
1269                    && selectedness == Selectability::Selected =>
1270            {
1271                let w = self.get_wrapped(Some(ev.dr.size));
1272                let Some(new_pos) = w.get_cursor_left_position(cursor_pos) else {
1273                    return (
1274                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1275                        EventResponses::default(),
1276                    );
1277                };
1278                self.set_cursor_pos(new_pos);
1279                return (
1280                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1281                    EventResponses::default(),
1282                );
1283            }
1284            MouseEventKind::ScrollRight
1285                if ev.modifiers == KeyModifiers::NONE
1286                    && selectedness == Selectability::Selected =>
1287            {
1288                let w = self.get_wrapped(Some(ev.dr.size));
1289                let Some(new_pos) = w.get_cursor_right_position(cursor_pos) else {
1290                    return (
1291                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1292                        EventResponses::default(),
1293                    );
1294                };
1295                self.set_cursor_pos(new_pos);
1296                return (
1297                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1298                    EventResponses::default(),
1299                );
1300            }
1301            MouseEventKind::ScrollUp
1302                if ev.modifiers == KeyModifiers::SHIFT
1303                    && selectedness == Selectability::Selected =>
1304            {
1305                let w = self.get_wrapped(Some(ev.dr.size));
1306                let Some(new_pos) = w.get_cursor_right_position(cursor_pos) else {
1307                    return (
1308                        self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1309                        EventResponses::default(),
1310                    );
1311                };
1312                self.set_cursor_pos(new_pos);
1313                return (
1314                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1315                    EventResponses::default(),
1316                );
1317            }
1318            MouseEventKind::ScrollDown
1319                if ev.modifiers == KeyModifiers::NONE && selectedness == Selectability::Ready =>
1320            {
1321                self.pane.scroll_down(Some(&ev.dr));
1322                return (
1323                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1324                    EventResponses::default(),
1325                );
1326            }
1327            MouseEventKind::ScrollUp
1328                if ev.modifiers == KeyModifiers::NONE && selectedness == Selectability::Ready =>
1329            {
1330                self.pane.scroll_up(Some(&ev.dr));
1331                return (
1332                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1333                    EventResponses::default(),
1334                );
1335            }
1336            MouseEventKind::ScrollLeft
1337                if ev.modifiers == KeyModifiers::NONE && selectedness == Selectability::Ready =>
1338            {
1339                self.pane.scroll_left(Some(&ev.dr));
1340                return (
1341                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1342                    EventResponses::default(),
1343                );
1344            }
1345            MouseEventKind::ScrollDown
1346                if ev.modifiers == KeyModifiers::SHIFT && selectedness == Selectability::Ready =>
1347            {
1348                self.pane.scroll_left(Some(&ev.dr));
1349                return (
1350                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1351                    EventResponses::default(),
1352                );
1353            }
1354            MouseEventKind::ScrollRight
1355                if ev.modifiers == KeyModifiers::NONE && selectedness == Selectability::Ready =>
1356            {
1357                self.pane.scroll_right(Some(&ev.dr));
1358                return (
1359                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1360                    EventResponses::default(),
1361                );
1362            }
1363            MouseEventKind::ScrollUp
1364                if ev.modifiers == KeyModifiers::SHIFT && selectedness == Selectability::Ready =>
1365            {
1366                self.pane.scroll_right(Some(&ev.dr));
1367                return (
1368                    self.changes_made(cursor_pos, start_offset_x, start_offset_y),
1369                    EventResponses::default(),
1370                );
1371            }
1372
1373            MouseEventKind::Moved | MouseEventKind::Up(_) => {
1374                *self.mouse_dragging.borrow_mut() = false;
1375
1376                // we need to capture the click, needed for selectability mechanism
1377                return (true, EventResponses::default());
1378            }
1379
1380            // set the cursor to the mouse position on primary click
1381            MouseEventKind::Down(MouseButton::Left) | MouseEventKind::Drag(MouseButton::Left)
1382                if selectedness == Selectability::Ready =>
1383            {
1384                return (true, EventResponses::default());
1385            }
1386
1387            // set the cursor to the mouse position on primary click
1388            MouseEventKind::Down(MouseButton::Left) | MouseEventKind::Drag(MouseButton::Left)
1389                if selectedness == Selectability::Selected =>
1390            {
1391                let x = ev.column as usize + self.pane.get_content_x_offset();
1392                let y = ev.row as usize + self.pane.get_content_y_offset();
1393                let w = self.get_wrapped(Some(ev.dr.size));
1394
1395                let mouse_dragging = *self.mouse_dragging.borrow();
1396                let visual_mode = *self.visual_mode.borrow();
1397                if mouse_dragging && !visual_mode {
1398                    *self.visual_mode.borrow_mut() = true;
1399                    *self.visual_mode_start_pos.borrow_mut() = cursor_pos;
1400                }
1401                if !mouse_dragging && visual_mode {
1402                    *self.visual_mode.borrow_mut() = false;
1403                }
1404                *self.mouse_dragging.borrow_mut() = true;
1405                if let Some(new_pos) = w.get_nearest_valid_cursor_from_position(x, y) {
1406                    self.set_cursor_pos(new_pos);
1407                    return (true, EventResponses::default());
1408                }
1409                return (true, EventResponses::default());
1410            }
1411            _ => {}
1412        }
1413
1414        (false, EventResponses::default())
1415    }
1416
1417    /// updates the content of the textbox
1418    pub fn update_content(&self, ctx: &Context, dr: &DrawRegion) {
1419        let w = self.get_wrapped(Some(dr.size));
1420        let wrapped = w.wrapped_string();
1421        let _ = self.correct_offsets(dr, &w);
1422
1423        let curr_sty = self.current_sty.borrow().clone();
1424        let mut sty = curr_sty.clone();
1425        if wrapped.len() == 1
1426            && *self.ch_cursor.borrow()
1427            && *self.selectedness.borrow() != Selectability::Selected
1428        {
1429            // set greyed out text
1430            let text = self.text_when_empty.borrow();
1431            sty.set_fg(self.text_when_empty_fg.borrow().clone());
1432            self.pane.set_content_from_string_with_style(dr, &text, sty);
1433            return;
1434        } else {
1435            self.pane
1436                .set_content_from_string_with_style(dr, &wrapped, sty);
1437        }
1438
1439        // set styles from hooks if applicable
1440        if let Some(hook) = &mut *self.position_style_hook.borrow_mut() {
1441            for wr_ch in w.chs.iter() {
1442                let existing_sty = curr_sty.clone();
1443                if wr_ch.abs_pos.is_none() {
1444                    continue;
1445                }
1446                if let Some(abs_pos) = wr_ch.abs_pos {
1447                    let sty = hook(ctx.clone(), abs_pos, existing_sty);
1448                    self.pane
1449                        .get_content_mut()
1450                        .change_style_at_xy(wr_ch.x_pos, wr_ch.y_pos, sty);
1451                }
1452            }
1453        }
1454
1455        // set cursor style
1456        if *self.selectedness.borrow() == Selectability::Selected && *self.ch_cursor.borrow() {
1457            let (cur_x, cur_y) = w.cursor_x_and_y(self.get_cursor_pos());
1458            if let (Some(cur_x), Some(cur_y)) = (cur_x, cur_y) {
1459                self.pane.get_content_mut().change_style_at_xy(
1460                    cur_x,
1461                    cur_y,
1462                    self.cursor_style.borrow().clone(),
1463                );
1464            }
1465        }
1466        if *self.visual_mode.borrow() {
1467            let start_pos = self.get_visual_mode_start_pos();
1468            let cur_pos = self.get_cursor_pos();
1469
1470            let start = if start_pos < cur_pos { start_pos } else { cur_pos };
1471            let end = if start_pos < cur_pos { cur_pos } else { start_pos };
1472            for i in start..=end {
1473                if let (Some(cur_x), Some(cur_y)) = w.cursor_x_and_y(i) {
1474                    self.pane.get_content_mut().change_style_at_xy(
1475                        cur_x,
1476                        cur_y,
1477                        self.cursor_style.borrow().clone(),
1478                    );
1479                }
1480            }
1481        }
1482    }
1483}
1484
1485#[yeehaw_derive::impl_element_from(pane)]
1486impl Element for TextBoxInner {
1487    fn receive_event(&self, ctx: &Context, ev: Event) -> (bool, EventResponses) {
1488        match ev {
1489            Event::KeyCombo(ke) => self.receive_key_event(ctx, ke),
1490            Event::Mouse(me) => self.receive_mouse_event(ctx, me),
1491            _ => (false, EventResponses::default()),
1492        }
1493    }
1494
1495    fn drawing(&self, ctx: &Context, dr: &DrawRegion, force_update: bool) -> Vec<DrawUpdate> {
1496        if self.is_dirty.replace(false) || *self.last_size.borrow() != dr.size || force_update {
1497            self.update_content(ctx, dr);
1498            self.last_size.replace(dr.size);
1499        }
1500        self.pane.drawing(ctx, dr, force_update)
1501    }
1502}
1503
1504/// wrapped character
1505#[derive(Clone, Default)]
1506pub struct WrCh {
1507    /// the character
1508    ch: char,
1509
1510    /// absolute position in the text
1511    /// If this character is a NOT a part of the text and only introduced
1512    /// due to line wrapping, the absPos will be None (and ch='\n')
1513    abs_pos: Option<usize>,
1514
1515    /// x position in the line
1516    x_pos: usize,
1517    /// y position of the line
1518    y_pos: usize,
1519}
1520
1521impl WrCh {
1522    pub fn new(ch: char, abs_pos: Option<usize>, x_pos: usize, y_pos: usize) -> Self {
1523        WrCh {
1524            ch,
1525            abs_pos,
1526            x_pos,
1527            y_pos,
1528        }
1529    }
1530}
1531
1532/// wrapped characters
1533#[derive(Clone, Default)]
1534pub struct WrChs {
1535    chs: Vec<WrCh>,
1536    /// the maximum x position within the wrapped characters
1537    max_x: usize,
1538}
1539
1540impl WrChs {
1541    pub fn wrapped_string(&self) -> String {
1542        self.chs.iter().map(|wr_ch| wr_ch.ch).collect()
1543    }
1544
1545    /// gets the cursor x and y position in the wrapped text
1546    /// from the absolute cursor position provided.
1547    pub fn cursor_x_and_y(&self, cur_abs: usize) -> (Option<usize>, Option<usize>) {
1548        self.chs
1549            .iter()
1550            .find(|wr_ch| wr_ch.abs_pos == Some(cur_abs))
1551            .map(|wr_ch| (Some(wr_ch.x_pos), Some(wr_ch.y_pos)))
1552            .unwrap_or_default()
1553    }
1554
1555    /// gets the line at the given y position
1556    pub fn get_line(&self, y: usize) -> Vec<char> {
1557        self.chs
1558            .iter()
1559            .filter(|wr_ch| wr_ch.y_pos == y)
1560            .map(|wr_ch| wr_ch.ch)
1561            .collect()
1562    }
1563
1564    /// maximum y position in the wrapped text
1565    pub fn max_y(&self) -> usize {
1566        self.chs.last().cloned().unwrap_or_default().y_pos
1567    }
1568
1569    pub fn max_x(&self) -> usize {
1570        self.max_x
1571    }
1572
1573    /// determine the cursor position above the current cursor position
1574    ///                                                         cur_abs
1575    pub fn get_cursor_above_position(&self, cur_abs: usize) -> Option<usize> {
1576        let (cur_i, cur_x, cur_y) = self
1577            .chs
1578            .iter()
1579            .enumerate()
1580            .find(|(_, wr_ch)| wr_ch.abs_pos == Some(cur_abs))
1581            .map(|(i, wr_ch)| (i, wr_ch.x_pos, wr_ch.y_pos))?;
1582
1583        if cur_y == 0 {
1584            return None;
1585        }
1586        self.chs
1587            .iter()
1588            .take(cur_i)
1589            .rev()
1590            .find(|wr_ch| wr_ch.y_pos == cur_y - 1 && wr_ch.x_pos <= cur_x)
1591            .map(|wr_ch| wr_ch.abs_pos)
1592            .unwrap_or(None)
1593    }
1594
1595    /// determine the cursor position below the current cursor position.
1596    ///                                                         cur_abs
1597    pub fn get_cursor_below_position(&self, cur_abs: usize) -> Option<usize> {
1598        let (cur_i, cur_x, cur_y) = self
1599            .chs
1600            .iter()
1601            .enumerate()
1602            .find(|(_, wr_ch)| wr_ch.abs_pos == Some(cur_abs))
1603            .map(|(i, wr_ch)| (i, wr_ch.x_pos, wr_ch.y_pos))?;
1604
1605        if cur_y == self.max_y() {
1606            return None;
1607        }
1608
1609        // move forwards in the wrapped text until we find the character with a y position one
1610        // greater than the current cursor position and with the maximum x position less than or
1611        // equal to the current cursor x position.
1612        self.chs
1613            .iter()
1614            .skip(cur_i)
1615            .take_while(|wr_ch| wr_ch.y_pos <= cur_y + 1) // optimization 
1616            .filter(|wr_ch| wr_ch.y_pos == cur_y + 1 && wr_ch.x_pos <= cur_x)
1617            .last()
1618            .map(|wr_ch| wr_ch.abs_pos)
1619            .unwrap_or(None)
1620    }
1621
1622    //                                                        cur_abs
1623    pub fn get_cursor_left_position(&self, cur_abs: usize) -> Option<usize> {
1624        let (cur_i, cur_x, cur_y) = self
1625            .chs
1626            .iter()
1627            .enumerate()
1628            .find(|(_, wr_ch)| wr_ch.abs_pos == Some(cur_abs))
1629            .map(|(i, wr_ch)| (i, wr_ch.x_pos, wr_ch.y_pos))?;
1630
1631        if cur_x == 0 {
1632            return None;
1633        }
1634        self.chs
1635            .iter()
1636            .take(cur_i)
1637            .rev()
1638            .find(|wr_ch| wr_ch.y_pos == cur_y && wr_ch.x_pos < cur_x)
1639            .map(|wr_ch| wr_ch.abs_pos)
1640            .unwrap_or(None)
1641    }
1642
1643    pub fn get_cursor_right_position(&self, cur_abs: usize) -> Option<usize> {
1644        let (cur_i, cur_x, cur_y) = self
1645            .chs
1646            .iter()
1647            .enumerate()
1648            .find(|(_, wr_ch)| wr_ch.abs_pos == Some(cur_abs))
1649            .map(|(i, wr_ch)| (i, wr_ch.x_pos, wr_ch.y_pos))?;
1650
1651        if cur_x >= self.max_x {
1652            return None;
1653        }
1654        self.chs
1655            .iter()
1656            .skip(cur_i)
1657            .find(|wr_ch| wr_ch.y_pos == cur_y && wr_ch.x_pos > cur_x)
1658            .map(|wr_ch| wr_ch.abs_pos)
1659            .unwrap_or(None)
1660    }
1661
1662    pub fn get_nearest_valid_cursor_from_position(&self, x: usize, y: usize) -> Option<usize> {
1663        let mut nearest_abs = None; // nearest absolute position with the same y position
1664        let mut nearest_abs_y_pos = None; // Y position of the nearest absolute position
1665        let mut nearest_abs_x_pos = None; // X position of the nearest absolute position
1666        for wr_ch in &self.chs {
1667            if wr_ch.abs_pos.is_none() {
1668                continue;
1669            }
1670            if wr_ch.y_pos == y && wr_ch.x_pos == x {
1671                return wr_ch.abs_pos;
1672            }
1673
1674            let y_diff = (wr_ch.y_pos as isize - y as isize).abs();
1675            let nearest_y_diff = match nearest_abs_y_pos {
1676                Some(nearest_abs_y_pos) => (nearest_abs_y_pos as isize - y as isize).abs(),
1677                None => y_diff,
1678            };
1679
1680            let x_diff = (wr_ch.x_pos as isize - x as isize).abs();
1681            let nearest_x_diff = match nearest_abs_x_pos {
1682                Some(nearest_abs_x_pos) => (nearest_abs_x_pos as isize - x as isize).abs(),
1683                None => x_diff,
1684            };
1685
1686            if y_diff < nearest_y_diff || (y_diff == nearest_y_diff && x_diff < nearest_x_diff) {
1687                nearest_abs_y_pos = Some(wr_ch.y_pos);
1688                nearest_abs_x_pos = Some(wr_ch.x_pos);
1689                nearest_abs = wr_ch.abs_pos;
1690            }
1691        }
1692        nearest_abs
1693    }
1694}