Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
radicle-tui src ui.rs
pub mod ext;
pub mod layout;
pub mod span;
pub mod theme;
pub mod utils;
pub mod widget;

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::layout::Spacing;
use crate::ui::theme::Theme;
use crate::ui::widget::{AddContentFn, Borders, Column, Widget};
use crate::{Interrupted, Share};

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!("Received key event: {key:?}");
                            ctx.store_input(event)
                        }
                        Event::Resize(x, y) => {
                            log::debug!("Received resize event: {x},{y}");
                            terminal.clear()?;
                        },
                        Event::Unknown => {
                            log::debug!("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::error!("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();
    }
}

/// 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(60), Constraint::Percentage(40)])
                            .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 with_theme(mut self, theme: Theme) -> Self {
        self.theme = theme;
        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())
            .with_theme(self.theme.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),
        }
    }
}

/// Needs to be implemented for items that are supposed to be rendered in tables.
pub trait ToRow<const W: usize> {
    fn to_row(&self) -> [Cell<'_>; W];
}

/// Needs to be implemented for items that are supposed to be rendered in trees.
pub trait ToTree<Id>
where
    Id: ToString,
{
    fn rows(&self) -> Vec<TreeItem<'_, Id>>;
}

/// A `BufferedValue` that writes updates to an internal
/// buffer. This buffer can be applied or reset.
///
/// Reading from a `BufferedValue` will return the buffer if it's
/// not empty. It will return the actual value otherwise.
#[derive(Clone, Debug)]
pub struct BufferedValue<T>
where
    T: Clone,
{
    value: T,
    buffer: Option<T>,
}

impl<T> BufferedValue<T>
where
    T: Clone,
{
    pub fn new(value: T) -> Self {
        Self {
            value,
            buffer: None,
        }
    }

    pub fn apply(&mut self) {
        if let Some(buffer) = self.buffer.clone() {
            self.value = buffer;
        }
        self.buffer = None;
    }

    pub fn reset(&mut self) {
        self.buffer = None;
    }

    pub fn write(&mut self, value: T) {
        self.buffer = Some(value);
    }

    pub fn read(&self) -> T {
        if let Some(buffer) = self.buffer.clone() {
            buffer
        } else {
            self.value.clone()
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn state_value_read_should_succeed() {
        let value = BufferedValue::new(0);
        assert_eq!(value.read(), 0);
    }

    #[test]
    fn state_value_read_buffer_should_succeed() {
        let mut value = BufferedValue::new(0);
        value.write(1);

        assert_eq!(value.read(), 1);
    }

    #[test]
    fn state_value_apply_should_succeed() {
        let mut value = BufferedValue::new(0);

        value.write(1);
        assert_eq!(value.read(), 1);

        value.apply();
        assert_eq!(value.read(), 1);
    }

    #[test]
    fn state_value_reset_should_succeed() {
        let mut value = BufferedValue::new(0);

        value.write(1);
        assert_eq!(value.read(), 1);

        value.reset();
        assert_eq!(value.read(), 0);
    }

    #[test]
    fn state_value_reset_after_apply_should_succeed() {
        let mut value = BufferedValue::new(0);

        value.write(1);
        assert_eq!(value.read(), 1);

        value.apply();
        value.reset();
        assert_eq!(value.read(), 1);
    }
}