Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
lib/ui: Remove imUI specialization
Erik Kundt committed 3 months ago
commit 670f615620b80b64cab9711e3a27588b30f3c57a
parent 3e4e210
13 files changed +2320 -2342
modified bin/commands/inbox/list.rs
@@ -6,8 +6,7 @@ use std::vec;

use anyhow::Result;

-
use ratatui::layout::Constraint;
-
use ratatui::layout::Layout;
+
use ratatui::layout::{Constraint, Layout};
use ratatui::prelude::*;
use ratatui::text::Span;
use ratatui::{Frame, Viewport};
@@ -22,10 +21,9 @@ use radicle_tui as tui;
use tui::event::Key;
use tui::store;
use tui::task::{Process, Task};
-
use tui::ui::im;
-
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
-
use tui::ui::im::{Borders, Show};
-
use tui::ui::{BufferedValue, Column, Spacing};
+
use tui::ui;
+
use tui::ui::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
+
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, Ui};
use tui::{Channel, Exit};

use super::common::RepositoryMode;
@@ -262,7 +260,7 @@ impl store::Update<Message> for App {
}

impl Show<Message> for App {
-
    fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
+
    fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
        Window::default().show(ctx, |ui| {
            // Initialize
            if !self.state.initialized {
@@ -281,7 +279,7 @@ impl Show<Message> for App {
                            let mut group_focus = self.state.main_group.focus();

                            let group = ui.container(
-
                                im::Layout::Expandable3 { left_only: true },
+
                                ui::Layout::Expandable3 { left_only: true },
                                &mut group_focus,
                                |ui| {
                                    self.show_browser(frame, ui);
@@ -338,7 +336,7 @@ impl Show<Message> for App {
}

impl App {
-
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let context = self.context.lock().unwrap();
        let notifs = self.notifications.lock().unwrap();
        let notifs = notifs
@@ -415,14 +413,14 @@ impl App {
        }
    }

-
    fn show_browser_footer(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_browser_footer(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.layout(Layout::vertical([3, 1]), None, |ui| {
            self.show_browser_context(frame, ui);
            self.show_browser_shortcuts(frame, ui);
        });
    }

-
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let (mut search_text, mut search_cursor) = (
            self.state.search.clone().read().text,
            self.state.search.clone().read().cursor,
@@ -453,7 +451,7 @@ impl App {
        }
    }

-
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let context = {
            let notifs = self.notifications.lock().unwrap();
            let search = self.state.search.read().text;
@@ -554,7 +552,7 @@ impl App {
        ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
    }

-
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.shortcuts(
            frame,
            &[
@@ -569,7 +567,7 @@ impl App {
        );
    }

-
    fn show_loading_popup(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_loading_popup(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.popup(Layout::vertical([Constraint::Min(1)]), |ui| {
            ui.layout(
                Layout::vertical([Constraint::Min(1), Constraint::Length(3)]).margin(1),
@@ -599,7 +597,7 @@ impl App {
        });
    }

-
    fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
@@ -621,7 +619,7 @@ impl App {
        }
    }

-
    fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [
modified bin/commands/issue/list.rs
@@ -4,8 +4,6 @@ use std::sync::{Arc, Mutex};

use anyhow::{bail, Result};

-
use radicle_tui::ui::im::widget::TreeState;
-
use radicle_tui::ui::ToRow;
use ratatui::layout::{Alignment, Constraint, Layout, Position};
use ratatui::style::Stylize;
use ratatui::text::{Line, Span, Text};
@@ -21,11 +19,12 @@ use radicle_tui as tui;
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
-
use tui::ui::im;
-
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
-
use tui::ui::im::{Borders, Show};
-
use tui::ui::Column;
-
use tui::ui::{span, BufferedValue, Spacing};
+
use tui::ui;
+
use tui::ui::span;
+
use tui::ui::widget::{
+
    ContainerState, TableState, TextEditState, TextViewState, TreeState, Window,
+
};
+
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, ToRow, Ui};
use tui::{Channel, Exit};

use crate::cob::issue;
@@ -476,7 +475,7 @@ impl store::Update<Message> for App {
}

impl Show<Message> for App {
-
    fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
+
    fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
        Window::default().show(ctx, |ui| {
            match self.state.page.clone() {
                state::Page::Main => {
@@ -491,7 +490,7 @@ impl Show<Message> for App {
                                { (self.state.sections.focus(), self.state.sections.len()) };

                            let group = ui.container(
-
                                im::Layout::Expandable3 {
+
                                ui::Layout::Expandable3 {
                                    left_only: !self.state.preview.show,
                                },
                                &mut focus,
@@ -586,7 +585,7 @@ impl Show<Message> for App {
}

impl App {
-
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let issues = self.issues.lock().unwrap();
        let issues = issues
            .iter()
@@ -675,7 +674,7 @@ impl App {
        }
    }

-
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let mut search = self.state.browser.search.clone();
        let (mut search_text, mut search_cursor) =
            (search.clone().read().text, search.clone().read().cursor);
@@ -710,7 +709,7 @@ impl App {
        }
    }

-
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        use radicle::issue::{CloseReason, State};

        let context = {
@@ -809,7 +808,7 @@ impl App {
        ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
    }

-
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        use radicle::issue::State;

        let issues = self.issues.lock().unwrap();
@@ -839,7 +838,7 @@ impl App {
        );
    }

-
    pub fn show_issue(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_issue(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        #[derive(Clone)]
        struct Property<'a>(Span<'a>, Text<'a>);

@@ -997,7 +996,7 @@ impl App {
        );
    }

-
    pub fn show_issue_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_issue_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [
@@ -1018,7 +1017,7 @@ impl App {
        );
    }

-
    pub fn show_issue_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_issue_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let shortcuts = vec![("e", "edit"), ("c", "reply")];
        let global_shortcuts = vec![("p", "toggle preview"), ("?", "help")];

@@ -1032,7 +1031,7 @@ impl App {
        );
    }

-
    pub fn show_comment(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_comment(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let (text, footer, mut cursor) = {
            let comment = self.state.preview.selected_comment();
            let body: String = comment
@@ -1065,7 +1064,7 @@ impl App {
        }
    }

-
    pub fn show_comment_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_comment_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [
@@ -1086,7 +1085,7 @@ impl App {
        );
    }

-
    pub fn show_comment_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_comment_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let shortcuts = vec![("e", "edit"), ("c", "reply")];
        let global_shortcuts = vec![("p", "toggle preview"), ("?", "help")];

@@ -1100,7 +1099,7 @@ impl App {
        );
    }

-
    fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
@@ -1122,7 +1121,7 @@ impl App {
        }
    }

-
    fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [
modified bin/commands/patch/list.rs
@@ -3,6 +3,8 @@ use std::sync::{Arc, Mutex};

use anyhow::Result;

+
use serde::Serialize;
+

use radicle::patch::cache::Patches;
use radicle::patch::PatchId;
use radicle::storage::git::Repository;
@@ -15,15 +17,12 @@ use ratatui::{Frame, Viewport};

use radicle_tui as tui;

-
use serde::Serialize;
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
-
use tui::ui::im;
-
use tui::ui::im::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
-
use tui::ui::im::Borders;
-
use tui::ui::im::Show;
-
use tui::ui::{BufferedValue, Column, Spacing};
+
use tui::ui;
+
use tui::ui::widget::{ContainerState, TableState, TextEditState, TextViewState, Window};
+
use tui::ui::{Borders, BufferedValue, Column, Show, Spacing, Ui};
use tui::{Channel, Exit};

use crate::ui::items::filter::Filter;
@@ -238,7 +237,7 @@ impl store::Update<Message> for App {
}

impl Show<Message> for App {
-
    fn show(&self, ctx: &im::Context<Message>, frame: &mut Frame) -> Result<()> {
+
    fn show(&self, ctx: &ui::Context<Message>, frame: &mut Frame) -> Result<()> {
        Window::default().show(ctx, |ui| {
            match self.state.page {
                Page::Main => {
@@ -252,7 +251,7 @@ impl Show<Message> for App {
                            let mut group_focus = self.state.main_group.focus();

                            let group = ui.container(
-
                                im::Layout::Expandable3 { left_only: true },
+
                                ui::Layout::Expandable3 { left_only: true },
                                &mut group_focus,
                                |ui| {
                                    self.show_browser(frame, ui);
@@ -310,7 +309,7 @@ impl Show<Message> for App {
}

impl App {
-
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let patches = self.patches.lock().unwrap();
        let patches = patches
            .iter()
@@ -378,14 +377,14 @@ impl App {
        }
    }

-
    fn show_browser_footer(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_browser_footer(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.layout(Layout::vertical([1, 1]), None, |ui| {
            self.show_browser_context(frame, ui);
            self.show_browser_shortcuts(frame, ui);
        });
    }

-
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_search(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let (mut search_text, mut search_cursor) = (
            self.state.search.clone().read().text,
            self.state.search.clone().read().cursor,
@@ -416,7 +415,7 @@ impl App {
        }
    }

-
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_browser_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        let context = {
            let patches = self.patches.lock().unwrap();
            let search = self.state.search.read().text;
@@ -560,7 +559,7 @@ impl App {
        ui.column_bar(frame, context, Spacing::from(0), Some(Borders::None));
    }

-
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    pub fn show_browser_shortcuts(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.shortcuts(
            frame,
            &[
@@ -575,7 +574,7 @@ impl App {
        );
    }

-
    fn show_help_text(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_text(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [Column::new(Span::raw(" Help ").bold(), Constraint::Fill(1))].to_vec(),
@@ -597,7 +596,7 @@ impl App {
        }
    }

-
    fn show_help_context(&self, frame: &mut Frame, ui: &mut im::Ui<Message>) {
+
    fn show_help_context(&self, frame: &mut Frame, ui: &mut Ui<Message>) {
        ui.column_bar(
            frame,
            [
modified bin/commands/patch/review.rs
@@ -23,9 +23,9 @@ use radicle_tui as tui;
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
-
use tui::ui::im::widget::{ContainerState, TableState, TextViewState, Window};
-
use tui::ui::im::{Borders, Context, Show, Ui};
use tui::ui::span;
+
use tui::ui::widget::{ContainerState, TableState, TextViewState, Window};
+
use tui::ui::{Borders, Context, Show, Ui};
use tui::ui::{Column, Spacing};
use tui::{Channel, Exit};

modified bin/ui.rs
@@ -1,9 +1,20 @@
pub mod format;
-
pub mod im;
pub mod items;
pub mod layout;
pub mod span;

+
use radicle_tui::ui::Spacing;
+

+
use ratatui::layout::{Constraint, Layout};
+
use ratatui::Frame;
+

+
use radicle_tui as tui;
+

+
use tui::event::Key;
+
use tui::ui::widget::{TableState, TextEditState, Widget};
+
use tui::ui::{Borders, Response, Ui};
+
use tui::ui::{BufferedValue, Column, ToRow};
+

#[derive(Clone, Debug)]
pub struct TerminalInfo {
    pub luma: Option<f32>,
@@ -14,3 +25,180 @@ impl TerminalInfo {
        self.luma.unwrap_or_default() <= 0.6
    }
}
+

+
pub struct UiExt<'a, M>(&'a mut Ui<M>);
+

+
impl<'a, M> UiExt<'a, M> {
+
    pub fn new(ui: &'a mut Ui<M>) -> Self {
+
        Self(ui)
+
    }
+
}
+

+
impl<'a, M> From<&'a mut Ui<M>> for UiExt<'a, M> {
+
    fn from(ui: &'a mut Ui<M>) -> Self {
+
        Self::new(ui)
+
    }
+
}
+

+
#[allow(dead_code)]
+
impl<'a, M> UiExt<'a, M>
+
where
+
    M: Clone,
+
{
+
    #[allow(clippy::too_many_arguments)]
+
    pub fn browser<R, const W: usize>(
+
        &mut self,
+
        frame: &mut Frame,
+
        selected: &'a mut Option<usize>,
+
        items: &'a Vec<R>,
+
        header: impl IntoIterator<Item = Column<'a>>,
+
        footer: impl IntoIterator<Item = Column<'a>>,
+
        show_search: &'a mut bool,
+
        search: &'a mut BufferedValue<TextEditState>,
+
    ) -> Response
+
    where
+
        R: ToRow<W> + Clone,
+
    {
+
        Browser::<R, W>::new(selected, items, header, footer, show_search, search).ui(self.0, frame)
+
    }
+
}
+

+
#[allow(dead_code)]
+
#[derive(Clone, Debug)]
+
pub struct BrowserState {
+
    items: TableState,
+
    search: BufferedValue<TextEditState>,
+
    show_search: bool,
+
}
+

+
#[allow(dead_code)]
+
impl BrowserState {
+
    pub fn new(items: TableState, search: BufferedValue<TextEditState>, show_search: bool) -> Self {
+
        Self {
+
            items,
+
            search,
+
            show_search,
+
        }
+
    }
+

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

+
pub struct Browser<'a, R, const W: usize> {
+
    items: &'a Vec<R>,
+
    selected: &'a mut Option<usize>,
+
    header: Vec<Column<'a>>,
+
    footer: Vec<Column<'a>>,
+
    show_search: &'a mut bool,
+
    search: &'a mut BufferedValue<TextEditState>,
+
}
+

+
#[allow(dead_code)]
+
impl<'a, R, const W: usize> Browser<'a, R, W> {
+
    pub fn new(
+
        selected: &'a mut Option<usize>,
+
        items: &'a Vec<R>,
+
        header: impl IntoIterator<Item = Column<'a>>,
+
        footer: impl IntoIterator<Item = Column<'a>>,
+
        show_search: &'a mut bool,
+
        search: &'a mut BufferedValue<TextEditState>,
+
    ) -> Self {
+
        Self {
+
            items,
+
            selected,
+
            header: header.into_iter().collect(),
+
            footer: footer.into_iter().collect(),
+
            show_search,
+
            search,
+
        }
+
    }
+

+
    pub fn items(&self) -> &Vec<R> {
+
        self.items
+
    }
+
}
+

+
/// TODO(erikli): Implement `show` that returns an `InnerResponse` such that it can
+
/// used like a group.
+
impl<R, const W: usize> Widget for Browser<'_, 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 (mut text, mut cursor) = (self.search.read().text, self.search.read().cursor);
+

+
        ui.layout(
+
            Layout::vertical([
+
                Constraint::Length(3),
+
                Constraint::Min(1),
+
                Constraint::Length(if *self.show_search { 2 } else { 3 }),
+
            ]),
+
            Some(1),
+
            |ui| {
+
                ui.column_bar(
+
                    frame,
+
                    self.header.clone().to_vec(),
+
                    Spacing::default(),
+
                    Some(Borders::Top),
+
                );
+

+
                let table = ui.table(
+
                    frame,
+
                    self.selected,
+
                    self.items,
+
                    self.header.to_vec(),
+
                    None,
+
                    Spacing::from(1),
+
                    if *self.show_search {
+
                        Some(Borders::BottomSides)
+
                    } else {
+
                        Some(Borders::Sides)
+
                    },
+
                );
+
                response.changed |= table.changed;
+

+
                if *self.show_search {
+
                    let text_edit = ui.text_edit_singleline(
+
                        frame,
+
                        &mut text,
+
                        &mut cursor,
+
                        Some("Search".to_string()),
+
                        Some(Borders::Spacer { top: 0, left: 1 }),
+
                    );
+
                    self.search.write(TextEditState { text, cursor });
+
                    response.changed |= text_edit.changed;
+
                } else {
+
                    ui.column_bar(
+
                        frame,
+
                        self.footer.clone().to_vec(),
+
                        Spacing::from(0),
+
                        Some(Borders::Bottom),
+
                    );
+
                }
+
            },
+
        );
+

+
        if !*self.show_search {
+
            if ui.has_input(|key| key == Key::Char('/')) {
+
                *self.show_search = true;
+
            }
+
        } else {
+
            if ui.has_input(|key| key == Key::Esc) {
+
                *self.show_search = false;
+
                self.search.reset();
+
            }
+
            if ui.has_input(|key| key == Key::Enter) {
+
                *self.show_search = false;
+
                self.search.apply();
+
            }
+
        }
+

+
        response
+
    }
+
}
deleted bin/ui/im.rs
@@ -1,188 +0,0 @@
-
use radicle_tui::ui::Spacing;
-

-
use ratatui::layout::{Constraint, Layout};
-
use ratatui::Frame;
-

-
use radicle_tui as tui;
-

-
use tui::event::Key;
-
use tui::ui::im::widget::{TableState, TextEditState, Widget};
-
use tui::ui::im::{Borders, Response, Ui};
-
use tui::ui::{BufferedValue, Column, ToRow};
-

-
pub struct UiExt<'a, M>(&'a mut Ui<M>);
-

-
impl<'a, M> UiExt<'a, M> {
-
    pub fn new(ui: &'a mut Ui<M>) -> Self {
-
        Self(ui)
-
    }
-
}
-

-
impl<'a, M> From<&'a mut Ui<M>> for UiExt<'a, M> {
-
    fn from(ui: &'a mut Ui<M>) -> Self {
-
        Self::new(ui)
-
    }
-
}
-

-
#[allow(dead_code)]
-
impl<'a, M> UiExt<'a, M>
-
where
-
    M: Clone,
-
{
-
    #[allow(clippy::too_many_arguments)]
-
    pub fn browser<R, const W: usize>(
-
        &mut self,
-
        frame: &mut Frame,
-
        selected: &'a mut Option<usize>,
-
        items: &'a Vec<R>,
-
        header: impl IntoIterator<Item = Column<'a>>,
-
        footer: impl IntoIterator<Item = Column<'a>>,
-
        show_search: &'a mut bool,
-
        search: &'a mut BufferedValue<TextEditState>,
-
    ) -> Response
-
    where
-
        R: ToRow<W> + Clone,
-
    {
-
        Browser::<R, W>::new(selected, items, header, footer, show_search, search).ui(self.0, frame)
-
    }
-
}
-

-
#[allow(dead_code)]
-
#[derive(Clone, Debug)]
-
pub struct BrowserState {
-
    items: TableState,
-
    search: BufferedValue<TextEditState>,
-
    show_search: bool,
-
}
-

-
#[allow(dead_code)]
-
impl BrowserState {
-
    pub fn new(items: TableState, search: BufferedValue<TextEditState>, show_search: bool) -> Self {
-
        Self {
-
            items,
-
            search,
-
            show_search,
-
        }
-
    }
-

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

-
pub struct Browser<'a, R, const W: usize> {
-
    items: &'a Vec<R>,
-
    selected: &'a mut Option<usize>,
-
    header: Vec<Column<'a>>,
-
    footer: Vec<Column<'a>>,
-
    show_search: &'a mut bool,
-
    search: &'a mut BufferedValue<TextEditState>,
-
}
-

-
#[allow(dead_code)]
-
impl<'a, R, const W: usize> Browser<'a, R, W> {
-
    pub fn new(
-
        selected: &'a mut Option<usize>,
-
        items: &'a Vec<R>,
-
        header: impl IntoIterator<Item = Column<'a>>,
-
        footer: impl IntoIterator<Item = Column<'a>>,
-
        show_search: &'a mut bool,
-
        search: &'a mut BufferedValue<TextEditState>,
-
    ) -> Self {
-
        Self {
-
            items,
-
            selected,
-
            header: header.into_iter().collect(),
-
            footer: footer.into_iter().collect(),
-
            show_search,
-
            search,
-
        }
-
    }
-

-
    pub fn items(&self) -> &Vec<R> {
-
        self.items
-
    }
-
}
-

-
/// TODO(erikli): Implement `show` that returns an `InnerResponse` such that it can
-
/// used like a group.
-
impl<R, const W: usize> Widget for Browser<'_, 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 (mut text, mut cursor) = (self.search.read().text, self.search.read().cursor);
-

-
        ui.layout(
-
            Layout::vertical([
-
                Constraint::Length(3),
-
                Constraint::Min(1),
-
                Constraint::Length(if *self.show_search { 2 } else { 3 }),
-
            ]),
-
            Some(1),
-
            |ui| {
-
                ui.column_bar(
-
                    frame,
-
                    self.header.clone().to_vec(),
-
                    Spacing::default(),
-
                    Some(Borders::Top),
-
                );
-

-
                let table = ui.table(
-
                    frame,
-
                    self.selected,
-
                    self.items,
-
                    self.header.to_vec(),
-
                    None,
-
                    Spacing::from(1),
-
                    if *self.show_search {
-
                        Some(Borders::BottomSides)
-
                    } else {
-
                        Some(Borders::Sides)
-
                    },
-
                );
-
                response.changed |= table.changed;
-

-
                if *self.show_search {
-
                    let text_edit = ui.text_edit_singleline(
-
                        frame,
-
                        &mut text,
-
                        &mut cursor,
-
                        Some("Search".to_string()),
-
                        Some(Borders::Spacer { top: 0, left: 1 }),
-
                    );
-
                    self.search.write(TextEditState { text, cursor });
-
                    response.changed |= text_edit.changed;
-
                } else {
-
                    ui.column_bar(
-
                        frame,
-
                        self.footer.clone().to_vec(),
-
                        Spacing::from(0),
-
                        Some(Borders::Bottom),
-
                    );
-
                }
-
            },
-
        );
-

-
        if !*self.show_search {
-
            if ui.has_input(|key| key == Key::Char('/')) {
-
                *self.show_search = true;
-
            }
-
        } else {
-
            if ui.has_input(|key| key == Key::Esc) {
-
                *self.show_search = false;
-
                self.search.reset();
-
            }
-
            if ui.has_input(|key| key == Key::Enter) {
-
                *self.show_search = false;
-
                self.search.apply();
-
            }
-
        }
-

-
        response
-
    }
-
}
modified examples/hello.rs
@@ -9,9 +9,8 @@ use radicle_tui as tui;
use tui::event::Key;
use tui::store;
use tui::task::EmptyProcessors;
-
use tui::ui::im::widget::Window;
-
use tui::ui::im::Show;
-
use tui::ui::im::{Borders, Context};
+
use tui::ui::widget::Window;
+
use tui::ui::{Borders, Context, Show};
use tui::{Channel, Exit};

const ALIEN: &str = r#"
modified examples/selection.rs
@@ -11,17 +11,12 @@ use ratatui::{Frame, Viewport};
use radicle_tui as tui;

use tui::event::Key;
+
use tui::store::Update;
use tui::task::EmptyProcessors;
-
use tui::ui::im::widget::Window;
-
use tui::ui::im::{Borders, Context};
-
use tui::ui::Spacing;
-
use tui::ui::{Column, ToRow};
+
use tui::ui::widget::{TableState, Window};
+
use tui::ui::{Borders, Column, Context, Show, Spacing, ToRow};
use tui::Channel;
-
use tui::{
-
    store::Update,
-
    ui::im::{widget::TableState, Show},
-
    Exit,
-
};
+
use tui::Exit;

#[derive(Clone, Debug)]
struct Item {
modified src/lib.rs
@@ -20,8 +20,7 @@ use ratatui::Viewport;

use store::Update;
use terminal::StdinReader;
-
use ui::im;
-
use ui::im::Show;
+
use ui::{Frontend, Show};

use crate::task::Process;

@@ -176,7 +175,7 @@ where

    let store = store::Store::<S, M, R>::new(state_tx.clone());
    let worker = task::Worker::<T, M, R>::new(work_tx.clone());
-
    let frontend = im::Frontend::default();
+
    let frontend = Frontend::default();
    let stdin_reader = StdinReader::default();

    // TODO(erikli): Handle errors
modified src/ui.rs
@@ -1,21 +1,674 @@
pub mod ext;
-
pub mod im;
pub mod layout;
pub mod span;
pub mod theme;
pub mod utils;
+
pub mod widget;

-
use ratatui::layout::Constraint;
-
use ratatui::text::Text;
+
use std::collections::{HashSet, VecDeque};
+
use std::hash::Hash;
+
use std::rc::Rc;
+
use std::time::Duration;
+

+
use anyhow::Result;
+

+
use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect};
+
use ratatui::prelude::*;
+
use ratatui::text::{Span, Text};
use ratatui::widgets::Cell;
+
use ratatui::{Frame, Viewport};
+

+
use tokio::sync::broadcast;
+
use tokio::sync::mpsc::UnboundedReceiver;

use tui_tree_widget::TreeItem;

+
use crate::event::{Event, Key};
+
use crate::store::Update;
+
use crate::terminal::Terminal;
+
use crate::ui::theme::Theme;
+
use crate::ui::widget::{AddContentFn, Widget};
+
use crate::{Interrupted, Share};
+

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;

+
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
+

+
/// The main UI trait for the ability to render an application.
+
pub trait Show<M> {
+
    fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
+
}
+

+
#[derive(Default)]
+
pub struct Frontend {}
+

+
impl Frontend {
+
    pub async fn run<S, M, R>(
+
        self,
+
        message_tx: broadcast::Sender<M>,
+
        mut state_rx: UnboundedReceiver<S>,
+
        mut event_rx: UnboundedReceiver<Event>,
+
        mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
+
        viewport: Viewport,
+
    ) -> anyhow::Result<Interrupted<R>>
+
    where
+
        S: Update<M, Return = R> + Show<M>,
+
        M: Share,
+
        R: Share,
+
    {
+
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
+
        let mut terminal = Terminal::try_from(viewport)?;
+

+
        let mut state = state_rx.recv().await.unwrap();
+
        let mut ctx = Context::default().with_sender(message_tx);
+

+
        let result: anyhow::Result<Interrupted<R>> = loop {
+
            tokio::select! {
+
                // Tick to terminate the select every N milliseconds
+
                _ = ticker.tick() => (),
+
                // Handle input events
+
                Some(event) = event_rx.recv() => {
+
                    match event {
+
                        Event::Key(key) => {
+
                            log::debug!(target: "frontend", "Received key event: {key:?}");
+
                            ctx.store_input(event)
+
                        }
+
                        Event::Resize(x, y) => {
+
                            log::debug!(target: "frontend", "Received resize event: {x},{y}");
+
                            terminal.clear()?;
+
                        },
+
                        Event::Unknown => {
+
                            log::debug!(target: "frontend", "Received unknown event")
+
                        }
+
                    }
+
                },
+
                // Handle state updates
+
                Some(s) = state_rx.recv() => {
+
                    state = s;
+
                },
+
                // Catch and handle interrupt signal to gracefully shutdown
+
                Ok(interrupted) = interrupt_rx.recv() => {
+
                    break Ok(interrupted);
+
                }
+
            }
+
            terminal.draw(|frame| {
+
                let ctx = ctx.clone().with_frame_size(frame.area());
+

+
                if let Err(err) = state.show(&ctx, frame) {
+
                    log::warn!("Drawing failed: {err}");
+
                }
+
            })?;
+

+
            ctx.clear_inputs();
+
        };
+
        terminal.restore()?;
+

+
        result
+
    }
+
}
+

+
#[derive(Default, Debug)]
+
pub struct Response {
+
    pub changed: bool,
+
}
+

+
#[derive(Debug)]
+
pub struct InnerResponse<R> {
+
    /// What the user closure returned.
+
    pub inner: R,
+
    /// The response of the area.
+
    pub response: Response,
+
}
+

+
impl<R> InnerResponse<R> {
+
    #[inline]
+
    pub fn new(inner: R, response: Response) -> Self {
+
        Self { inner, response }
+
    }
+
}
+

+
/// A `Context` is held by the `Ui` and reflects the environment a `Ui` runs in.
+
#[derive(Clone, Debug)]
+
pub struct Context<M> {
+
    /// Currently captured user inputs. Inputs that where stored via `store_input`
+
    /// need to be cleared manually via `clear_inputs` (usually for each frame drawn).
+
    inputs: VecDeque<Event>,
+
    /// Current frame of the application.
+
    pub(crate) frame_size: Rect,
+
    /// The message sender used by the `Ui` to send application messages.
+
    pub(crate) sender: Option<broadcast::Sender<M>>,
+
}
+

+
impl<M> Default for Context<M> {
+
    fn default() -> Self {
+
        Self {
+
            inputs: VecDeque::default(),
+
            frame_size: Rect::default(),
+
            sender: None,
+
        }
+
    }
+
}
+

+
impl<M> Context<M> {
+
    pub fn new(frame_size: Rect) -> Self {
+
        Self {
+
            frame_size,
+
            ..Default::default()
+
        }
+
    }
+

+
    pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
+
        self.inputs = inputs;
+
        self
+
    }
+

+
    pub fn with_frame_size(mut self, frame_size: Rect) -> Self {
+
        self.frame_size = frame_size;
+
        self
+
    }
+

+
    pub fn with_sender(mut self, sender: broadcast::Sender<M>) -> Self {
+
        self.sender = Some(sender);
+
        self
+
    }
+

+
    pub fn frame_size(&self) -> Rect {
+
        self.frame_size
+
    }
+

+
    pub fn store_input(&mut self, event: Event) {
+
        self.inputs.push_back(event);
+
    }
+

+
    pub fn clear_inputs(&mut self) {
+
        self.inputs.clear();
+
    }
+
}
+

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

+
/// A `Layout` is used to support pre-defined layouts. It either represents
+
/// such a predefined layout or a wrapped `ratatui` layout. It's used internally
+
/// but can be build from a `ratatui` layout.
+
#[derive(Clone, Default, Debug)]
+
pub enum Layout {
+
    #[default]
+
    None,
+
    Wrapped {
+
        internal: ratatui::layout::Layout,
+
    },
+
    Expandable3 {
+
        left_only: bool,
+
    },
+
    Popup {
+
        percent_x: u16,
+
        percent_y: u16,
+
    },
+
}
+

+
impl From<ratatui::layout::Layout> for Layout {
+
    fn from(layout: ratatui::layout::Layout) -> Self {
+
        Layout::Wrapped { internal: layout }
+
    }
+
}
+

+
impl Layout {
+
    pub fn len(&self) -> usize {
+
        match self {
+
            Layout::None => 0,
+
            Layout::Wrapped { internal } => internal.split(Rect::default()).len(),
+
            Layout::Expandable3 { left_only } => {
+
                if *left_only {
+
                    1
+
                } else {
+
                    3
+
                }
+
            }
+
            Layout::Popup {
+
                percent_x: _,
+
                percent_y: _,
+
            } => 1,
+
        }
+
    }
+

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

+
    pub fn split(&self, area: Rect) -> Rc<[Rect]> {
+
        match self {
+
            Layout::None => Rc::new([]),
+
            Layout::Wrapped { internal } => internal.split(area),
+
            Layout::Expandable3 { left_only } => {
+
                use ratatui::layout::Layout;
+

+
                if *left_only {
+
                    [area].into()
+
                } else if area.width <= 140 {
+
                    let [left, right] = Layout::horizontal([
+
                        Constraint::Percentage(50),
+
                        Constraint::Percentage(50),
+
                    ])
+
                    .areas(area);
+
                    let [right_top, right_bottom] =
+
                        Layout::vertical([Constraint::Percentage(65), Constraint::Percentage(35)])
+
                            .areas(right);
+

+
                    [left, right_top, right_bottom].into()
+
                } else {
+
                    Layout::horizontal([
+
                        Constraint::Percentage(33),
+
                        Constraint::Percentage(33),
+
                        Constraint::Percentage(33),
+
                    ])
+
                    .split(area)
+
                }
+
            }
+
            Layout::Popup {
+
                percent_x,
+
                percent_y,
+
            } => {
+
                use ratatui::layout::Layout;
+

+
                let vertical =
+
                    Layout::vertical([Constraint::Percentage(*percent_y)]).flex(Flex::Center);
+
                let horizontal =
+
                    Layout::horizontal([Constraint::Percentage(*percent_x)]).flex(Flex::Center);
+
                let [area] = vertical.areas(area);
+
                let [area] = horizontal.areas(area);
+

+
                [area].into()
+
            }
+
        }
+
    }
+
}
+

+
/// The `Ui` is the main frontend component that provides render and user-input capture
+
/// capabilities. An application consists of at least 1 root `Ui`. An `Ui` can build child
+
/// `Ui`s that partially inherit attributes.
+
#[derive(Clone, Debug)]
+
pub struct Ui<M> {
+
    /// The context this runs in: frame sizes, captured user-inputs etc.
+
    ctx: Context<M>,
+
    /// The UI theme.
+
    theme: Theme,
+
    /// The area this can render in.
+
    area: Rect,
+
    /// The layout used to calculate the next area to draw.
+
    layout: Layout,
+
    /// Currently focused area.
+
    focus_area: Option<usize>,
+
    /// If this has focus.
+
    has_focus: bool,
+
    /// Current rendering counter that is increased whenever the next area to draw
+
    /// on is requested.
+
    count: usize,
+
}
+

+
impl<M> Ui<M> {
+
    pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
+
        self.has_focus
+
            && self.is_area_focused()
+
            && self.ctx.inputs.iter().any(|event| {
+
                if let Event::Key(key) = event {
+
                    return f(*key);
+
                }
+
                false
+
            })
+
    }
+

+
    pub fn has_global_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
+
        self.has_focus
+
            && self.ctx.inputs.iter().any(|event| {
+
                if let Event::Key(key) = event {
+
                    return f(*key);
+
                }
+
                false
+
            })
+
    }
+

+
    pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
+
        if self.has_focus && self.is_area_focused() {
+
            let matches = |&event| {
+
                if let Event::Key(key) = event {
+
                    return f(key);
+
                }
+
                false
+
            };
+

+
            if let Some(Event::Key(key)) =
+
                self.ctx.inputs.iter().find(|event| matches(event)).copied()
+
            {
+
                return Some(key);
+
            }
+
            None
+
        } else {
+
            None
+
        }
+
    }
+
}
+

+
impl<M> Default for Ui<M> {
+
    fn default() -> Self {
+
        Self {
+
            theme: Theme::default(),
+
            area: Rect::default(),
+
            layout: Layout::default(),
+
            focus_area: None,
+
            has_focus: true,
+
            count: 0,
+
            ctx: Context::default(),
+
        }
+
    }
+
}
+

+
impl<M> Ui<M> {
+
    pub fn new(area: Rect) -> Self {
+
        Self {
+
            area,
+
            ..Default::default()
+
        }
+
    }
+

+
    pub fn with_area(mut self, area: Rect) -> Self {
+
        self.area = area;
+
        self
+
    }
+

+
    pub fn with_layout(mut self, layout: Layout) -> Self {
+
        self.layout = layout;
+
        self
+
    }
+

+
    pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
+
        self.focus_area = focus;
+
        self
+
    }
+

+
    pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
+
        self.ctx = ctx;
+
        self
+
    }
+

+
    pub fn with_focus(mut self) -> Self {
+
        self.has_focus = true;
+
        self
+
    }
+

+
    pub fn without_focus(mut self) -> Self {
+
        self.has_focus = false;
+
        self
+
    }
+

+
    pub fn theme(&self) -> &Theme {
+
        &self.theme
+
    }
+

+
    pub fn area(&self) -> Rect {
+
        self.area
+
    }
+

+
    pub fn next_area(&mut self) -> Option<(Rect, bool)> {
+
        let area_focus = self
+
            .focus_area
+
            .map(|focus| self.count == focus)
+
            .unwrap_or(false);
+
        let rect = self.layout.split(self.area).get(self.count).cloned();
+

+
        self.count += 1;
+

+
        rect.map(|rect| (rect, area_focus))
+
    }
+

+
    pub fn current_area(&mut self) -> Option<(Rect, bool)> {
+
        let count = self.count.saturating_sub(1);
+

+
        let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
+
        let rect = self.layout.split(self.area).get(self.count).cloned();
+

+
        rect.map(|rect| (rect, area_focus))
+
    }
+

+
    pub fn is_area_focused(&self) -> bool {
+
        let count = self.count.saturating_sub(1);
+
        self.focus_area.map(|focus| count == focus).unwrap_or(false)
+
    }
+

+
    pub fn has_focus(&self) -> bool {
+
        self.has_focus
+
    }
+

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

+
    pub fn focus_next(&mut self) {
+
        if self.focus_area.is_none() {
+
            self.focus_area = Some(0);
+
        } else {
+
            self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
+
        }
+
    }
+

+
    pub fn send_message(&self, message: M) {
+
        if let Some(sender) = &self.ctx.sender {
+
            let _ = sender.send(message);
+
        }
+
    }
+
}
+

+
impl<M> Ui<M>
+
where
+
    M: Clone,
+
{
+
    pub fn add(&mut self, frame: &mut Frame, widget: impl Widget) -> Response {
+
        widget.ui(self, frame)
+
    }
+

+
    pub fn child_ui(&mut self, area: Rect, layout: impl Into<Layout>) -> Self {
+
        Ui::default()
+
            .with_area(area)
+
            .with_layout(layout.into())
+
            .with_ctx(self.ctx.clone())
+
    }
+

+
    pub fn layout<R>(
+
        &mut self,
+
        layout: impl Into<Layout>,
+
        focus: Option<usize>,
+
        add_contents: impl FnOnce(&mut Self) -> R,
+
    ) -> InnerResponse<R> {
+
        self.layout_dyn(layout, focus, Box::new(add_contents))
+
    }
+

+
    pub fn layout_dyn<R>(
+
        &mut self,
+
        layout: impl Into<Layout>,
+
        focus: Option<usize>,
+
        add_contents: Box<AddContentFn<M, R>>,
+
    ) -> InnerResponse<R> {
+
        let (area, area_focus) = self.next_area().unwrap_or_default();
+

+
        let mut child_ui = Ui {
+
            has_focus: area_focus,
+
            focus_area: focus,
+
            ..self.child_ui(area, layout)
+
        };
+

+
        InnerResponse::new(add_contents(&mut child_ui), Response::default())
+
    }
+
}
+

+
impl<M> Ui<M>
+
where
+
    M: Clone,
+
{
+
    pub fn container<R>(
+
        &mut self,
+
        layout: impl Into<Layout>,
+
        focus: &mut Option<usize>,
+
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
+
    ) -> InnerResponse<R> {
+
        let (area, area_focus) = self.next_area().unwrap_or_default();
+

+
        let layout: Layout = layout.into();
+
        let len = layout.len();
+

+
        // TODO(erikli): Check if setting the focus area is needed at all.
+
        let mut child_ui = Ui {
+
            has_focus: area_focus,
+
            focus_area: *focus,
+
            ..self.child_ui(area, layout)
+
        };
+

+
        widget::Container::new(len, focus).show(&mut child_ui, add_contents)
+
    }
+

+
    pub fn popup<R>(
+
        &mut self,
+
        layout: impl Into<Layout>,
+
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
+
    ) -> InnerResponse<R> {
+
        let layout: Layout = layout.into();
+
        let areas = layout.split(self.area());
+
        let area = areas.first().cloned().unwrap_or(self.area());
+

+
        let mut child_ui = self.child_ui(area, layout::fill());
+
        child_ui.has_focus = true;
+

+
        widget::Popup::default().show(&mut child_ui, add_contents)
+
    }
+

+
    pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
+
        widget::Label::new(content).ui(self, frame)
+
    }
+

+
    pub fn overline(&mut self, frame: &mut Frame) -> Response {
+
        let overline = String::from("▔").repeat(256);
+
        self.label(frame, Span::raw(overline).cyan())
+
    }
+

+
    pub fn separator(&mut self, frame: &mut Frame) -> Response {
+
        let overline = String::from("─").repeat(256);
+
        self.label(
+
            frame,
+
            Span::raw(overline).fg(self.theme.border_style.fg.unwrap_or_default()),
+
        )
+
    }
+

+
    #[allow(clippy::too_many_arguments)]
+
    pub fn table<'a, R, const W: usize>(
+
        &mut self,
+
        frame: &mut Frame,
+
        selected: &mut Option<usize>,
+
        items: &'a Vec<R>,
+
        columns: Vec<Column<'a>>,
+
        empty_message: Option<String>,
+
        spacing: Spacing,
+
        borders: Option<Borders>,
+
    ) -> Response
+
    where
+
        R: ToRow<W> + Clone,
+
    {
+
        widget::Table::new(selected, items, columns, empty_message, borders)
+
            .spacing(spacing)
+
            .ui(self, frame)
+
    }
+

+
    pub fn tree<R, Id>(
+
        &mut self,
+
        frame: &mut Frame,
+
        items: &'_ Vec<R>,
+
        opened: &mut Option<HashSet<Vec<Id>>>,
+
        selected: &mut Option<Vec<Id>>,
+
        borders: Option<Borders>,
+
    ) -> Response
+
    where
+
        R: ToTree<Id> + Clone,
+
        Id: ToString + Clone + Eq + Hash,
+
    {
+
        widget::Tree::new(items, opened, selected, borders, false).ui(self, frame)
+
    }
+

+
    pub fn shortcuts(
+
        &mut self,
+
        frame: &mut Frame,
+
        shortcuts: &[(&str, &str)],
+
        divider: char,
+
        alignment: Alignment,
+
    ) -> Response {
+
        widget::Shortcuts::new(shortcuts, divider, alignment).ui(self, frame)
+
    }
+

+
    pub fn column_bar(
+
        &mut self,
+
        frame: &mut Frame,
+
        columns: Vec<Column<'_>>,
+
        spacing: Spacing,
+
        borders: Option<Borders>,
+
    ) -> Response {
+
        widget::ColumnBar::new(columns, spacing, borders).ui(self, frame)
+
    }
+

+
    pub fn text_view<'a>(
+
        &mut self,
+
        frame: &mut Frame,
+
        text: impl Into<Text<'a>>,
+
        scroll: &'a mut Position,
+
        borders: Option<Borders>,
+
    ) -> Response {
+
        widget::TextView::new(text, None::<String>, scroll, borders).ui(self, frame)
+
    }
+

+
    pub fn text_view_with_footer<'a>(
+
        &mut self,
+
        frame: &mut Frame,
+
        text: impl Into<Text<'a>>,
+
        footer: impl Into<Text<'a>>,
+
        scroll: &'a mut Position,
+
        borders: Option<Borders>,
+
    ) -> Response {
+
        widget::TextView::new(text, Some(footer), scroll, borders).ui(self, frame)
+
    }
+

+
    pub fn centered_text_view<'a>(
+
        &mut self,
+
        frame: &mut Frame,
+
        text: impl Into<Text<'a>>,
+
        borders: Option<Borders>,
+
    ) -> Response {
+
        widget::CenteredTextView::new(text, borders).ui(self, frame)
+
    }
+

+
    pub fn text_edit_singleline(
+
        &mut self,
+
        frame: &mut Frame,
+
        text: &mut String,
+
        cursor: &mut usize,
+
        label: Option<impl ToString>,
+
        borders: Option<Borders>,
+
    ) -> Response {
+
        match label {
+
            Some(label) => widget::TextEdit::new(text, cursor, borders)
+
                .with_label(label)
+
                .ui(self, frame),
+
            _ => widget::TextEdit::new(text, cursor, borders).ui(self, frame),
+
        }
+
    }
+
}
+

#[derive(Clone, Debug, Default)]
pub struct ColumnView {
    small: bool,
deleted src/ui/im.rs
@@ -1,664 +0,0 @@
-
pub mod widget;
-

-
use std::collections::{HashSet, VecDeque};
-
use std::fmt::Debug;
-
use std::hash::Hash;
-
use std::rc::Rc;
-
use std::time::Duration;
-

-
use anyhow::Result;
-

-
use ratatui::style::Stylize;
-
use ratatui::text::{Span, Text};
-
use tokio::sync::broadcast;
-
use tokio::sync::mpsc::UnboundedReceiver;
-

-
use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect};
-
use ratatui::{Frame, Viewport};
-

-
use crate::event::{Event, Key};
-
use crate::store::Update;
-
use crate::terminal::Terminal;
-
use crate::ui::theme::Theme;
-
use crate::ui::{Column, Spacing, ToRow, ToTree};
-
use crate::{Interrupted, Share};
-

-
use crate::ui::im::widget::Widget;
-

-
use self::widget::AddContentFn;
-

-
use super::layout;
-

-
const RENDERING_TICK_RATE: Duration = Duration::from_millis(250);
-

-
/// The main UI trait for the ability to render an application.
-
pub trait Show<M> {
-
    fn show(&self, ctx: &Context<M>, frame: &mut Frame) -> Result<()>;
-
}
-

-
#[derive(Default)]
-
pub struct Frontend {}
-

-
impl Frontend {
-
    pub async fn run<S, M, R>(
-
        self,
-
        message_tx: broadcast::Sender<M>,
-
        mut state_rx: UnboundedReceiver<S>,
-
        mut event_rx: UnboundedReceiver<Event>,
-
        mut interrupt_rx: broadcast::Receiver<Interrupted<R>>,
-
        viewport: Viewport,
-
    ) -> anyhow::Result<Interrupted<R>>
-
    where
-
        S: Update<M, Return = R> + Show<M>,
-
        M: Share,
-
        R: Share,
-
    {
-
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
-
        let mut terminal = Terminal::try_from(viewport)?;
-

-
        let mut state = state_rx.recv().await.unwrap();
-
        let mut ctx = Context::default().with_sender(message_tx);
-

-
        let result: anyhow::Result<Interrupted<R>> = loop {
-
            tokio::select! {
-
                // Tick to terminate the select every N milliseconds
-
                _ = ticker.tick() => (),
-
                // Handle input events
-
                Some(event) = event_rx.recv() => {
-
                    match event {
-
                        Event::Key(key) => {
-
                            log::debug!(target: "frontend", "Received key event: {key:?}");
-
                            ctx.store_input(event)
-
                        }
-
                        Event::Resize(x, y) => {
-
                            log::debug!(target: "frontend", "Received resize event: {x},{y}");
-
                            terminal.clear()?;
-
                        },
-
                        Event::Unknown => {
-
                            log::debug!(target: "frontend", "Received unknown event")
-
                        }
-
                    }
-
                },
-
                // Handle state updates
-
                Some(s) = state_rx.recv() => {
-
                    state = s;
-
                },
-
                // Catch and handle interrupt signal to gracefully shutdown
-
                Ok(interrupted) = interrupt_rx.recv() => {
-
                    break Ok(interrupted);
-
                }
-
            }
-
            terminal.draw(|frame| {
-
                let ctx = ctx.clone().with_frame_size(frame.area());
-

-
                if let Err(err) = state.show(&ctx, frame) {
-
                    log::warn!("Drawing failed: {err}");
-
                }
-
            })?;
-

-
            ctx.clear_inputs();
-
        };
-
        terminal.restore()?;
-

-
        result
-
    }
-
}
-

-
#[derive(Default, Debug)]
-
pub struct Response {
-
    pub changed: bool,
-
}
-

-
#[derive(Debug)]
-
pub struct InnerResponse<R> {
-
    /// What the user closure returned.
-
    pub inner: R,
-
    /// The response of the area.
-
    pub response: Response,
-
}
-

-
impl<R> InnerResponse<R> {
-
    #[inline]
-
    pub fn new(inner: R, response: Response) -> Self {
-
        Self { inner, response }
-
    }
-
}
-

-
/// A `Context` is held by the `Ui` and reflects the environment a `Ui` runs in.
-
#[derive(Clone, Debug)]
-
pub struct Context<M> {
-
    /// Currently captured user inputs. Inputs that where stored via `store_input`
-
    /// need to be cleared manually via `clear_inputs` (usually for each frame drawn).
-
    inputs: VecDeque<Event>,
-
    /// Current frame of the application.
-
    pub(crate) frame_size: Rect,
-
    /// The message sender used by the `Ui` to send application messages.
-
    pub(crate) sender: Option<broadcast::Sender<M>>,
-
}
-

-
impl<M> Default for Context<M> {
-
    fn default() -> Self {
-
        Self {
-
            inputs: VecDeque::default(),
-
            frame_size: Rect::default(),
-
            sender: None,
-
        }
-
    }
-
}
-

-
impl<M> Context<M> {
-
    pub fn new(frame_size: Rect) -> Self {
-
        Self {
-
            frame_size,
-
            ..Default::default()
-
        }
-
    }
-

-
    pub fn with_inputs(mut self, inputs: VecDeque<Event>) -> Self {
-
        self.inputs = inputs;
-
        self
-
    }
-

-
    pub fn with_frame_size(mut self, frame_size: Rect) -> Self {
-
        self.frame_size = frame_size;
-
        self
-
    }
-

-
    pub fn with_sender(mut self, sender: broadcast::Sender<M>) -> Self {
-
        self.sender = Some(sender);
-
        self
-
    }
-

-
    pub fn frame_size(&self) -> Rect {
-
        self.frame_size
-
    }
-

-
    pub fn store_input(&mut self, event: Event) {
-
        self.inputs.push_back(event);
-
    }
-

-
    pub fn clear_inputs(&mut self) {
-
        self.inputs.clear();
-
    }
-
}
-

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

-
/// A `Layout` is used to support pre-defined layouts. It either represents
-
/// such a predefined layout or a wrapped `ratatui` layout. It's used internally
-
/// but can be build from a `ratatui` layout.
-
#[derive(Clone, Default, Debug)]
-
pub enum Layout {
-
    #[default]
-
    None,
-
    Wrapped {
-
        internal: ratatui::layout::Layout,
-
    },
-
    Expandable3 {
-
        left_only: bool,
-
    },
-
    Popup {
-
        percent_x: u16,
-
        percent_y: u16,
-
    },
-
}
-

-
impl From<ratatui::layout::Layout> for Layout {
-
    fn from(layout: ratatui::layout::Layout) -> Self {
-
        Layout::Wrapped { internal: layout }
-
    }
-
}
-

-
impl Layout {
-
    pub fn len(&self) -> usize {
-
        match self {
-
            Layout::None => 0,
-
            Layout::Wrapped { internal } => internal.split(Rect::default()).len(),
-
            Layout::Expandable3 { left_only } => {
-
                if *left_only {
-
                    1
-
                } else {
-
                    3
-
                }
-
            }
-
            Layout::Popup {
-
                percent_x: _,
-
                percent_y: _,
-
            } => 1,
-
        }
-
    }
-

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

-
    pub fn split(&self, area: Rect) -> Rc<[Rect]> {
-
        match self {
-
            Layout::None => Rc::new([]),
-
            Layout::Wrapped { internal } => internal.split(area),
-
            Layout::Expandable3 { left_only } => {
-
                use ratatui::layout::Layout;
-

-
                if *left_only {
-
                    [area].into()
-
                } else if area.width <= 140 {
-
                    let [left, right] = Layout::horizontal([
-
                        Constraint::Percentage(50),
-
                        Constraint::Percentage(50),
-
                    ])
-
                    .areas(area);
-
                    let [right_top, right_bottom] =
-
                        Layout::vertical([Constraint::Percentage(65), Constraint::Percentage(35)])
-
                            .areas(right);
-

-
                    [left, right_top, right_bottom].into()
-
                } else {
-
                    Layout::horizontal([
-
                        Constraint::Percentage(33),
-
                        Constraint::Percentage(33),
-
                        Constraint::Percentage(33),
-
                    ])
-
                    .split(area)
-
                }
-
            }
-
            Layout::Popup {
-
                percent_x,
-
                percent_y,
-
            } => {
-
                use ratatui::layout::Layout;
-

-
                let vertical =
-
                    Layout::vertical([Constraint::Percentage(*percent_y)]).flex(Flex::Center);
-
                let horizontal =
-
                    Layout::horizontal([Constraint::Percentage(*percent_x)]).flex(Flex::Center);
-
                let [area] = vertical.areas(area);
-
                let [area] = horizontal.areas(area);
-

-
                [area].into()
-
            }
-
        }
-
    }
-
}
-

-
/// The `Ui` is the main frontend component that provides render and user-input capture
-
/// capabilities. An application consists of at least 1 root `Ui`. An `Ui` can build child
-
/// `Ui`s that partially inherit attributes.
-
#[derive(Clone, Debug)]
-
pub struct Ui<M> {
-
    /// The context this runs in: frame sizes, captured user-inputs etc.
-
    ctx: Context<M>,
-
    /// The UI theme.
-
    theme: Theme,
-
    /// The area this can render in.
-
    area: Rect,
-
    /// The layout used to calculate the next area to draw.
-
    layout: Layout,
-
    /// Currently focused area.
-
    focus_area: Option<usize>,
-
    /// If this has focus.
-
    has_focus: bool,
-
    /// Current rendering counter that is increased whenever the next area to draw
-
    /// on is requested.
-
    count: usize,
-
}
-

-
impl<M> Ui<M> {
-
    pub fn has_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
-
        self.has_focus
-
            && self.is_area_focused()
-
            && self.ctx.inputs.iter().any(|event| {
-
                if let Event::Key(key) = event {
-
                    return f(*key);
-
                }
-
                false
-
            })
-
    }
-

-
    pub fn has_global_input(&mut self, f: impl Fn(Key) -> bool) -> bool {
-
        self.has_focus
-
            && self.ctx.inputs.iter().any(|event| {
-
                if let Event::Key(key) = event {
-
                    return f(*key);
-
                }
-
                false
-
            })
-
    }
-

-
    pub fn get_input(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
-
        if self.has_focus && self.is_area_focused() {
-
            let matches = |&event| {
-
                if let Event::Key(key) = event {
-
                    return f(key);
-
                }
-
                false
-
            };
-

-
            if let Some(Event::Key(key)) =
-
                self.ctx.inputs.iter().find(|event| matches(event)).copied()
-
            {
-
                return Some(key);
-
            }
-
            None
-
        } else {
-
            None
-
        }
-
    }
-
}
-

-
impl<M> Default for Ui<M> {
-
    fn default() -> Self {
-
        Self {
-
            theme: Theme::default(),
-
            area: Rect::default(),
-
            layout: Layout::default(),
-
            focus_area: None,
-
            has_focus: true,
-
            count: 0,
-
            ctx: Context::default(),
-
        }
-
    }
-
}
-

-
impl<M> Ui<M> {
-
    pub fn new(area: Rect) -> Self {
-
        Self {
-
            area,
-
            ..Default::default()
-
        }
-
    }
-

-
    pub fn with_area(mut self, area: Rect) -> Self {
-
        self.area = area;
-
        self
-
    }
-

-
    pub fn with_layout(mut self, layout: Layout) -> Self {
-
        self.layout = layout;
-
        self
-
    }
-

-
    pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
-
        self.focus_area = focus;
-
        self
-
    }
-

-
    pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
-
        self.ctx = ctx;
-
        self
-
    }
-

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

-
    pub fn without_focus(mut self) -> Self {
-
        self.has_focus = false;
-
        self
-
    }
-

-
    pub fn theme(&self) -> &Theme {
-
        &self.theme
-
    }
-

-
    pub fn area(&self) -> Rect {
-
        self.area
-
    }
-

-
    pub fn next_area(&mut self) -> Option<(Rect, bool)> {
-
        let area_focus = self
-
            .focus_area
-
            .map(|focus| self.count == focus)
-
            .unwrap_or(false);
-
        let rect = self.layout.split(self.area).get(self.count).cloned();
-

-
        self.count += 1;
-

-
        rect.map(|rect| (rect, area_focus))
-
    }
-

-
    pub fn current_area(&mut self) -> Option<(Rect, bool)> {
-
        let count = self.count.saturating_sub(1);
-

-
        let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
-
        let rect = self.layout.split(self.area).get(self.count).cloned();
-

-
        rect.map(|rect| (rect, area_focus))
-
    }
-

-
    pub fn is_area_focused(&self) -> bool {
-
        let count = self.count.saturating_sub(1);
-
        self.focus_area.map(|focus| count == focus).unwrap_or(false)
-
    }
-

-
    pub fn has_focus(&self) -> bool {
-
        self.has_focus
-
    }
-

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

-
    pub fn focus_next(&mut self) {
-
        if self.focus_area.is_none() {
-
            self.focus_area = Some(0);
-
        } else {
-
            self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
-
        }
-
    }
-

-
    pub fn send_message(&self, message: M) {
-
        if let Some(sender) = &self.ctx.sender {
-
            let _ = sender.send(message);
-
        }
-
    }
-
}
-

-
impl<M> Ui<M>
-
where
-
    M: Clone,
-
{
-
    pub fn add(&mut self, frame: &mut Frame, widget: impl Widget) -> Response {
-
        widget.ui(self, frame)
-
    }
-

-
    pub fn child_ui(&mut self, area: Rect, layout: impl Into<Layout>) -> Self {
-
        Ui::default()
-
            .with_area(area)
-
            .with_layout(layout.into())
-
            .with_ctx(self.ctx.clone())
-
    }
-

-
    pub fn layout<R>(
-
        &mut self,
-
        layout: impl Into<Layout>,
-
        focus: Option<usize>,
-
        add_contents: impl FnOnce(&mut Self) -> R,
-
    ) -> InnerResponse<R> {
-
        self.layout_dyn(layout, focus, Box::new(add_contents))
-
    }
-

-
    pub fn layout_dyn<R>(
-
        &mut self,
-
        layout: impl Into<Layout>,
-
        focus: Option<usize>,
-
        add_contents: Box<AddContentFn<M, R>>,
-
    ) -> InnerResponse<R> {
-
        let (area, area_focus) = self.next_area().unwrap_or_default();
-

-
        let mut child_ui = Ui {
-
            has_focus: area_focus,
-
            focus_area: focus,
-
            ..self.child_ui(area, layout)
-
        };
-

-
        InnerResponse::new(add_contents(&mut child_ui), Response::default())
-
    }
-
}
-

-
impl<M> Ui<M>
-
where
-
    M: Clone,
-
{
-
    pub fn container<R>(
-
        &mut self,
-
        layout: impl Into<Layout>,
-
        focus: &mut Option<usize>,
-
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
-
    ) -> InnerResponse<R> {
-
        let (area, area_focus) = self.next_area().unwrap_or_default();
-

-
        let layout: Layout = layout.into();
-
        let len = layout.len();
-

-
        // TODO(erikli): Check if setting the focus area is needed at all.
-
        let mut child_ui = Ui {
-
            has_focus: area_focus,
-
            focus_area: *focus,
-
            ..self.child_ui(area, layout)
-
        };
-

-
        widget::Container::new(len, focus).show(&mut child_ui, add_contents)
-
    }
-

-
    pub fn popup<R>(
-
        &mut self,
-
        layout: impl Into<Layout>,
-
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
-
    ) -> InnerResponse<R> {
-
        let layout: Layout = layout.into();
-
        let areas = layout.split(self.area());
-
        let area = areas.first().cloned().unwrap_or(self.area());
-

-
        let mut child_ui = self.child_ui(area, layout::fill());
-
        child_ui.has_focus = true;
-

-
        widget::Popup::default().show(&mut child_ui, add_contents)
-
    }
-

-
    pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
-
        widget::Label::new(content).ui(self, frame)
-
    }
-

-
    pub fn overline(&mut self, frame: &mut Frame) -> Response {
-
        let overline = String::from("▔").repeat(256);
-
        self.label(frame, Span::raw(overline).cyan())
-
    }
-

-
    pub fn separator(&mut self, frame: &mut Frame) -> Response {
-
        let overline = String::from("─").repeat(256);
-
        self.label(
-
            frame,
-
            Span::raw(overline).fg(self.theme.border_style.fg.unwrap_or_default()),
-
        )
-
    }
-

-
    #[allow(clippy::too_many_arguments)]
-
    pub fn table<'a, R, const W: usize>(
-
        &mut self,
-
        frame: &mut Frame,
-
        selected: &mut Option<usize>,
-
        items: &'a Vec<R>,
-
        columns: Vec<Column<'a>>,
-
        empty_message: Option<String>,
-
        spacing: Spacing,
-
        borders: Option<Borders>,
-
    ) -> Response
-
    where
-
        R: ToRow<W> + Clone,
-
    {
-
        widget::Table::new(selected, items, columns, empty_message, borders)
-
            .spacing(spacing)
-
            .ui(self, frame)
-
    }
-

-
    pub fn tree<R, Id>(
-
        &mut self,
-
        frame: &mut Frame,
-
        items: &'_ Vec<R>,
-
        opened: &mut Option<HashSet<Vec<Id>>>,
-
        selected: &mut Option<Vec<Id>>,
-
        borders: Option<Borders>,
-
    ) -> Response
-
    where
-
        R: ToTree<Id> + Clone,
-
        Id: ToString + Clone + Eq + Hash,
-
    {
-
        widget::Tree::new(items, opened, selected, borders, false).ui(self, frame)
-
    }
-

-
    pub fn shortcuts(
-
        &mut self,
-
        frame: &mut Frame,
-
        shortcuts: &[(&str, &str)],
-
        divider: char,
-
        alignment: Alignment,
-
    ) -> Response {
-
        widget::Shortcuts::new(shortcuts, divider, alignment).ui(self, frame)
-
    }
-

-
    pub fn column_bar(
-
        &mut self,
-
        frame: &mut Frame,
-
        columns: Vec<Column<'_>>,
-
        spacing: Spacing,
-
        borders: Option<Borders>,
-
    ) -> Response {
-
        widget::ColumnBar::new(columns, spacing, borders).ui(self, frame)
-
    }
-

-
    pub fn text_view<'a>(
-
        &mut self,
-
        frame: &mut Frame,
-
        text: impl Into<Text<'a>>,
-
        scroll: &'a mut Position,
-
        borders: Option<Borders>,
-
    ) -> Response {
-
        widget::TextView::new(text, None::<String>, scroll, borders).ui(self, frame)
-
    }
-

-
    pub fn text_view_with_footer<'a>(
-
        &mut self,
-
        frame: &mut Frame,
-
        text: impl Into<Text<'a>>,
-
        footer: impl Into<Text<'a>>,
-
        scroll: &'a mut Position,
-
        borders: Option<Borders>,
-
    ) -> Response {
-
        widget::TextView::new(text, Some(footer), scroll, borders).ui(self, frame)
-
    }
-

-
    pub fn centered_text_view<'a>(
-
        &mut self,
-
        frame: &mut Frame,
-
        text: impl Into<Text<'a>>,
-
        borders: Option<Borders>,
-
    ) -> Response {
-
        widget::CenteredTextView::new(text, borders).ui(self, frame)
-
    }
-

-
    pub fn text_edit_singleline(
-
        &mut self,
-
        frame: &mut Frame,
-
        text: &mut String,
-
        cursor: &mut usize,
-
        label: Option<impl ToString>,
-
        borders: Option<Borders>,
-
    ) -> Response {
-
        match label {
-
            Some(label) => widget::TextEdit::new(text, cursor, borders)
-
                .with_label(label)
-
                .ui(self, frame),
-
            _ => widget::TextEdit::new(text, cursor, borders).ui(self, frame),
-
        }
-
    }
-
}
deleted src/ui/im/widget.rs
@@ -1,1417 +0,0 @@
-
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::theme::style;
-
use crate::ui::{layout, span, Spacing, ToTree};
-
use crate::ui::{Column, ToRow};
-

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

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

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

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

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

-
    fn show_dyn<M, R>(
-
        self,
-
        ctx: &Context<M>,
-
        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));
-

-
        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) {
-
        self.focus = self
-
            .focus
-
            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)))
-
    }
-

-
    pub fn focus_prev(&mut self) {
-
        self.focus = self.focus.map(|focus| focus.saturating_sub(1))
-
    }
-
}
-

-
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 {
-
            focus: *self.focus,
-
            len: self.len,
-
        };
-

-
        if ui.has_global_input(|key| key == Key::Tab) {
-
            state.focus_next();
-
            response.changed = true;
-
        }
-
        if ui.has_global_input(|key| key == Key::BackTab) {
-
            state.focus_prev();
-
            response.changed = true;
-
        }
-
        *self.focus = state.focus;
-

-
        let mut ui = Ui {
-
            focus_area: state.focus,
-
            ..ui.clone()
-
        };
-

-
        let inner = add_contents(&mut ui);
-

-
        InnerResponse::new(inner, response)
-
    }
-
}
-

-
#[derive(Clone, Debug, Serialize, Deserialize)]
-
pub struct CompositeState {
-
    len: usize,
-
    focus: usize,
-
}
-

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

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

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

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

-
#[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(style::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 area_focus {
-
                        Style::default()
-
                    } else {
-
                        Style::default().dim()
-
                    });
-

-
                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("┃"),
-
                ))
-
                .highlight_style(style::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(style::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 virtual_length = length * ((length as f64).log2() as usize) / 100;
-
        // let content_length = area.height as usize + virtual_length;
-
        // let content_length = length;
-
        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 {
-
                Style::default()
-
            } else {
-
                Style::default().dim()
-
            });
-

-
        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
-
    }
-
}
added src/ui/widget.rs
@@ -0,0 +1,1417 @@
+
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::theme::style;
+
use crate::ui::{layout, span, Spacing, ToTree};
+
use crate::ui::{Column, ToRow};
+

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

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

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

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

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

+
    fn show_dyn<M, R>(
+
        self,
+
        ctx: &Context<M>,
+
        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));
+

+
        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) {
+
        self.focus = self
+
            .focus
+
            .map(|focus| cmp::min(focus.saturating_add(1), self.len.saturating_sub(1)))
+
    }
+

+
    pub fn focus_prev(&mut self) {
+
        self.focus = self.focus.map(|focus| focus.saturating_sub(1))
+
    }
+
}
+

+
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 {
+
            focus: *self.focus,
+
            len: self.len,
+
        };
+

+
        if ui.has_global_input(|key| key == Key::Tab) {
+
            state.focus_next();
+
            response.changed = true;
+
        }
+
        if ui.has_global_input(|key| key == Key::BackTab) {
+
            state.focus_prev();
+
            response.changed = true;
+
        }
+
        *self.focus = state.focus;
+

+
        let mut ui = Ui {
+
            focus_area: state.focus,
+
            ..ui.clone()
+
        };
+

+
        let inner = add_contents(&mut ui);
+

+
        InnerResponse::new(inner, response)
+
    }
+
}
+

+
#[derive(Clone, Debug, Serialize, Deserialize)]
+
pub struct CompositeState {
+
    len: usize,
+
    focus: usize,
+
}
+

+
impl CompositeState {
+
    pub fn new(len: usize, focus: usize) -> Self {
+
        Self { len, focus }
+
    }
+

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

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

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

+
#[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(style::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 area_focus {
+
                        Style::default()
+
                    } else {
+
                        Style::default().dim()
+
                    });
+

+
                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("┃"),
+
                ))
+
                .highlight_style(style::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(style::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 virtual_length = length * ((length as f64).log2() as usize) / 100;
+
        // let content_length = area.height as usize + virtual_length;
+
        // let content_length = length;
+
        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 {
+
                Style::default()
+
            } else {
+
                Style::default().dim()
+
            });
+

+
        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
+
    }
+
}