glyph_ui 0.1.0

TUI library utilizing the Elm architecture
Documentation
//! Single line text input

use std::convert::TryFrom;

use euclid::Size2D;
use keyboard_types::Key;
use unicode_segmentation::UnicodeSegmentation;

use crate::{event::Event, unit::Cell, Printer, View as ViewTrait};

// The view can be as short as 2 cells but 10 provides a reasonably editable
// space without being too large
const SOFT_MIN_LINE_WIDTH: u16 = 10;

/// Shorthand for [`View::new()`]
///
/// [`View::new()`]: View::new
pub fn new<F, M>(state: &mut State, on_submit: F) -> View<'_, F>
where
    F: Fn(String) -> M,
{
    View::new(state, on_submit)
}

/// The view itself
pub struct View<'a, F> {
    state: &'a mut State,
    on_submit: F,
    clear_on_submit: bool,
    placeholder: Option<char>,
    echo: Echo,
}

impl<'a, F, M> View<'a, F>
where
    F: Fn(String) -> M,
{
    pub fn new(state: &'a mut State, on_submit: F) -> Self {
        Self {
            state,
            on_submit,
            clear_on_submit: true,
            placeholder: Some('_'),
            echo: Default::default(),
        }
    }

    /// Set a placeholder character
    ///
    /// The default is `Some('_')`.
    pub fn placeholder(mut self, placeholder: Option<char>) -> Self {
        self.placeholder = placeholder;

        self
    }

    /// Change echo mode
    ///
    /// This can be set to [`Echo::Off`](Echo::Off) or
    /// [`Echo::Faux`](Echo::Faux) for entering secrets such as passwords. The
    /// default is [`Echo::On`](Echo::On), which is suitable for regular text.
    pub fn echo(mut self, echo: Echo) -> Self {
        self.echo = echo;

        self
    }

    /// Change buffer behavior on submission
    ///
    /// By default, pressing `Enter` will clear the user input. If the text
    /// should remain in the input line after the user submits it, call this
    /// function with `false`.
    pub fn clear_on_submit(mut self, enable: bool) -> Self {
        self.clear_on_submit = enable;

        self
    }
}

/// Controls how the input line view echoes text
pub enum Echo {
    /// Normal text input
    ///
    /// This displays the text entered by the user as-is.
    On,

    /// Secret text input; less secure but more intuitive
    ///
    /// This mode echoes one character repeatedly (`*` is the most common
    /// choice), so it's possible to see the length of the secret. However,
    /// this is less likely to cause the user to think that their keyboard has
    /// suddenly stopped working.
    Faux(char),

    /// Secret text input; more secure but less intuitive
    ///
    /// This mode echoes no input at all, a side effect of which is that it's
    /// impossible to tell the length of the password by looking at the screen.
    Off,
}

impl Default for Echo {
    fn default() -> Self {
        Self::On
    }
}

/// Persistent state for this view
#[derive(Default)]
pub struct State {
    input: String,
}

impl State {
    /// Returns the current text the user has entered
    pub fn content(&self) -> &str {
        &self.input
    }
}

impl<T, M, F> ViewTrait<T, M> for View<'_, F>
where
    F: Fn(String) -> M,
    M: 'static,
{
    fn draw(&self, printer: &Printer, focused: bool) {
        if let Some(c) = self.placeholder {
            let line = std::iter::repeat(c)
                .take(printer.size().width.into())
                .fold(String::new(), |mut s, c| {
                    s.push(c);

                    s
                });

            printer.print(&line, (0, 0)).unwrap();
        }

        // Show only the ending of the input if it's too long to fit inside the
        // printable area. The +1 gives us a cell for the cursor at the end.
        let width = printer.size().width;
        let start = self
            .state
            .input
            .len()
            .saturating_add(1)
            .saturating_sub(width.into());

        // On and Faux will simply overwrite the placeholders if they were
        // printed and the input has contents
        match self.echo {
            // Show the actual text
            Echo::On => {
                printer.print(&self.state.input[start..], (0, 0)).unwrap();
            }

            // Generate/show a line of characters equal to the length of the
            // actual text
            Echo::Faux(c) => {
                let line: String =
                    std::iter::repeat(c).take(self.state.input.len()).collect();

                printer.print(&line[start..], (0, 0)).unwrap();
            }

            // Obvious
            Echo::Off => (),
        }

        // Only mess with the cursor location if we're the focused view
        if focused {
            match self.echo {
                // Calculate the cursor position based on input length and show
                // it
                Echo::On | Echo::Faux(_) => {
                    let cursor_x =
                        u16::try_from(self.state.input.graphemes(true).count())
                            .unwrap()
                            .min(width.saturating_sub(1));

                    printer.show_cursor_at((cursor_x, 0)).unwrap();
                }

                // The cursor will only ever be at the very beginning
                Echo::Off => {
                    printer.show_cursor_at((0, 0)).unwrap();
                }
            }
        }
    }

    fn width(&self) -> Size2D<u16, Cell> {
        (SOFT_MIN_LINE_WIDTH, 1).into()
    }

    fn height(&self) -> Size2D<u16, Cell> {
        (SOFT_MIN_LINE_WIDTH, 1).into()
    }

    fn layout(&self, constraint: Size2D<u16, Cell>) -> Size2D<u16, Cell> {
        (constraint.width, 1).into()
    }

    fn event(
        &mut self,
        event: &Event<T>,
        focused: bool,
    ) -> Box<dyn Iterator<Item = M>> {
        if !focused {
            return Box::new(std::iter::empty());
        }

        let message = if let Event::Key(k) = event {
            match &k.key {
                Key::Character(c) => {
                    self.state.input.push_str(c);

                    None
                }
                Key::Enter => {
                    let line = self.state.input.clone();
                    if self.clear_on_submit {
                        self.state.input.clear();
                    }
                    Some((self.on_submit)(line))
                }
                Key::Backspace => {
                    self.state.input.pop();

                    None
                }
                // TODO handle arrow keys and delete
                _ => None,
            }
        } else {
            None
        };

        if let Some(message) = message {
            Box::new(std::iter::once(message))
        } else {
            Box::new(std::iter::empty())
        }
    }

    fn interactive(&self) -> bool {
        true
    }
}