Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
radicle-tui src ui widget.rs
use std::cmp;
use std::collections::HashSet;
use std::hash::Hash;

use ratatui::symbols::border;
use serde::{Deserialize, Serialize};

use ratatui::layout::{Alignment, Direction, Layout, Position, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, BorderType, Row, Scrollbar, ScrollbarOrientation, ScrollbarState};
use ratatui::Frame;
use ratatui::{layout::Constraint, widgets::Paragraph};

use crate::event::Key;
use crate::ui::ext::{FooterBlock, FooterBlockType, HeaderBlock};
use crate::ui::layout::Spacing;
use crate::ui::theme::{style, Theme};
use crate::ui::ToRow;
use crate::ui::{layout, span, ToTree};

use super::{Context, InnerResponse, Response, Ui};

pub type AddContentFn<'a, M, R> = dyn FnOnce(&mut Ui<M>) -> R + 'a;

pub const RENDER_WIDTH_XSMALL: usize = 50;
pub const RENDER_WIDTH_SMALL: usize = 70;
pub const RENDER_WIDTH_MEDIUM: usize = 150;
pub const RENDER_WIDTH_LARGE: usize = usize::MAX;

pub trait Widget {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone;
}

/// `Borders` defines which borders should be drawn around a widget.
pub enum Borders {
    None,
    Spacer { top: usize, left: usize },
    All,
    Top,
    Sides,
    Bottom,
    BottomSides,
}

#[derive(Clone, Debug, Default)]
pub struct ColumnView {
    small: bool,
    medium: bool,
    large: bool,
}

impl ColumnView {
    pub fn all() -> Self {
        Self {
            small: true,
            medium: true,
            large: true,
        }
    }

    pub fn small(mut self) -> Self {
        self.small = true;
        self
    }

    pub fn medium(mut self) -> Self {
        self.medium = true;
        self
    }

    pub fn large(mut self) -> Self {
        self.large = true;
        self
    }
}

#[derive(Clone, Debug)]
pub struct Column<'a> {
    pub text: Text<'a>,
    pub width: Constraint,
    pub skip: bool,
    pub view: ColumnView,
}

impl<'a> Column<'a> {
    pub fn new(text: impl Into<Text<'a>>, width: Constraint) -> Self {
        Self {
            text: text.into(),
            width,
            skip: false,
            view: ColumnView::all(),
        }
    }

    pub fn skip(mut self, skip: bool) -> Self {
        self.skip = skip;
        self
    }

    pub fn hide_small(mut self) -> Self {
        self.view = ColumnView::default().medium().large();
        self
    }

    pub fn hide_medium(mut self) -> Self {
        self.view = ColumnView::default().large();
        self
    }

    pub fn displayed(&self, area_width: usize) -> bool {
        if area_width < RENDER_WIDTH_SMALL {
            self.view.small
        } else if area_width < RENDER_WIDTH_MEDIUM {
            self.view.medium
        } else if area_width < RENDER_WIDTH_LARGE {
            self.view.large
        } else {
            true
        }
    }
}

#[derive(Default)]
pub struct Window {}

impl Window {
    #[inline]
    pub fn show<M, R>(
        self,
        ctx: &Context<M>,
        theme: Theme,
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
    ) -> Option<InnerResponse<Option<R>>>
    where
        M: Clone,
    {
        self.show_dyn(ctx, theme, Box::new(add_contents))
    }

    fn show_dyn<M, R>(
        self,
        ctx: &Context<M>,
        theme: Theme,
        add_contents: Box<AddContentFn<M, R>>,
    ) -> Option<InnerResponse<Option<R>>>
    where
        M: Clone,
    {
        let mut ui = Ui::default()
            .with_focus()
            .with_area(ctx.frame_size())
            .with_ctx(ctx.clone())
            .with_layout(Layout::horizontal([Constraint::Min(1)]).into())
            .with_area_focus(Some(0))
            .with_theme(theme);

        let inner = add_contents(&mut ui);

        Some(InnerResponse::new(Some(inner), Response::default()))
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContainerState {
    len: usize,
    focus: Option<usize>,
}

impl ContainerState {
    pub fn new(len: usize, focus: Option<usize>) -> Self {
        Self { len, focus }
    }

    pub fn focus(&self) -> Option<usize> {
        self.focus
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn focus_next(&mut self) -> bool {
        let focus = self
            .focus
            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)));
        let changed = focus != self.focus;
        if changed {
            self.focus = focus;
        }
        changed
    }

    pub fn focus_prev(&mut self) -> bool {
        let focus = self.focus.map(|f| f.saturating_sub(1));
        let changed = focus != self.focus;
        if changed {
            self.focus = focus;
        }
        changed
    }

    pub fn focus_index(&mut self, focus: usize) -> bool {
        let focus = (focus < self.len).then_some(focus);
        let changed = focus.is_some() && focus != self.focus;
        if changed {
            self.focus = focus;
        }
        changed
    }
}

impl<'a> From<&Container<'a>> for ContainerState {
    fn from(container: &Container<'a>) -> Self {
        Self {
            len: container.len,
            focus: *container.focus,
        }
    }
}

pub struct Container<'a> {
    focus: &'a mut Option<usize>,
    len: usize,
}

impl<'a> Container<'a> {
    pub fn new(len: usize, focus: &'a mut Option<usize>) -> Self {
        Self { len, focus }
    }

    pub fn show<M, R>(
        self,
        ui: &mut Ui<M>,
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
    ) -> InnerResponse<R>
    where
        M: Clone,
    {
        self.show_dyn(ui, Box::new(add_contents))
    }

    pub fn show_dyn<M, R>(
        self,
        ui: &mut Ui<M>,
        add_contents: Box<AddContentFn<M, R>>,
    ) -> InnerResponse<R>
    where
        M: Clone,
    {
        let mut response = Response::default();
        let mut state = ContainerState::from(&self);

        response.changed |= ui.has_global_input(|key| key == Key::Tab) && state.focus_next();
        response.changed |= ui.has_global_input(|key| key == Key::BackTab) && state.focus_prev();
        for index in 1..=self.len {
            if let Some(c) = char::from_digit(index as u32, 10) {
                response.changed |=
                    ui.has_global_input(|key| key == Key::Char(c)) && state.focus_index(index - 1);
            }
        }
        *self.focus = state.focus;

        InnerResponse::new(
            add_contents(&mut ui.clone().with_area_focus(state.focus)),
            response,
        )
    }
}

#[derive(Default)]
pub struct Popup {}

impl Popup {
    pub fn show<M, R>(
        self,
        ui: &mut Ui<M>,
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
    ) -> InnerResponse<R>
    where
        M: Clone,
    {
        self.show_dyn(ui, Box::new(add_contents))
    }

    pub fn show_dyn<M, R>(
        self,
        ui: &mut Ui<M>,
        add_contents: Box<AddContentFn<M, R>>,
    ) -> InnerResponse<R>
    where
        M: Clone,
    {
        let inner = add_contents(ui);
        InnerResponse::new(inner, Response::default())
    }
}

pub struct Label<'a> {
    content: Text<'a>,
}

impl<'a> Label<'a> {
    pub fn new(content: impl Into<Text<'a>>) -> Self {
        Self {
            content: content.into(),
        }
    }
}

impl Widget for Label<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response {
        let (area, _) = ui.next_area().unwrap_or_default();
        frame.render_widget(self.content, area);

        Response::default()
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TableState {
    internal: ratatui::widgets::TableState,
}

impl TableState {
    pub fn new(selected: Option<usize>) -> Self {
        let mut internal = ratatui::widgets::TableState::default();
        internal.select(selected);

        Self { internal }
    }

    pub fn selected(&self) -> Option<usize> {
        self.internal.selected()
    }

    pub fn select_first(&mut self) {
        self.internal.select(Some(0));
    }
}

impl TableState {
    fn prev(&mut self) -> Option<usize> {
        let selected = self
            .internal
            .selected()
            .map(|current| current.saturating_sub(1));
        self.select(selected);
        selected
    }

    fn next(&mut self, len: usize) -> Option<usize> {
        let selected = self.internal.selected().map(|current| {
            if current < len.saturating_sub(1) {
                current.saturating_add(1)
            } else {
                current
            }
        });
        self.select(selected);
        selected
    }

    fn prev_page(&mut self, page_size: usize) -> Option<usize> {
        let selected = self
            .internal
            .selected()
            .map(|current| current.saturating_sub(page_size));
        self.select(selected);
        selected
    }

    fn next_page(&mut self, len: usize, page_size: usize) -> Option<usize> {
        let selected = self.internal.selected().map(|current| {
            if current < len.saturating_sub(1) {
                cmp::min(current.saturating_add(page_size), len.saturating_sub(1))
            } else {
                current
            }
        });
        self.select(selected);
        selected
    }

    fn begin(&mut self) {
        self.select(Some(0));
    }

    fn end(&mut self, len: usize) {
        self.select(Some(len.saturating_sub(1)));
    }

    fn select(&mut self, selected: Option<usize>) {
        self.internal.select(selected);
    }
}

pub struct Table<'a, R, const W: usize> {
    items: &'a Vec<R>,
    selected: &'a mut Option<usize>,
    columns: Vec<Column<'a>>,
    spacing: Spacing,
    borders: Option<Borders>,
    show_scrollbar: bool,
    empty_message: Option<String>,
    dim: bool,
}

impl<'a, R, const W: usize> Table<'a, R, W>
where
    R: ToRow<W>,
{
    pub fn new(
        selected: &'a mut Option<usize>,
        items: &'a Vec<R>,
        columns: Vec<Column<'a>>,
        empty_message: Option<String>,
        borders: Option<Borders>,
    ) -> Self {
        Self {
            items,
            selected,
            columns,
            spacing: Spacing::from(1),
            empty_message,
            borders,
            show_scrollbar: true,
            dim: false,
        }
    }

    pub fn dim(mut self, dim: bool) -> Self {
        self.dim = dim;
        self
    }

    pub fn spacing(mut self, spacing: Spacing) -> Self {
        self.spacing = spacing;
        self
    }
}

impl<R, const W: usize> Widget for Table<'_, R, W>
where
    R: ToRow<W> + Clone,
{
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        let mut response = Response::default();

        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let show_scrollbar = self.show_scrollbar && self.items.len() >= area.height.into();
        let has_items = !self.items.is_empty();

        let mut state = TableState {
            internal: {
                let mut state = ratatui::widgets::TableState::default();
                state.select(*self.selected);
                state
            },
        };

        let border_style = if ui.has_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };

        let area = render_block(frame, area, self.borders, border_style);

        if let Some(key) = ui.get_input(|_| true) {
            let len = self.items.len();
            let page_size = area.height as usize;

            match key {
                Key::Up | Key::Char('k') => {
                    state.prev();
                    response.changed = true;
                }
                Key::Down | Key::Char('j') => {
                    state.next(len);
                    response.changed = true;
                }
                Key::PageUp => {
                    state.prev_page(page_size);
                    response.changed = true;
                }
                Key::PageDown => {
                    state.next_page(len, page_size);
                    response.changed = true;
                }
                Key::Home => {
                    state.begin();
                    response.changed = true;
                }
                Key::End => {
                    state.end(len);
                    response.changed = true;
                }
                _ => {}
            }
        }

        let widths: Vec<Constraint> = self
            .columns
            .iter()
            .filter_map(|c| {
                if !c.skip && c.displayed(area.width as usize) {
                    Some(c.width)
                } else {
                    None
                }
            })
            .collect();

        if has_items {
            let [table_area, scroller_area] =
                Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).areas(area);

            let rows = self
                .items
                .iter()
                .map(|item| {
                    let mut cells = vec![];
                    let mut it = self.columns.iter();

                    for cell in item.to_row() {
                        if let Some(col) = it.next() {
                            if !col.skip && col.displayed(table_area.width as usize) {
                                cells.push(cell.clone())
                            }
                        } else {
                            continue;
                        }
                    }

                    Row::new(cells)
                })
                .collect::<Vec<_>>();

            let table = ratatui::widgets::Table::default()
                .rows(rows)
                .widths(widths)
                .column_spacing(self.spacing.into())
                .row_highlight_style(ui.theme.highlight(ui.has_focus));

            let table = if !area_focus && self.dim {
                table.dim()
            } else {
                table
            };

            frame.render_stateful_widget(table, table_area, &mut state.internal);

            if show_scrollbar {
                let content_length = self.items.len();
                let scroller = Scrollbar::default()
                    .begin_symbol(None)
                    .track_symbol(None)
                    .end_symbol(None)
                    .thumb_symbol("┃")
                    .style(if ui.has_focus {
                        ui.theme.focus_scroll_style
                    } else {
                        ui.theme.scroll_style
                    });

                let mut state = ScrollbarState::default()
                    .content_length(content_length)
                    .viewport_content_length(1)
                    .position(state.internal.offset());

                frame.render_stateful_widget(scroller, scroller_area, &mut state);
            }
        } else if let Some(message) = self.empty_message {
            let center = layout::centered_rect(area, 50, 10);
            let hint = Text::from(span::default(&message))
                .centered()
                .light_magenta()
                .dim();

            frame.render_widget(hint, center);
        }

        *self.selected = state.selected();

        response
    }
}

#[derive(Debug)]
pub struct TreeState<Id>
where
    Id: ToString + Clone + Eq + Hash,
{
    pub internal: tui_tree_widget::TreeState<Id>,
}

impl<Id> Clone for TreeState<Id>
where
    Id: ToString + Clone + Eq + Hash,
{
    fn clone(&self) -> Self {
        let mut state = tui_tree_widget::TreeState::default();
        for path in self.internal.opened() {
            state.open(path.to_vec());
        }
        state.select(self.internal.selected().to_vec());

        Self { internal: state }
    }
}

pub struct Tree<'a, R, Id>
where
    R: ToTree<Id> + Clone,
    Id: ToString + Clone + Eq + Hash,
{
    /// Root items.
    items: &'a Vec<R>,
    /// Optional identifier set of opened items. If not `None`,
    /// it will override the internal tree state.
    opened: Option<HashSet<Vec<Id>>>,
    /// Optional path to selected item, e.g. ["1.0", "1.0.1", "1.0.2"]. If not `None`,
    /// it will override the internal tree state.
    selected: &'a mut Option<Vec<Id>>,
    /// If this widget should render its scrollbar. Default: `true`.
    show_scrollbar: bool,
    /// Set to `true` if the content style should be dimmed whenever the widget
    /// has no focus.
    dim: bool,
    /// The borders to use.
    borders: Option<Borders>,
}

impl<'a, R, Id> Tree<'a, R, Id>
where
    Id: ToString + Clone + Eq + Hash,
    R: ToTree<Id> + Clone,
{
    pub fn new(
        items: &'a Vec<R>,
        opened: &'a Option<HashSet<Vec<Id>>>,
        selected: &'a mut Option<Vec<Id>>,
        borders: Option<Borders>,
        dim: bool,
    ) -> Self {
        Self {
            items,
            selected,
            opened: opened.clone(),
            borders,
            show_scrollbar: true,
            dim,
        }
    }
}

impl<R, Id> Widget for Tree<'_, R, Id>
where
    R: ToTree<Id> + Clone,
    Id: ToString + Clone + Eq + Hash,
{
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        let mut response = Response::default();
        let mut state = TreeState {
            internal: {
                let mut state = tui_tree_widget::TreeState::default();

                if let Some(opened) = &self.opened {
                    if opened != state.opened() {
                        state.close_all();
                        for path in opened {
                            state.open(path.to_vec());
                        }
                    }
                }
                if let Some(selected) = self.selected {
                    state.select(selected.clone());
                }
                state
            },
        };

        let mut items = vec![];
        for item in self.items {
            items.extend(item.rows());
        }

        let (area, area_focus) = ui.next_area().unwrap_or_default();
        let border_style = if area_focus && ui.has_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };
        let area = render_block(frame, area, self.borders, border_style);

        let tree_style = if !area_focus && self.dim {
            Style::default().dim()
        } else {
            Style::default()
        };

        let show_scrollbar = self.show_scrollbar && self.items.len() >= area.height.into();
        let tree = if show_scrollbar {
            tui_tree_widget::Tree::new(&items)
                .expect("all item identifiers are unique")
                .block(
                    Block::default()
                        .borders(ratatui::widgets::Borders::RIGHT)
                        .border_set(border::Set {
                            vertical_right: " ",
                            ..Default::default()
                        })
                        .border_style(if area_focus {
                            Style::default()
                        } else {
                            Style::default().dim()
                        }),
                )
                .experimental_scrollbar(Some(
                    Scrollbar::new(ScrollbarOrientation::VerticalRight)
                        .begin_symbol(None)
                        .track_symbol(None)
                        .end_symbol(None)
                        .thumb_symbol("┃")
                        .style(if area_focus {
                            ui.theme.focus_scroll_style
                        } else {
                            ui.theme.scroll_style
                        }),
                ))
                .highlight_style(ui.theme.highlight(ui.has_focus))
                .style(tree_style)
        } else {
            tui_tree_widget::Tree::new(&items)
                .expect("all item identifiers are unique")
                .style(tree_style)
                .highlight_style(ui.theme.highlight(ui.has_focus))
        };

        frame.render_stateful_widget(tree, area, &mut state.internal);

        if let Some(key) = ui.get_input(|_| true) {
            match key {
                Key::Up | Key::Char('k') => {
                    state.internal.key_up();
                    response.changed = true;
                }
                Key::Down | Key::Char('j') => {
                    state.internal.key_down();
                    response.changed = true;
                }
                Key::Left | Key::Char('h')
                    if !state.internal.selected().is_empty()
                        && !state.internal.opened().is_empty() =>
                {
                    state.internal.key_left();
                    response.changed = true;
                }
                Key::Right | Key::Char('l') => {
                    state.internal.key_right();
                    response.changed = true;
                }
                _ => {}
            }
        }

        *self.selected = Some(state.internal.selected().to_vec());

        response
    }
}

pub struct ColumnBar<'a> {
    columns: Vec<Column<'a>>,
    spacing: Spacing,
    borders: Option<Borders>,
}

impl<'a> ColumnBar<'a> {
    pub fn new(columns: Vec<Column<'a>>, spacing: Spacing, borders: Option<Borders>) -> Self {
        Self {
            columns,
            spacing,
            borders,
        }
    }
}

impl Widget for ColumnBar<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        let (area, _) = ui.next_area().unwrap_or_default();

        let border_style = if ui.has_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };

        let area = render_block(frame, area, self.borders, border_style);
        let area = Rect {
            width: area.width.saturating_sub(1),
            ..area
        };

        let widths: Vec<Constraint> = self
            .columns
            .iter()
            .filter_map(|c| {
                if !c.skip && c.displayed(area.width as usize) {
                    Some(c.width)
                } else {
                    None
                }
            })
            .collect();

        let cells = self
            .columns
            .iter()
            .filter(|c| !c.skip && c.displayed(area.width as usize))
            .map(|c| c.text.clone())
            .collect::<Vec<_>>();

        let table = ratatui::widgets::Table::default()
            .column_spacing(self.spacing.into())
            .rows([Row::new(cells)])
            .widths(widths);
        frame.render_widget(table, area);

        Response::default()
    }
}

pub struct Bar<'a> {
    columns: Vec<Column<'a>>,
    borders: Option<Borders>,
}

impl<'a> Bar<'a> {
    pub fn new(columns: Vec<Column<'a>>, borders: Option<Borders>) -> Self {
        Self { columns, borders }
    }
}

impl Widget for Bar<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let border_style = if area_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };

        let widths = self.columns.iter().map(|c| c.width).collect::<Vec<_>>();
        let cells = self
            .columns
            .iter()
            .map(|c| c.text.clone())
            .collect::<Vec<_>>();

        let area = render_block(frame, area, self.borders, border_style);
        let table = ratatui::widgets::Table::default()
            .header(Row::new(cells))
            .widths(widths)
            .column_spacing(0);
        frame.render_widget(table, area);

        Response::default()
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TextViewState {
    cursor: Position,
}

impl TextViewState {
    pub fn new(cursor: Position) -> Self {
        Self { cursor }
    }

    pub fn cursor(&self) -> Position {
        self.cursor
    }
}

impl TextViewState {
    fn scroll_up(&mut self) {
        self.cursor.x = self.cursor.x.saturating_sub(1);
    }

    fn scroll_down(&mut self, len: usize, page_size: usize) {
        let end = len.saturating_sub(page_size);
        self.cursor.x = std::cmp::min(self.cursor.x.saturating_add(1), end as u16);
    }

    fn scroll_left(&mut self) {
        self.cursor.y = self.cursor.y.saturating_sub(3);
    }

    fn scroll_right(&mut self, max_line_length: usize) {
        self.cursor.y = std::cmp::min(
            self.cursor.y.saturating_add(3),
            max_line_length.saturating_add(3) as u16,
        );
    }

    fn prev_page(&mut self, page_size: usize) {
        self.cursor.x = self.cursor.x.saturating_sub(page_size as u16);
    }

    fn next_page(&mut self, len: usize, page_size: usize) {
        let end = len.saturating_sub(page_size);

        self.cursor.x = std::cmp::min(self.cursor.x.saturating_add(page_size as u16), end as u16);
    }

    fn begin(&mut self) {
        self.cursor.x = 0;
    }

    fn end(&mut self, len: usize, page_size: usize) {
        self.cursor.x = len.saturating_sub(page_size) as u16;
    }
}

pub struct TextView<'a> {
    text: Text<'a>,
    footer: Option<Text<'a>>,
    borders: Option<Borders>,
    cursor: &'a mut Position,
}

impl<'a> TextView<'a> {
    pub fn new(
        text: impl Into<Text<'a>>,
        footer: Option<impl Into<Text<'a>>>,
        cursor: &'a mut Position,
        borders: Option<Borders>,
    ) -> Self {
        Self {
            text: text.into(),
            footer: footer.map(|f| f.into()),
            borders,
            cursor,
        }
    }
}

impl Widget for TextView<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        let mut response = Response::default();

        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let show_scrollbar = true;
        let border_style = if area_focus && ui.has_focus() {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };
        let length = self.text.lines.len();
        let content_length = area.height as usize;

        let area = render_block(frame, area, self.borders, border_style);
        let area = Rect {
            x: area.x.saturating_add(1),
            width: area.width.saturating_sub(1),
            ..area
        };
        let [text_area, scroller_area] = Layout::horizontal([
            Constraint::Min(1),
            if show_scrollbar {
                Constraint::Length(1)
            } else {
                Constraint::Length(0)
            },
        ])
        .areas(area);
        let [text_area, footer_area] = Layout::vertical([
            Constraint::Min(1),
            if self.footer.is_some() {
                Constraint::Length(1)
            } else {
                Constraint::Length(0)
            },
        ])
        .areas(text_area);

        let scroller = Scrollbar::default()
            .begin_symbol(None)
            .track_symbol(None)
            .end_symbol(None)
            .thumb_symbol("┃")
            .style(if area_focus {
                ui.theme.focus_scroll_style
            } else {
                ui.theme.scroll_style
            });

        let mut scroller_state = ScrollbarState::default()
            .content_length(length.saturating_sub(content_length))
            .viewport_content_length(1)
            .position(self.cursor.x as usize);

        frame.render_stateful_widget(scroller, scroller_area, &mut scroller_state);
        frame.render_widget(
            Paragraph::new(self.text.clone()).scroll((self.cursor.x, self.cursor.y)),
            text_area,
        );
        if let Some(footer) = self.footer {
            frame.render_widget(Paragraph::new(footer.clone()), footer_area);
        }

        let mut state = TextViewState::new(*self.cursor);

        if let Some(key) = ui.get_input(|_| true) {
            let lines = self.text.lines.clone();
            let len = lines.clone().len();
            let max_line_len = lines
                .into_iter()
                .map(|l| l.to_string().chars().count())
                .max()
                .unwrap_or_default();
            let page_size = area.height as usize;

            match key {
                Key::Up | Key::Char('k') => {
                    state.scroll_up();
                }
                Key::Down | Key::Char('j') => {
                    state.scroll_down(len, page_size);
                }
                Key::Left | Key::Char('h') => {
                    state.scroll_left();
                }
                Key::Right | Key::Char('l') => {
                    state.scroll_right(max_line_len.saturating_sub(area.height.into()));
                }
                Key::PageUp => {
                    state.prev_page(page_size);
                }
                Key::PageDown => {
                    state.next_page(len, page_size);
                }
                Key::Home => {
                    state.begin();
                }
                Key::End => {
                    state.end(len, page_size);
                }
                _ => {}
            }
            *self.cursor = state.cursor;
            response.changed = true;
        }

        response
    }
}

pub struct CenteredTextView<'a> {
    content: Text<'a>,
    borders: Option<Borders>,
}

impl<'a> CenteredTextView<'a> {
    pub fn new(content: impl Into<Text<'a>>, borders: Option<Borders>) -> Self {
        Self {
            content: content.into(),
            borders,
        }
    }
}

impl Widget for CenteredTextView<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response {
        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let border_style = if area_focus && ui.has_focus() {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };

        let area = render_block(frame, area, self.borders, border_style);
        let area = Rect {
            x: area.x.saturating_add(1),
            width: area.width.saturating_sub(1),
            ..area
        };
        let center = layout::centered_rect(area, 50, 10);

        frame.render_widget(self.content.centered(), center);

        Response::default()
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TextEditState {
    pub text: String,
    pub cursor: usize,
}

impl TextEditState {
    fn move_cursor_left(&mut self) {
        let cursor_moved_left = self.cursor.saturating_sub(1);
        self.cursor = self.clamp_cursor(cursor_moved_left);
    }

    fn move_cursor_right(&mut self) {
        let cursor_moved_right = self.cursor.saturating_add(1);
        self.cursor = self.clamp_cursor(cursor_moved_right);
    }

    fn enter_char(&mut self, new_char: char) {
        self.text = self.text.clone();
        self.text.insert(self.cursor, new_char);
        self.move_cursor_right();
    }

    fn delete_char_right(&mut self) {
        self.text = self.text.clone();

        // Method "remove" is not used on the saved text for deleting the selected char.
        // Reason: Using remove on String works on bytes instead of the chars.
        // Using remove would require special care because of char boundaries.

        let current_index = self.cursor;
        let from_left_to_current_index = current_index;

        // Getting all characters before the selected character.
        let before_char_to_delete = self.text.chars().take(from_left_to_current_index);
        // Getting all characters after selected character.
        let after_char_to_delete = self.text.chars().skip(current_index.saturating_add(1));

        // Put all characters together except the selected one.
        // By leaving the selected one out, it is forgotten and therefore deleted.
        self.text = before_char_to_delete.chain(after_char_to_delete).collect();
    }

    fn delete_char_left(&mut self) {
        self.text = self.text.clone();

        let is_not_cursor_leftmost = self.cursor != 0;
        if is_not_cursor_leftmost {
            // Method "remove" is not used on the saved text for deleting the selected char.
            // Reason: Using remove on String works on bytes instead of the chars.
            // Using remove would require special care because of char boundaries.

            let current_index = self.cursor;
            let from_left_to_current_index = current_index - 1;

            // Getting all characters before the selected character.
            let before_char_to_delete = self.text.chars().take(from_left_to_current_index);
            // Getting all characters after selected character.
            let after_char_to_delete = self.text.chars().skip(current_index);

            // Put all characters together except the selected one.
            // By leaving the selected one out, it is forgotten and therefore deleted.
            self.text = before_char_to_delete.chain(after_char_to_delete).collect();

            self.move_cursor_left();
        }
    }

    fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
        new_cursor_pos.clamp(0, self.text.len())
    }
}

pub struct TextEditOutput {
    pub response: Response,
    pub state: TextEditState,
}

pub struct TextEdit<'a> {
    text: &'a mut String,
    cursor: &'a mut usize,
    borders: Option<Borders>,
    label: Option<String>,
    inline_label: bool,
    show_cursor: bool,
    dim: bool,
}

impl<'a> TextEdit<'a> {
    pub fn new(text: &'a mut String, cursor: &'a mut usize, borders: Option<Borders>) -> Self {
        Self {
            text,
            cursor,
            label: None,
            borders,
            inline_label: true,
            show_cursor: true,
            dim: true,
        }
    }

    pub fn with_label(mut self, label: impl ToString) -> Self {
        self.label = Some(label.to_string());
        self
    }
}

impl TextEdit<'_> {
    pub fn show<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> TextEditOutput
    where
        M: Clone,
    {
        let mut response = Response::default();

        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let border_style = if area_focus && ui.has_focus() {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
        };

        let area = render_block(frame, area, self.borders, border_style);

        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);

        let mut state = TextEditState {
            text: self.text.to_string(),
            cursor: *self.cursor,
        };

        let label_content = format!(" {} ", self.label.unwrap_or_default());
        let overline = String::from("▔").repeat(area.width as usize);
        let cursor_pos = *self.cursor as u16;

        let (label, input, overline) = if !area_focus && self.dim {
            (
                Span::from(label_content.clone()).magenta().dim().reversed(),
                Span::from(state.text.clone()).reset().dim(),
                Span::raw(overline).magenta().dim(),
            )
        } else {
            (
                Span::from(label_content.clone()).magenta().reversed(),
                Span::from(state.text.clone()).reset(),
                Span::raw(overline).magenta(),
            )
        };

        if self.inline_label {
            let top_layout = Layout::horizontal([
                Constraint::Length(label_content.chars().count() as u16),
                Constraint::Length(1),
                Constraint::Min(1),
            ])
            .split(layout[0]);

            let overline = Line::from([overline].to_vec());

            frame.render_widget(label, top_layout[0]);
            frame.render_widget(input, top_layout[2]);
            frame.render_widget(overline, layout[1]);

            if self.show_cursor {
                let position = Position::new(top_layout[2].x + cursor_pos, top_layout[2].y);
                frame.set_cursor_position(position)
            }
        } else {
            let top = Line::from([input].to_vec());
            let bottom = Line::from([label, overline].to_vec());

            frame.render_widget(top, layout[0]);
            frame.render_widget(bottom, layout[1]);

            if self.show_cursor {
                let position = Position::new(area.x + cursor_pos, area.y);
                frame.set_cursor_position(position);
            }
        }

        if let Some(key) = ui.get_input(|_| true) {
            match key {
                Key::Char(to_insert)
                    if (key != Key::Alt('\n'))
                        && (key != Key::Char('\n'))
                        && (key != Key::Ctrl('\n')) =>
                {
                    state.enter_char(to_insert);
                }
                Key::Backspace => {
                    state.delete_char_left();
                }
                Key::Delete => {
                    state.delete_char_right();
                }
                Key::Left => {
                    state.move_cursor_left();
                }
                Key::Right => {
                    state.move_cursor_right();
                }
                _ => {}
            }
            response.changed = true;
        }

        *self.text = state.text.clone();
        *self.cursor = state.cursor;

        TextEditOutput { response, state }
    }
}

impl Widget for TextEdit<'_> {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        self.show(ui, frame).response
    }
}

pub struct Shortcuts {
    pub shortcuts: Vec<(String, String)>,
    pub divider: char,
    pub alignment: Alignment,
}

impl Shortcuts {
    pub fn new(shortcuts: &[(&str, &str)], divider: char, alignment: Alignment) -> Self {
        Self {
            shortcuts: shortcuts
                .iter()
                .map(|(s, a)| (s.to_string(), a.to_string()))
                .collect(),
            divider,
            alignment,
        }
    }
}

impl Widget for Shortcuts {
    fn ui<M>(self, ui: &mut Ui<M>, frame: &mut Frame) -> Response
    where
        M: Clone,
    {
        use ratatui::widgets::Table;

        let (area, _) = ui.next_area().unwrap_or_default();

        let mut shortcuts = self.shortcuts.iter().peekable();
        let mut row = vec![];

        while let Some(shortcut) = shortcuts.next() {
            let short = Text::from(shortcut.0.clone())
                .style(ui.theme.shortcuts_keys_style)
                .bold();
            let long = Text::from(shortcut.1.clone()).style(ui.theme.shortcuts_action_style);
            let spacer = Text::from(String::new());
            let divider = Text::from(format!(" {} ", self.divider)).style(style::gray().dim());

            row.push((shortcut.0.chars().count(), short));
            row.push((1, spacer));
            row.push((shortcut.1.chars().count(), long));

            if shortcuts.peek().is_some() {
                row.push((3, divider));
            }
        }

        let row_copy = row.clone();
        let row: Vec<Text<'_>> = row_copy
            .clone()
            .iter()
            .map(|(_, text)| text.clone())
            .collect();
        let widths: Vec<Constraint> = row_copy
            .clone()
            .iter()
            .map(|(width, _)| Constraint::Length(*width as u16))
            .collect();

        let (row, widths) = match self.alignment {
            Alignment::Left => ([row.as_slice(), &[Text::from("")]].concat(), widths),
            Alignment::Center => (
                [&[Text::from("")], row.as_slice(), &[Text::from("")]].concat(),
                [
                    &[Constraint::Fill(1)],
                    widths.as_slice(),
                    &[Constraint::Fill(1)],
                ]
                .concat(),
            ),
            Alignment::Right => (
                [&[Text::from("")], row.as_slice()].concat(),
                [&[Constraint::Fill(1)], widths.as_slice()].concat(),
            ),
        };

        let table = Table::new([Row::new(row)], widths).column_spacing(0);

        frame.render_widget(table, area);

        Response::default()
    }
}

fn render_block(frame: &mut Frame, area: Rect, borders: Option<Borders>, style: Style) -> Rect {
    if let Some(border) = borders {
        match border {
            Borders::None => area,
            Borders::Spacer { top, left } => {
                let areas = Layout::horizontal([Constraint::Fill(1)])
                    .vertical_margin(top as u16)
                    .horizontal_margin(left as u16)
                    .split(area);

                areas[0]
            }
            Borders::All => {
                let block = Block::default()
                    .border_style(style)
                    .border_type(BorderType::Rounded)
                    .borders(ratatui::widgets::Borders::ALL);
                frame.render_widget(block.clone(), area);

                block.inner(area)
            }
            Borders::Top => {
                let block = HeaderBlock::default()
                    .border_style(style)
                    .border_type(BorderType::Rounded)
                    .borders(ratatui::widgets::Borders::ALL);
                frame.render_widget(block, area);

                let areas = Layout::default()
                    .direction(Direction::Vertical)
                    .constraints(vec![Constraint::Min(1)])
                    .vertical_margin(1)
                    .horizontal_margin(1)
                    .split(area);

                areas[0]
            }
            Borders::Sides => {
                let block = Block::default()
                    .border_style(style)
                    .border_type(BorderType::Rounded)
                    .borders(ratatui::widgets::Borders::LEFT | ratatui::widgets::Borders::RIGHT);
                frame.render_widget(block.clone(), area);

                block.inner(area)
            }
            Borders::Bottom => {
                let areas = Layout::default()
                    .direction(Direction::Vertical)
                    .constraints(vec![Constraint::Min(1)])
                    .vertical_margin(1)
                    .horizontal_margin(1)
                    .split(area);

                let footer_block = FooterBlock::default()
                    .border_style(style)
                    .block_type(FooterBlockType::Single { top: true });
                frame.render_widget(footer_block, area);

                areas[0]
            }
            Borders::BottomSides => {
                let areas = Layout::default()
                    .direction(Direction::Vertical)
                    .constraints(vec![Constraint::Min(1)])
                    .horizontal_margin(1)
                    .split(area);

                let footer_block = FooterBlock::default()
                    .border_style(style)
                    .block_type(FooterBlockType::Single { top: false });
                frame.render_widget(footer_block, area);

                Rect {
                    height: areas[0].height.saturating_sub(1),
                    ..areas[0]
                }
            }
        }
    } else {
        area
    }
}