Skip to main content

radicle_tui/ui/
widget.rs

1use std::cmp;
2use std::collections::HashSet;
3use std::hash::Hash;
4
5use ratatui::symbols::border;
6use serde::{Deserialize, Serialize};
7
8use ratatui::layout::{Alignment, Direction, Layout, Position, Rect};
9use ratatui::style::{Style, Stylize};
10use ratatui::text::{Line, Span, Text};
11use ratatui::widgets::{Block, BorderType, Row, Scrollbar, ScrollbarOrientation, ScrollbarState};
12use ratatui::Frame;
13use ratatui::{layout::Constraint, widgets::Paragraph};
14
15use crate::event::Key;
16use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
17use crate::ui::layout::Spacing;
18use crate::ui::theme::{style, Theme};
19use crate::ui::ToRow;
20use crate::ui::{layout, span, ToTree};
21
22use super::{Context, InnerResponse, Response, Ui};
23
24pub type AddContentFn<'a, M, R> = dyn FnOnce(&mut Ui<M>) -> R + 'a;
25
26pub const RENDER_WIDTH_XSMALL: usize = 50;
27pub const RENDER_WIDTH_SMALL: usize = 70;
28pub const RENDER_WIDTH_MEDIUM: usize = 150;
29pub const RENDER_WIDTH_LARGE: usize = usize::MAX;
30
31pub trait Widget {
32    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
33    where
34        M: Clone;
35}
36
37/// `Borders` defines which borders should be drawn around a widget.
38pub enum Borders {
39    None,
40    Spacer { top: usize, left: usize },
41    All,
42    Top,
43    Sides,
44    Bottom,
45    BottomSides,
46}
47
48#[derive(Clone, Debug, Default)]
49pub struct ColumnView {
50    small: bool,
51    medium: bool,
52    large: bool,
53}
54
55impl ColumnView {
56    pub fn all() -> Self {
57        Self {
58            small: true,
59            medium: true,
60            large: true,
61        }
62    }
63
64    pub fn small(mut self) -> Self {
65        self.small = true;
66        self
67    }
68
69    pub fn medium(mut self) -> Self {
70        self.medium = true;
71        self
72    }
73
74    pub fn large(mut self) -> Self {
75        self.large = true;
76        self
77    }
78}
79
80#[derive(Clone, Debug)]
81pub struct Column<'a> {
82    pub text: Text<'a>,
83    pub width: Constraint,
84    pub skip: bool,
85    pub view: ColumnView,
86}
87
88impl<'a> Column<'a> {
89    pub fn new(text: impl Into<Text<'a>>, width: Constraint) -> Self {
90        Self {
91            text: text.into(),
92            width,
93            skip: false,
94            view: ColumnView::all(),
95        }
96    }
97
98    pub fn skip(mut self, skip: bool) -> Self {
99        self.skip = skip;
100        self
101    }
102
103    pub fn hide_small(mut self) -> Self {
104        self.view = ColumnView::default().medium().large();
105        self
106    }
107
108    pub fn hide_medium(mut self) -> Self {
109        self.view = ColumnView::default().large();
110        self
111    }
112
113    pub fn displayed(&self, area_width: usize) -> bool {
114        if area_width < RENDER_WIDTH_SMALL {
115            self.view.small
116        } else if area_width < RENDER_WIDTH_MEDIUM {
117            self.view.medium
118        } else if area_width < RENDER_WIDTH_LARGE {
119            self.view.large
120        } else {
121            true
122        }
123    }
124}
125
126#[derive(Default)]
127pub struct Window {}
128
129impl Window {
130    #[inline]
131    pub fn show<M, R>(
132        self,
133        ctx: &Context<M>,
134        theme: Theme,
135        add_contents: impl FnOnce(&mut Ui<M>) -> R,
136    ) -> Option<InnerResponse<Option<R>>>
137    where
138        M: Clone,
139    {
140        self.show_dyn(ctx, theme, Box::new(add_contents))
141    }
142
143    fn show_dyn<M, R>(
144        self,
145        ctx: &Context<M>,
146        theme: Theme,
147        add_contents: Box<AddContentFn<M, R>>,
148    ) -> Option<InnerResponse<Option<R>>>
149    where
150        M: Clone,
151    {
152        let mut ui = Ui::default()
153            .with_focus()
154            .with_area(ctx.frame_size())
155            .with_ctx(ctx.clone())
156            .with_layout(Layout::horizontal([Constraint::Min(1)]).into())
157            .with_area_focus(Some(0))
158            .with_theme(theme);
159
160        let inner = add_contents(&mut ui);
161
162        Some(InnerResponse::new(Some(inner), Response::default()))
163    }
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct ContainerState {
168    len: usize,
169    focus: Option<usize>,
170}
171
172impl ContainerState {
173    pub fn new(len: usize, focus: Option<usize>) -> Self {
174        Self { len, focus }
175    }
176
177    pub fn focus(&self) -> Option<usize> {
178        self.focus
179    }
180
181    pub fn len(&self) -> usize {
182        self.len
183    }
184
185    pub fn is_empty(&self) -> bool {
186        self.len == 0
187    }
188
189    pub fn focus_next(&mut self) -> bool {
190        let focus = self
191            .focus
192            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)));
193        let changed = focus != self.focus;
194        if changed {
195            self.focus = focus;
196        }
197        changed
198    }
199
200    pub fn focus_prev(&mut self) -> bool {
201        let focus = self.focus.map(|f| f.saturating_sub(1));
202        let changed = focus != self.focus;
203        if changed {
204            self.focus = focus;
205        }
206        changed
207    }
208
209    pub fn focus_index(&mut self, focus: usize) -> bool {
210        let focus = (focus < self.len).then_some(focus);
211        let changed = focus.is_some() && focus != self.focus;
212        if changed {
213            self.focus = focus;
214        }
215        changed
216    }
217}
218
219impl<'a> From<&Container<'a>> for ContainerState {
220    fn from(container: &Container<'a>) -> Self {
221        Self {
222            len: container.len,
223            focus: *container.focus,
224        }
225    }
226}
227
228pub struct Container<'a> {
229    focus: &'a mut Option<usize>,
230    len: usize,
231}
232
233impl<'a> Container<'a> {
234    pub fn new(len: usize, focus: &'a mut Option<usize>) -> Self {
235        Self { len, focus }
236    }
237
238    pub fn show<M, R>(
239        self,
240        ui: &mut Ui<M>,
241        add_contents: impl FnOnce(&mut Ui<M>) -> R,
242    ) -> InnerResponse<R>
243    where
244        M: Clone,
245    {
246        self.show_dyn(ui, Box::new(add_contents))
247    }
248
249    pub fn show_dyn<M, R>(
250        self,
251        ui: &mut Ui<M>,
252        add_contents: Box<AddContentFn<M, R>>,
253    ) -> InnerResponse<R>
254    where
255        M: Clone,
256    {
257        let mut response = Response::default();
258        let mut state = ContainerState::from(&self);
259
260        response.changed |= ui.has_global_input(|key| key == Key::Tab) && state.focus_next();
261        response.changed |= ui.has_global_input(|key| key == Key::BackTab) && state.focus_prev();
262        for index in 1..=self.len {
263            if let Some(c) = char::from_digit(index as u32, 10) {
264                response.changed |=
265                    ui.has_global_input(|key| key == Key::Char(c)) && state.focus_index(index - 1);
266            }
267        }
268        *self.focus = state.focus;
269
270        InnerResponse::new(
271            add_contents(&mut ui.clone().with_area_focus(state.focus)),
272            response,
273        )
274    }
275}
276
277#[derive(Default)]
278pub struct Popup {}
279
280impl Popup {
281    pub fn show<M, R>(
282        self,
283        ui: &mut Ui<M>,
284        add_contents: impl FnOnce(&mut Ui<M>) -> R,
285    ) -> InnerResponse<R>
286    where
287        M: Clone,
288    {
289        self.show_dyn(ui, Box::new(add_contents))
290    }
291
292    pub fn show_dyn<M, R>(
293        self,
294        ui: &mut Ui<M>,
295        add_contents: Box<AddContentFn<M, R>>,
296    ) -> InnerResponse<R>
297    where
298        M: Clone,
299    {
300        let inner = add_contents(ui);
301        InnerResponse::new(inner, Response::default())
302    }
303}
304
305pub struct Label<'a> {
306    content: Text<'a>,
307}
308
309impl<'a> Label<'a> {
310    pub fn new(content: impl Into<Text<'a>>) -> Self {
311        Self {
312            content: content.into(),
313        }
314    }
315}
316
317impl Widget for Label<'_> {
318    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response {
319        let (area, _) = ui.next_area().unwrap_or_default();
320        frame.render_widget(self.content, area);
321
322        Response::default()
323    }
324}
325
326#[derive(Clone, Debug, Serialize, Deserialize)]
327pub struct TableState {
328    internal: ratatui::widgets::TableState,
329}
330
331impl TableState {
332    pub fn new(selected: Option<usize>) -> Self {
333        let mut internal = ratatui::widgets::TableState::default();
334        internal.select(selected);
335
336        Self { internal }
337    }
338
339    pub fn selected(&self) -> Option<usize> {
340        self.internal.selected()
341    }
342
343    pub fn select_first(&mut self) {
344        self.internal.select(Some(0));
345    }
346}
347
348impl TableState {
349    fn prev(&mut self) -> Option<usize> {
350        let selected = self
351            .internal
352            .selected()
353            .map(|current| current.saturating_sub(1));
354        self.select(selected);
355        selected
356    }
357
358    fn next(&mut self, len: usize) -> Option<usize> {
359        let selected = self.internal.selected().map(|current| {
360            if current < len.saturating_sub(1) {
361                current.saturating_add(1)
362            } else {
363                current
364            }
365        });
366        self.select(selected);
367        selected
368    }
369
370    fn prev_page(&mut self, page_size: usize) -> Option<usize> {
371        let selected = self
372            .internal
373            .selected()
374            .map(|current| current.saturating_sub(page_size));
375        self.select(selected);
376        selected
377    }
378
379    fn next_page(&mut self, len: usize, page_size: usize) -> Option<usize> {
380        let selected = self.internal.selected().map(|current| {
381            if current < len.saturating_sub(1) {
382                cmp::min(current.saturating_add(page_size), len.saturating_sub(1))
383            } else {
384                current
385            }
386        });
387        self.select(selected);
388        selected
389    }
390
391    fn begin(&mut self) {
392        self.select(Some(0));
393    }
394
395    fn end(&mut self, len: usize) {
396        self.select(Some(len.saturating_sub(1)));
397    }
398
399    fn select(&mut self, selected: Option<usize>) {
400        self.internal.select(selected);
401    }
402}
403
404pub struct Table<'a, R, const W: usize> {
405    items: &'a Vec<R>,
406    selected: &'a mut Option<usize>,
407    columns: Vec<Column<'a>>,
408    spacing: Spacing,
409    borders: Option<Borders>,
410    show_scrollbar: bool,
411    empty_message: Option<String>,
412    dim: bool,
413}
414
415impl<'a, R, const W: usize> Table<'a, R, W>
416where
417    R: ToRow<W>,
418{
419    pub fn new(
420        selected: &'a mut Option<usize>,
421        items: &'a Vec<R>,
422        columns: Vec<Column<'a>>,
423        empty_message: Option<String>,
424        borders: Option<Borders>,
425    ) -> Self {
426        Self {
427            items,
428            selected,
429            columns,
430            spacing: Spacing::from(1),
431            empty_message,
432            borders,
433            show_scrollbar: true,
434            dim: false,
435        }
436    }
437
438    pub fn dim(mut self, dim: bool) -> Self {
439        self.dim = dim;
440        self
441    }
442
443    pub fn spacing(mut self, spacing: Spacing) -> Self {
444        self.spacing = spacing;
445        self
446    }
447}
448
449impl<R, const W: usize> Widget for Table<'_, R, W>
450where
451    R: ToRow<W> + Clone,
452{
453    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
454    where
455        M: Clone,
456    {
457        let mut response = Response::default();
458
459        let (area, area_focus) = ui.next_area().unwrap_or_default();
460
461        let show_scrollbar = self.show_scrollbar && self.items.len() >= area.height.into();
462        let has_items = !self.items.is_empty();
463
464        let mut state = TableState {
465            internal: {
466                let mut state = ratatui::widgets::TableState::default();
467                state.select(*self.selected);
468                state
469            },
470        };
471
472        let border_style = if ui.has_focus {
473            ui.theme.focus_border_style
474        } else {
475            ui.theme.border_style
476        };
477
478        let area = render_block(frame, area, self.borders, border_style);
479
480        if let Some(key) = ui.get_input(|_| true) {
481            let len = self.items.len();
482            let page_size = area.height as usize;
483
484            match key {
485                Key::Up | Key::Char('k') => {
486                    state.prev();
487                    response.changed = true;
488                }
489                Key::Down | Key::Char('j') => {
490                    state.next(len);
491                    response.changed = true;
492                }
493                Key::PageUp => {
494                    state.prev_page(page_size);
495                    response.changed = true;
496                }
497                Key::PageDown => {
498                    state.next_page(len, page_size);
499                    response.changed = true;
500                }
501                Key::Home => {
502                    state.begin();
503                    response.changed = true;
504                }
505                Key::End => {
506                    state.end(len);
507                    response.changed = true;
508                }
509                _ => {}
510            }
511        }
512
513        let widths: Vec<Constraint> = self
514            .columns
515            .iter()
516            .filter_map(|c| {
517                if !c.skip && c.displayed(area.width as usize) {
518                    Some(c.width)
519                } else {
520                    None
521                }
522            })
523            .collect();
524
525        if has_items {
526            let [table_area, scroller_area] =
527                Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).areas(area);
528
529            let rows = self
530                .items
531                .iter()
532                .map(|item| {
533                    let mut cells = vec![];
534                    let mut it = self.columns.iter();
535
536                    for cell in item.to_row() {
537                        if let Some(col) = it.next() {
538                            if !col.skip && col.displayed(table_area.width as usize) {
539                                cells.push(cell.clone())
540                            }
541                        } else {
542                            continue;
543                        }
544                    }
545
546                    Row::new(cells)
547                })
548                .collect::<Vec<_>>();
549
550            let table = ratatui::widgets::Table::default()
551                .rows(rows)
552                .widths(widths)
553                .column_spacing(self.spacing.into())
554                .row_highlight_style(ui.theme.highlight(ui.has_focus));
555
556            let table = if !area_focus && self.dim {
557                table.dim()
558            } else {
559                table
560            };
561
562            frame.render_stateful_widget(table, table_area, &mut state.internal);
563
564            if show_scrollbar {
565                let content_length = self.items.len();
566                let scroller = Scrollbar::default()
567                    .begin_symbol(None)
568                    .track_symbol(None)
569                    .end_symbol(None)
570                    .thumb_symbol("┃")
571                    .style(if ui.has_focus {
572                        ui.theme.focus_scroll_style
573                    } else {
574                        ui.theme.scroll_style
575                    });
576
577                let mut state = ScrollbarState::default()
578                    .content_length(content_length)
579                    .viewport_content_length(1)
580                    .position(state.internal.offset());
581
582                frame.render_stateful_widget(scroller, scroller_area, &mut state);
583            }
584        } else if let Some(message) = self.empty_message {
585            let center = layout::centered_rect(area, 50, 10);
586            let hint = Text::from(span::default(&message))
587                .centered()
588                .light_magenta()
589                .dim();
590
591            frame.render_widget(hint, center);
592        }
593
594        *self.selected = state.selected();
595
596        response
597    }
598}
599
600#[derive(Debug)]
601pub struct TreeState<Id>
602where
603    Id: ToString + Clone + Eq + Hash,
604{
605    pub internal: tui_tree_widget::TreeState<Id>,
606}
607
608impl<Id> Clone for TreeState<Id>
609where
610    Id: ToString + Clone + Eq + Hash,
611{
612    fn clone(&self) -> Self {
613        let mut state = tui_tree_widget::TreeState::default();
614        for path in self.internal.opened() {
615            state.open(path.to_vec());
616        }
617        state.select(self.internal.selected().to_vec());
618
619        Self { internal: state }
620    }
621}
622
623pub struct Tree<'a, R, Id>
624where
625    R: ToTree<Id> + Clone,
626    Id: ToString + Clone + Eq + Hash,
627{
628    /// Root items.
629    items: &'a Vec<R>,
630    /// Optional identifier set of opened items. If not `None`,
631    /// it will override the internal tree state.
632    opened: Option<HashSet<Vec<Id>>>,
633    /// Optional path to selected item, e.g. ["1.0", "1.0.1", "1.0.2"]. If not `None`,
634    /// it will override the internal tree state.
635    selected: &'a mut Option<Vec<Id>>,
636    /// If this widget should render its scrollbar. Default: `true`.
637    show_scrollbar: bool,
638    /// Set to `true` if the content style should be dimmed whenever the widget
639    /// has no focus.
640    dim: bool,
641    /// The borders to use.
642    borders: Option<Borders>,
643}
644
645impl<'a, R, Id> Tree<'a, R, Id>
646where
647    Id: ToString + Clone + Eq + Hash,
648    R: ToTree<Id> + Clone,
649{
650    pub fn new(
651        items: &'a Vec<R>,
652        opened: &'a Option<HashSet<Vec<Id>>>,
653        selected: &'a mut Option<Vec<Id>>,
654        borders: Option<Borders>,
655        dim: bool,
656    ) -> Self {
657        Self {
658            items,
659            selected,
660            opened: opened.clone(),
661            borders,
662            show_scrollbar: true,
663            dim,
664        }
665    }
666}
667
668impl<R, Id> Widget for Tree<'_, R, Id>
669where
670    R: ToTree<Id> + Clone,
671    Id: ToString + Clone + Eq + Hash,
672{
673    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
674    where
675        M: Clone,
676    {
677        let mut response = Response::default();
678        let mut state = TreeState {
679            internal: {
680                let mut state = tui_tree_widget::TreeState::default();
681
682                if let Some(opened) = &self.opened {
683                    if opened != state.opened() {
684                        state.close_all();
685                        for path in opened {
686                            state.open(path.to_vec());
687                        }
688                    }
689                }
690                if let Some(selected) = self.selected {
691                    state.select(selected.clone());
692                }
693                state
694            },
695        };
696
697        let mut items = vec![];
698        for item in self.items {
699            items.extend(item.rows());
700        }
701
702        let (area, area_focus) = ui.next_area().unwrap_or_default();
703        let border_style = if area_focus && ui.has_focus {
704            ui.theme.focus_border_style
705        } else {
706            ui.theme.border_style
707        };
708        let area = render_block(frame, area, self.borders, border_style);
709
710        let tree_style = if !area_focus && self.dim {
711            Style::default().dim()
712        } else {
713            Style::default()
714        };
715
716        let show_scrollbar = self.show_scrollbar && self.items.len() >= area.height.into();
717        let tree = if show_scrollbar {
718            tui_tree_widget::Tree::new(&items)
719                .expect("all item identifiers are unique")
720                .block(
721                    Block::default()
722                        .borders(ratatui::widgets::Borders::RIGHT)
723                        .border_set(border::Set {
724                            vertical_right: " ",
725                            ..Default::default()
726                        })
727                        .border_style(if area_focus {
728                            Style::default()
729                        } else {
730                            Style::default().dim()
731                        }),
732                )
733                .experimental_scrollbar(Some(
734                    Scrollbar::new(ScrollbarOrientation::VerticalRight)
735                        .begin_symbol(None)
736                        .track_symbol(None)
737                        .end_symbol(None)
738                        .thumb_symbol("┃")
739                        .style(if area_focus {
740                            ui.theme.focus_scroll_style
741                        } else {
742                            ui.theme.scroll_style
743                        }),
744                ))
745                .highlight_style(ui.theme.highlight(ui.has_focus))
746                .style(tree_style)
747        } else {
748            tui_tree_widget::Tree::new(&items)
749                .expect("all item identifiers are unique")
750                .style(tree_style)
751                .highlight_style(ui.theme.highlight(ui.has_focus))
752        };
753
754        frame.render_stateful_widget(tree, area, &mut state.internal);
755
756        if let Some(key) = ui.get_input(|_| true) {
757            match key {
758                Key::Up | Key::Char('k') => {
759                    state.internal.key_up();
760                    response.changed = true;
761                }
762                Key::Down | Key::Char('j') => {
763                    state.internal.key_down();
764                    response.changed = true;
765                }
766                Key::Left | Key::Char('h')
767                    if !state.internal.selected().is_empty()
768                        && !state.internal.opened().is_empty() =>
769                {
770                    state.internal.key_left();
771                    response.changed = true;
772                }
773                Key::Right | Key::Char('l') => {
774                    state.internal.key_right();
775                    response.changed = true;
776                }
777                _ => {}
778            }
779        }
780
781        *self.selected = Some(state.internal.selected().to_vec());
782
783        response
784    }
785}
786
787pub struct ColumnBar<'a> {
788    columns: Vec<Column<'a>>,
789    spacing: Spacing,
790    borders: Option<Borders>,
791}
792
793impl<'a> ColumnBar<'a> {
794    pub fn new(columns: Vec<Column<'a>>, spacing: Spacing, borders: Option<Borders>) -> Self {
795        Self {
796            columns,
797            spacing,
798            borders,
799        }
800    }
801}
802
803impl Widget for ColumnBar<'_> {
804    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
805    where
806        M: Clone,
807    {
808        let (area, _) = ui.next_area().unwrap_or_default();
809
810        let border_style = if ui.has_focus {
811            ui.theme.focus_border_style
812        } else {
813            ui.theme.border_style
814        };
815
816        let area = render_block(frame, area, self.borders, border_style);
817        let area = Rect {
818            width: area.width.saturating_sub(1),
819            ..area
820        };
821
822        let widths: Vec<Constraint> = self
823            .columns
824            .iter()
825            .filter_map(|c| {
826                if !c.skip && c.displayed(area.width as usize) {
827                    Some(c.width)
828                } else {
829                    None
830                }
831            })
832            .collect();
833
834        let cells = self
835            .columns
836            .iter()
837            .filter(|c| !c.skip && c.displayed(area.width as usize))
838            .map(|c| c.text.clone())
839            .collect::<Vec<_>>();
840
841        let table = ratatui::widgets::Table::default()
842            .column_spacing(self.spacing.into())
843            .rows([Row::new(cells)])
844            .widths(widths);
845        frame.render_widget(table, area);
846
847        Response::default()
848    }
849}
850
851pub struct Bar<'a> {
852    columns: Vec<Column<'a>>,
853    borders: Option<Borders>,
854}
855
856impl<'a> Bar<'a> {
857    pub fn new(columns: Vec<Column<'a>>, borders: Option<Borders>) -> Self {
858        Self { columns, borders }
859    }
860}
861
862impl Widget for Bar<'_> {
863    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
864    where
865        M: Clone,
866    {
867        let (area, area_focus) = ui.next_area().unwrap_or_default();
868
869        let border_style = if area_focus {
870            ui.theme.focus_border_style
871        } else {
872            ui.theme.border_style
873        };
874
875        let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
876        let cells = self
877            .columns
878            .iter()
879            .map(|c| c.text.clone())
880            .collect::<Vec<_>>();
881
882        let area = render_block(frame, area, self.borders, border_style);
883        let table = ratatui::widgets::Table::default()
884            .header(Row::new(cells))
885            .widths(widths)
886            .column_spacing(0);
887        frame.render_widget(table, area);
888
889        Response::default()
890    }
891}
892
893#[derive(Clone, Debug, Serialize, Deserialize)]
894pub struct TextViewState {
895    cursor: Position,
896}
897
898impl TextViewState {
899    pub fn new(cursor: Position) -> Self {
900        Self { cursor }
901    }
902
903    pub fn cursor(&self) -> Position {
904        self.cursor
905    }
906}
907
908impl TextViewState {
909    fn scroll_up(&mut self) {
910        self.cursor.x = self.cursor.x.saturating_sub(1);
911    }
912
913    fn scroll_down(&mut self, len: usize, page_size: usize) {
914        let end = len.saturating_sub(page_size);
915        self.cursor.x = std::cmp::min(self.cursor.x.saturating_add(1), end as u16);
916    }
917
918    fn scroll_left(&mut self) {
919        self.cursor.y = self.cursor.y.saturating_sub(3);
920    }
921
922    fn scroll_right(&mut self, max_line_length: usize) {
923        self.cursor.y = std::cmp::min(
924            self.cursor.y.saturating_add(3),
925            max_line_length.saturating_add(3) as u16,
926        );
927    }
928
929    fn prev_page(&mut self, page_size: usize) {
930        self.cursor.x = self.cursor.x.saturating_sub(page_size as u16);
931    }
932
933    fn next_page(&mut self, len: usize, page_size: usize) {
934        let end = len.saturating_sub(page_size);
935
936        self.cursor.x = std::cmp::min(self.cursor.x.saturating_add(page_size as u16), end as u16);
937    }
938
939    fn begin(&mut self) {
940        self.cursor.x = 0;
941    }
942
943    fn end(&mut self, len: usize, page_size: usize) {
944        self.cursor.x = len.saturating_sub(page_size) as u16;
945    }
946}
947
948pub struct TextView<'a> {
949    text: Text<'a>,
950    footer: Option<Text<'a>>,
951    borders: Option<Borders>,
952    cursor: &'a mut Position,
953}
954
955impl<'a> TextView<'a> {
956    pub fn new(
957        text: impl Into<Text<'a>>,
958        footer: Option<impl Into<Text<'a>>>,
959        cursor: &'a mut Position,
960        borders: Option<Borders>,
961    ) -> Self {
962        Self {
963            text: text.into(),
964            footer: footer.map(|f| f.into()),
965            borders,
966            cursor,
967        }
968    }
969}
970
971impl Widget for TextView<'_> {
972    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
973    where
974        M: Clone,
975    {
976        let mut response = Response::default();
977
978        let (area, area_focus) = ui.next_area().unwrap_or_default();
979
980        let show_scrollbar = true;
981        let border_style = if area_focus && ui.has_focus() {
982            ui.theme.focus_border_style
983        } else {
984            ui.theme.border_style
985        };
986        let length = self.text.lines.len();
987        let content_length = area.height as usize;
988
989        let area = render_block(frame, area, self.borders, border_style);
990        let area = Rect {
991            x: area.x.saturating_add(1),
992            width: area.width.saturating_sub(1),
993            ..area
994        };
995        let [text_area, scroller_area] = Layout::horizontal([
996            Constraint::Min(1),
997            if show_scrollbar {
998                Constraint::Length(1)
999            } else {
1000                Constraint::Length(0)
1001            },
1002        ])
1003        .areas(area);
1004        let [text_area, footer_area] = Layout::vertical([
1005            Constraint::Min(1),
1006            if self.footer.is_some() {
1007                Constraint::Length(1)
1008            } else {
1009                Constraint::Length(0)
1010            },
1011        ])
1012        .areas(text_area);
1013
1014        let scroller = Scrollbar::default()
1015            .begin_symbol(None)
1016            .track_symbol(None)
1017            .end_symbol(None)
1018            .thumb_symbol("┃")
1019            .style(if area_focus {
1020                ui.theme.focus_scroll_style
1021            } else {
1022                ui.theme.scroll_style
1023            });
1024
1025        let mut scroller_state = ScrollbarState::default()
1026            .content_length(length.saturating_sub(content_length))
1027            .viewport_content_length(1)
1028            .position(self.cursor.x as usize);
1029
1030        frame.render_stateful_widget(scroller, scroller_area, &mut scroller_state);
1031        frame.render_widget(
1032            Paragraph::new(self.text.clone()).scroll((self.cursor.x, self.cursor.y)),
1033            text_area,
1034        );
1035        if let Some(footer) = self.footer {
1036            frame.render_widget(Paragraph::new(footer.clone()), footer_area);
1037        }
1038
1039        let mut state = TextViewState::new(*self.cursor);
1040
1041        if let Some(key) = ui.get_input(|_| true) {
1042            let lines = self.text.lines.clone();
1043            let len = lines.clone().len();
1044            let max_line_len = lines
1045                .into_iter()
1046                .map(|l| l.to_string().chars().count())
1047                .max()
1048                .unwrap_or_default();
1049            let page_size = area.height as usize;
1050
1051            match key {
1052                Key::Up | Key::Char('k') => {
1053                    state.scroll_up();
1054                }
1055                Key::Down | Key::Char('j') => {
1056                    state.scroll_down(len, page_size);
1057                }
1058                Key::Left | Key::Char('h') => {
1059                    state.scroll_left();
1060                }
1061                Key::Right | Key::Char('l') => {
1062                    state.scroll_right(max_line_len.saturating_sub(area.height.into()));
1063                }
1064                Key::PageUp => {
1065                    state.prev_page(page_size);
1066                }
1067                Key::PageDown => {
1068                    state.next_page(len, page_size);
1069                }
1070                Key::Home => {
1071                    state.begin();
1072                }
1073                Key::End => {
1074                    state.end(len, page_size);
1075                }
1076                _ => {}
1077            }
1078            *self.cursor = state.cursor;
1079            response.changed = true;
1080        }
1081
1082        response
1083    }
1084}
1085
1086pub struct CenteredTextView<'a> {
1087    content: Text<'a>,
1088    borders: Option<Borders>,
1089}
1090
1091impl<'a> CenteredTextView<'a> {
1092    pub fn new(content: impl Into<Text<'a>>, borders: Option<Borders>) -> Self {
1093        Self {
1094            content: content.into(),
1095            borders,
1096        }
1097    }
1098}
1099
1100impl Widget for CenteredTextView<'_> {
1101    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response {
1102        let (area, area_focus) = ui.next_area().unwrap_or_default();
1103
1104        let border_style = if area_focus && ui.has_focus() {
1105            ui.theme.focus_border_style
1106        } else {
1107            ui.theme.border_style
1108        };
1109
1110        let area = render_block(frame, area, self.borders, border_style);
1111        let area = Rect {
1112            x: area.x.saturating_add(1),
1113            width: area.width.saturating_sub(1),
1114            ..area
1115        };
1116        let center = layout::centered_rect(area, 50, 10);
1117
1118        frame.render_widget(self.content.centered(), center);
1119
1120        Response::default()
1121    }
1122}
1123
1124#[derive(Clone, Debug, Serialize, Deserialize)]
1125pub struct TextEditState {
1126    pub text: String,
1127    pub cursor: usize,
1128}
1129
1130impl TextEditState {
1131    fn move_cursor_left(&mut self) {
1132        let cursor_moved_left = self.cursor.saturating_sub(1);
1133        self.cursor = self.clamp_cursor(cursor_moved_left);
1134    }
1135
1136    fn move_cursor_right(&mut self) {
1137        let cursor_moved_right = self.cursor.saturating_add(1);
1138        self.cursor = self.clamp_cursor(cursor_moved_right);
1139    }
1140
1141    fn enter_char(&mut self, new_char: char) {
1142        self.text = self.text.clone();
1143        self.text.insert(self.cursor, new_char);
1144        self.move_cursor_right();
1145    }
1146
1147    fn delete_char_right(&mut self) {
1148        self.text = self.text.clone();
1149
1150        // Method "remove" is not used on the saved text for deleting the selected char.
1151        // Reason: Using remove on String works on bytes instead of the chars.
1152        // Using remove would require special care because of char boundaries.
1153
1154        let current_index = self.cursor;
1155        let from_left_to_current_index = current_index;
1156
1157        // Getting all characters before the selected character.
1158        let before_char_to_delete = self.text.chars().take(from_left_to_current_index);
1159        // Getting all characters after selected character.
1160        let after_char_to_delete = self.text.chars().skip(current_index.saturating_add(1));
1161
1162        // Put all characters together except the selected one.
1163        // By leaving the selected one out, it is forgotten and therefore deleted.
1164        self.text = before_char_to_delete.chain(after_char_to_delete).collect();
1165    }
1166
1167    fn delete_char_left(&mut self) {
1168        self.text = self.text.clone();
1169
1170        let is_not_cursor_leftmost = self.cursor != 0;
1171        if is_not_cursor_leftmost {
1172            // Method "remove" is not used on the saved text for deleting the selected char.
1173            // Reason: Using remove on String works on bytes instead of the chars.
1174            // Using remove would require special care because of char boundaries.
1175
1176            let current_index = self.cursor;
1177            let from_left_to_current_index = current_index - 1;
1178
1179            // Getting all characters before the selected character.
1180            let before_char_to_delete = self.text.chars().take(from_left_to_current_index);
1181            // Getting all characters after selected character.
1182            let after_char_to_delete = self.text.chars().skip(current_index);
1183
1184            // Put all characters together except the selected one.
1185            // By leaving the selected one out, it is forgotten and therefore deleted.
1186            self.text = before_char_to_delete.chain(after_char_to_delete).collect();
1187
1188            self.move_cursor_left();
1189        }
1190    }
1191
1192    fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
1193        new_cursor_pos.clamp(0, self.text.len())
1194    }
1195}
1196
1197pub struct TextEditOutput {
1198    pub response: Response,
1199    pub state: TextEditState,
1200}
1201
1202pub struct TextEdit<'a> {
1203    text: &'a mut String,
1204    cursor: &'a mut usize,
1205    borders: Option<Borders>,
1206    label: Option<String>,
1207    inline_label: bool,
1208    show_cursor: bool,
1209    dim: bool,
1210}
1211
1212impl<'a> TextEdit<'a> {
1213    pub fn new(text: &'a mut String, cursor: &'a mut usize, borders: Option<Borders>) -> Self {
1214        Self {
1215            text,
1216            cursor,
1217            label: None,
1218            borders,
1219            inline_label: true,
1220            show_cursor: true,
1221            dim: true,
1222        }
1223    }
1224
1225    pub fn with_label(mut self, label: impl ToString) -> Self {
1226        self.label = Some(label.to_string());
1227        self
1228    }
1229}
1230
1231impl TextEdit<'_> {
1232    pub fn show<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> TextEditOutput
1233    where
1234        M: Clone,
1235    {
1236        let mut response = Response::default();
1237
1238        let (area, area_focus) = ui.next_area().unwrap_or_default();
1239
1240        let border_style = if area_focus && ui.has_focus() {
1241            ui.theme.focus_border_style
1242        } else {
1243            ui.theme.border_style
1244        };
1245
1246        let area = render_block(frame, area, self.borders, border_style);
1247
1248        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);
1249
1250        let mut state = TextEditState {
1251            text: self.text.to_string(),
1252            cursor: *self.cursor,
1253        };
1254
1255        let label_content = format!(" {} ", self.label.unwrap_or_default());
1256        let overline = String::from("▔").repeat(area.width as usize);
1257        let cursor_pos = *self.cursor as u16;
1258
1259        let (label, input, overline) = if !area_focus && self.dim {
1260            (
1261                Span::from(label_content.clone()).magenta().dim().reversed(),
1262                Span::from(state.text.clone()).reset().dim(),
1263                Span::raw(overline).magenta().dim(),
1264            )
1265        } else {
1266            (
1267                Span::from(label_content.clone()).magenta().reversed(),
1268                Span::from(state.text.clone()).reset(),
1269                Span::raw(overline).magenta(),
1270            )
1271        };
1272
1273        if self.inline_label {
1274            let top_layout = Layout::horizontal([
1275                Constraint::Length(label_content.chars().count() as u16),
1276                Constraint::Length(1),
1277                Constraint::Min(1),
1278            ])
1279            .split(layout[0]);
1280
1281            let overline = Line::from([overline].to_vec());
1282
1283            frame.render_widget(label, top_layout[0]);
1284            frame.render_widget(input, top_layout[2]);
1285            frame.render_widget(overline, layout[1]);
1286
1287            if self.show_cursor {
1288                let position = Position::new(top_layout[2].x + cursor_pos, top_layout[2].y);
1289                frame.set_cursor_position(position)
1290            }
1291        } else {
1292            let top = Line::from([input].to_vec());
1293            let bottom = Line::from([label, overline].to_vec());
1294
1295            frame.render_widget(top, layout[0]);
1296            frame.render_widget(bottom, layout[1]);
1297
1298            if self.show_cursor {
1299                let position = Position::new(area.x + cursor_pos, area.y);
1300                frame.set_cursor_position(position);
1301            }
1302        }
1303
1304        if let Some(key) = ui.get_input(|_| true) {
1305            match key {
1306                Key::Char(to_insert)
1307                    if (key != Key::Alt('\n'))
1308                        && (key != Key::Char('\n'))
1309                        && (key != Key::Ctrl('\n')) =>
1310                {
1311                    state.enter_char(to_insert);
1312                }
1313                Key::Backspace => {
1314                    state.delete_char_left();
1315                }
1316                Key::Delete => {
1317                    state.delete_char_right();
1318                }
1319                Key::Left => {
1320                    state.move_cursor_left();
1321                }
1322                Key::Right => {
1323                    state.move_cursor_right();
1324                }
1325                _ => {}
1326            }
1327            response.changed = true;
1328        }
1329
1330        *self.text = state.text.clone();
1331        *self.cursor = state.cursor;
1332
1333        TextEditOutput { response, state }
1334    }
1335}
1336
1337impl Widget for TextEdit<'_> {
1338    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
1339    where
1340        M: Clone,
1341    {
1342        self.show(ui, frame).response
1343    }
1344}
1345
1346pub struct Shortcuts {
1347    pub shortcuts: Vec<(String, String)>,
1348    pub divider: char,
1349    pub alignment: Alignment,
1350}
1351
1352impl Shortcuts {
1353    pub fn new(shortcuts: &[(&str, &str)], divider: char, alignment: Alignment) -> Self {
1354        Self {
1355            shortcuts: shortcuts
1356                .iter()
1357                .map(|(s, a)| (s.to_string(), a.to_string()))
1358                .collect(),
1359            divider,
1360            alignment,
1361        }
1362    }
1363}
1364
1365impl Widget for Shortcuts {
1366    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
1367    where
1368        M: Clone,
1369    {
1370        use ratatui::widgets::Table;
1371
1372        let (area, _) = ui.next_area().unwrap_or_default();
1373
1374        let mut shortcuts = self.shortcuts.iter().peekable();
1375        let mut row = vec![];
1376
1377        while let Some(shortcut) = shortcuts.next() {
1378            let short = Text::from(shortcut.0.clone())
1379                .style(ui.theme.shortcuts_keys_style)
1380                .bold();
1381            let long = Text::from(shortcut.1.clone()).style(ui.theme.shortcuts_action_style);
1382            let spacer = Text::from(String::new());
1383            let divider = Text::from(format!(" {} ", self.divider)).style(style::gray().dim());
1384
1385            row.push((shortcut.0.chars().count(), short));
1386            row.push((1, spacer));
1387            row.push((shortcut.1.chars().count(), long));
1388
1389            if shortcuts.peek().is_some() {
1390                row.push((3, divider));
1391            }
1392        }
1393
1394        let row_copy = row.clone();
1395        let row: Vec<Text<'_>> = row_copy
1396            .clone()
1397            .iter()
1398            .map(|(_, text)| text.clone())
1399            .collect();
1400        let widths: Vec<Constraint> = row_copy
1401            .clone()
1402            .iter()
1403            .map(|(width, _)| Constraint::Length(*width as u16))
1404            .collect();
1405
1406        let (row, widths) = match self.alignment {
1407            Alignment::Left => ([row.as_slice(), &[Text::from("")]].concat(), widths),
1408            Alignment::Center => (
1409                [&[Text::from("")], row.as_slice(), &[Text::from("")]].concat(),
1410                [
1411                    &[Constraint::Fill(1)],
1412                    widths.as_slice(),
1413                    &[Constraint::Fill(1)],
1414                ]
1415                .concat(),
1416            ),
1417            Alignment::Right => (
1418                [&[Text::from("")], row.as_slice()].concat(),
1419                [&[Constraint::Fill(1)], widths.as_slice()].concat(),
1420            ),
1421        };
1422
1423        let table = Table::new([Row::new(row)], widths).column_spacing(0);
1424
1425        frame.render_widget(table, area);
1426
1427        Response::default()
1428    }
1429}
1430
1431fn render_block(frame: &mut Frame, area: Rect, borders: Option<Borders>, style: Style) -> Rect {
1432    if let Some(border) = borders {
1433        match border {
1434            Borders::None => area,
1435            Borders::Spacer { top, left } => {
1436                let areas = Layout::horizontal([Constraint::Fill(1)])
1437                    .vertical_margin(top as u16)
1438                    .horizontal_margin(left as u16)
1439                    .split(area);
1440
1441                areas[0]
1442            }
1443            Borders::All => {
1444                let block = Block::default()
1445                    .border_style(style)
1446                    .border_type(BorderType::Rounded)
1447                    .borders(ratatui::widgets::Borders::ALL);
1448                frame.render_widget(block.clone(), area);
1449
1450                block.inner(area)
1451            }
1452            Borders::Top => {
1453                let block = HeaderBlock::default()
1454                    .border_style(style)
1455                    .border_type(BorderType::Rounded)
1456                    .borders(ratatui::widgets::Borders::ALL);
1457                frame.render_widget(block, area);
1458
1459                let areas = Layout::default()
1460                    .direction(Direction::Vertical)
1461                    .constraints(vec![Constraint::Min(1)])
1462                    .vertical_margin(1)
1463                    .horizontal_margin(1)
1464                    .split(area);
1465
1466                areas[0]
1467            }
1468            Borders::Sides => {
1469                let block = Block::default()
1470                    .border_style(style)
1471                    .border_type(BorderType::Rounded)
1472                    .borders(ratatui::widgets::Borders::LEFT | ratatui::widgets::Borders::RIGHT);
1473                frame.render_widget(block.clone(), area);
1474
1475                block.inner(area)
1476            }
1477            Borders::Bottom => {
1478                let areas = Layout::default()
1479                    .direction(Direction::Vertical)
1480                    .constraints(vec![Constraint::Min(1)])
1481                    .vertical_margin(1)
1482                    .horizontal_margin(1)
1483                    .split(area);
1484
1485                let footer_block = FooterBlock::default()
1486                    .border_style(style)
1487                    .block_type(FooterBlockType::Single { top: true });
1488                frame.render_widget(footer_block, area);
1489
1490                areas[0]
1491            }
1492            Borders::BottomSides => {
1493                let areas = Layout::default()
1494                    .direction(Direction::Vertical)
1495                    .constraints(vec![Constraint::Min(1)])
1496                    .horizontal_margin(1)
1497                    .split(area);
1498
1499                let footer_block = FooterBlock::default()
1500                    .border_style(style)
1501                    .block_type(FooterBlockType::Single { top: false });
1502                frame.render_widget(footer_block, area);
1503
1504                Rect {
1505                    height: areas[0].height.saturating_sub(1),
1506                    ..areas[0]
1507                }
1508            }
1509        }
1510    } else {
1511        area
1512    }
1513}