Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Add text field widget
Erik Kundt committed 2 years ago
commit 1fd061770d68e999b0e8a65617937b4910567bce
parent ac76014a554de83185395a96148c5d4410158fb7
2 files changed +180 -0
modified src/flux/ui/widget.rs
@@ -1,4 +1,5 @@
pub mod container;
+
pub mod input;

use std::cmp;
use std::fmt::Debug;
added src/flux/ui/widget/input.rs
@@ -0,0 +1,179 @@
+
use termion::event::Key;
+

+
use tokio::sync::mpsc::UnboundedSender;
+

+
use ratatui::layout::{Constraint, Layout};
+
use ratatui::prelude::{Backend, Rect};
+
use ratatui::style::Stylize;
+
use ratatui::text::{Line, Span};
+

+
use super::{Render, Widget};
+

+
pub struct TextField {
+
    /// Current value of the input box
+
    text: String,
+
    /// Position of cursor in the editor area.
+
    cursor_position: usize,
+
}
+

+
impl TextField {
+
    pub fn text(&self) -> &str {
+
        &self.text
+
    }
+

+
    pub fn set_text(&mut self, new_text: &str) {
+
        if self.text != new_text {
+
            self.text = String::from(new_text);
+
            self.cursor_position = self.text.len();
+
        }
+
    }
+

+
    pub fn reset(&mut self) {
+
        self.cursor_position = 0;
+
        self.text.clear();
+
    }
+

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

+
    fn move_cursor_left(&mut self) {
+
        let cursor_moved_left = self.cursor_position.saturating_sub(1);
+
        self.cursor_position = self.clamp_cursor(cursor_moved_left);
+
    }
+

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

+
    fn enter_char(&mut self, new_char: char) {
+
        self.text.insert(self.cursor_position, new_char);
+

+
        self.move_cursor_right();
+
    }
+

+
    fn delete_char(&mut self) {
+
        let is_not_cursor_leftmost = self.cursor_position != 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_position;
+
            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())
+
    }
+
}
+

+
impl<S, A> Widget<S, A> for TextField {
+
    fn new(_state: &S, _action_tx: UnboundedSender<A>) -> Self {
+
        Self {
+
            //
+
            text: String::new(),
+
            cursor_position: 0,
+
        }
+
    }
+

+
    fn move_with_state(self, _state: &S) -> Self
+
    where
+
        Self: Sized,
+
    {
+
        Self { ..self }
+
    }
+

+
    fn name(&self) -> &str {
+
        "Input Box"
+
    }
+

+
    fn handle_key_event(&mut self, key: Key) {
+
        match key {
+
            Key::Char(to_insert)
+
                if (key != Key::Alt('\n'))
+
                    && (key != Key::Char('\n'))
+
                    && (key != Key::Ctrl('\n')) =>
+
            {
+
                self.enter_char(to_insert);
+
            }
+
            Key::Backspace => {
+
                self.delete_char();
+
            }
+
            Key::Left => {
+
                self.move_cursor_left();
+
            }
+
            Key::Right => {
+
                self.move_cursor_right();
+
            }
+
            _ => {}
+
        }
+
    }
+
}
+

+
pub struct TextFieldProps {
+
    pub titles: (String, String),
+
    pub inline_label: bool,
+
    pub show_cursor: bool,
+
}
+

+
impl Render<TextFieldProps> for TextField {
+
    fn render<B: Backend>(&self, frame: &mut ratatui::Frame, area: Rect, props: TextFieldProps) {
+
        let layout = Layout::vertical(Constraint::from_lengths([1, 1])).split(area);
+

+
        let input = self.text.as_str();
+
        let label = format!(" {} ", props.titles.0);
+
        let overline = String::from("▔").repeat(area.width as usize);
+
        let cursor_pos = self.cursor_position as u16;
+

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

+
            let label = Span::from(label.clone()).magenta().dim().reversed();
+
            let input = Span::from(input).reset();
+

+
            let overline = Line::from([Span::raw(overline).magenta().dim()].to_vec());
+

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

+
            if props.show_cursor {
+
                frame.set_cursor(top_layout[2].x + cursor_pos, top_layout[2].y)
+
            }
+
        } else {
+
            let top = Line::from([Span::from(input).reset()].to_vec());
+
            let bottom = Line::from(
+
                [
+
                    Span::from(label).magenta().dim().reversed(),
+
                    Span::raw(overline).magenta().dim(),
+
                ]
+
                .to_vec(),
+
            );
+

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

+
            if props.show_cursor {
+
                frame.set_cursor(area.x + cursor_pos, area.y)
+
            }
+
        }
+
    }
+
}