cursive 0.1.1

A TUI library based on ncurses-rs.
Documentation
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use With;
use direction::Direction;
use theme::{ColorStyle, Effect};
use vec::Vec2;
use view::{IdView, View};
use event::*;
use Printer;


/// Input box where the user can enter and edit text.
///
/// # Examples
///
/// From the [edit example].
///
/// [edit example]: https://github.com/gyscos/Cursive/blob/master/examples/edit.rs
///
/// ```
/// # extern crate cursive;
/// # use cursive::prelude::*;
/// # fn main() {
/// let mut siv = Cursive::new();
///
/// // Create a dialog with an edit text and a button.
/// siv.add_layer(Dialog::new(EditView::new().min_length(20).with_id("edit"))
///                   .padding((1, 1, 1, 0))
///                   .title("Enter your name")
///                   .button("Ok", |s| {
///                       // When the button is clicked,
///                       // read the text and print it in a new dialog.
///                       let name = s.find_id::<EditView>("edit")
///                                   .unwrap()
///                                   .get_content()
///                                   .to_string();
///                       if name.is_empty() {
///                           s.add_layer(Dialog::new(TextView::new("Please enter a name!"))
///                                           .dismiss_button("Ok"));
///                       } else {
///                           let content = format!("Hello {}!", name);
///                           s.pop_layer();
///                           s.add_layer(Dialog::new(TextView::new(&content))
///                                           .button("Quit", |s| s.quit()));
///                       }
///                   }));
/// # }
/// ```
pub struct EditView {
    /// Current content.
    content: String,
    /// Cursor position in the content, in bytes.
    cursor: usize,
    /// Minimum layout length asked to the parent.
    min_length: usize,

    /// Number of bytes to skip at the beginning of the content.
    ///
    /// (When the content is too long for the display, we hide part of it)
    offset: usize,
    /// Last display length, to know the possible offset range
    last_length: usize,

    enabled: bool,
}

new_default!(EditView);

impl EditView {
    /// Creates a new, empty edit view.
    pub fn new() -> Self {
        EditView {
            content: String::new(),
            cursor: 0,
            offset: 0,
            min_length: 1,
            last_length: 0, // scrollable: false,
            enabled: true,
        }
    }

    /// Disables this view.
    ///
    /// A disabled view cannot be selected.
    pub fn disable(&mut self) {
        self.enabled = false;
    }

    /// Disables this view.
    ///
    /// Chainable variant.
    pub fn disabled(self) -> Self {
        self.with(Self::disable)
    }

    /// Re-enables this view.
    pub fn enable(&mut self) {
        self.enabled = true;
    }

    /// Enable or disable this view.
    pub fn set_enabled(&mut self, enabled: bool) {
        self.enabled = enabled;
    }

    /// Returns `true` if this view is enabled.
    pub fn is_enabled(&self) -> bool {
        self.enabled
    }

    /// Replace the entire content of the view with the given one.
    pub fn set_content(&mut self, content: &str) {
        self.offset = 0;
        self.content = content.to_string();
    }

    /// Get the current text.
    pub fn get_content(&self) -> &str {
        &self.content
    }

    /// Sets the current content to the given value.
    ///
    /// Convenient chainable method.
    pub fn content(mut self, content: &str) -> Self {
        self.set_content(content);
        self
    }

    /// Sets the minimum length for this view.
    /// (This applies to the layout, not the content.)
    pub fn min_length(mut self, min_length: usize) -> Self {
        self.min_length = min_length;

        self
    }

    /// Wraps this view into an IdView with the given id.
    pub fn with_id(self, label: &str) -> IdView<Self> {
        IdView::new(label, self)
    }
}

impl View for EditView {
    fn draw(&self, printer: &Printer) {
        assert!(printer.size.x == self.last_length,
                "Was promised {}, received {}",
                self.last_length,
                printer.size.x);

        let width = self.content.width();
        printer.with_color(ColorStyle::Secondary, |printer| {
            let effect = if self.enabled {
                Effect::Reverse
            } else {
                Effect::Simple
            };
            printer.with_effect(effect, |printer| {
                if width < self.last_length {
                    // No problem, everything fits.
                    printer.print((0, 0), &self.content);
                    printer.print_hline((width, 0),
                                        printer.size.x - width,
                                        "_");
                } else {
                    let content = &self.content[self.offset..];
                    let display_bytes = content.graphemes(true)
                        .scan(0, |w, g| {
                            *w += g.width();
                            if *w > self.last_length {
                                None
                            } else {
                                Some(g)
                            }
                        })
                        .map(|g| g.len())
                        .fold(0, |a, b| a + b);

                    let content = &content[..display_bytes];

                    printer.print((0, 0), content);
                    let width = content.width();

                    if width < self.last_length {
                        printer.print_hline((width, 0),
                                            self.last_length - width,
                                            "_");
                    }
                }
            });

            // Now print cursor
            if printer.focused {
                let c = if self.cursor == self.content.len() {
                    "_"
                } else {
                    // Get the char from the string... Is it so hard?
                    self.content[self.cursor..]
                        .graphemes(true)
                        .next()
                        .expect(&format!("Found no char at cursor {} in {}",
                                         self.cursor,
                                         self.content))
                };
                let offset = self.content[self.offset..self.cursor].width();
                printer.print((offset, 0), c);
            }
        });
    }

    fn layout(&mut self, size: Vec2) {
        self.last_length = size.x;
    }

    fn get_min_size(&mut self, _: Vec2) -> Vec2 {
        Vec2::new(self.min_length, 1)
    }

    fn take_focus(&mut self, _: Direction) -> bool {
        self.enabled
    }

    fn on_event(&mut self, event: Event) -> EventResult {

        match event {
            Event::Char(ch) => {
                // Find the byte index of the char at self.cursor

                self.content.insert(self.cursor, ch);
                self.cursor += ch.len_utf8();
            }
            // TODO: handle ctrl-key?
            Event::Key(Key::Home) => self.cursor = 0,
            Event::Key(Key::End) => self.cursor = self.content.len(),
            Event::Key(Key::Left) if self.cursor > 0 => {
                let len = self.content[..self.cursor]
                    .graphemes(true)
                    .last()
                    .unwrap()
                    .len();
                self.cursor -= len;
            }
            Event::Key(Key::Right) if self.cursor < self.content.len() => {
                let len = self.content[self.cursor..]
                    .graphemes(true)
                    .next()
                    .unwrap()
                    .len();
                self.cursor += len;
            }
            Event::Key(Key::Backspace) if self.cursor > 0 => {
                let len = self.content[..self.cursor]
                    .graphemes(true)
                    .last()
                    .unwrap()
                    .len();
                self.cursor -= len;
                self.content.remove(self.cursor);
            }
            Event::Key(Key::Del) if self.cursor < self.content.len() => {
                self.content.remove(self.cursor);
            }
            _ => return EventResult::Ignored,
        }

        // Keep cursor in [offset, offset+last_length] by changing offset
        // So keep offset in [last_length-cursor,cursor]
        // Also call this on resize,
        // but right now it is an event like any other
        if self.cursor < self.offset {
            self.offset = self.cursor;
        } else {
            // So we're against the right wall.
            // Let's find how much space will be taken by the selection
            // (either a char, or _)
            let c_len = self.content[self.cursor..]
                .graphemes(true)
                .map(|g| g.width())
                .next()
                .unwrap_or(1);
            // Now, we have to fit self.content[..self.cursor]
            // into self.last_length - c_len.
            let available = self.last_length - c_len;
            // Look at the content before the cursor (we will print its tail).
            // From the end, count the length until we reach `available`.
            // Then sum the byte lengths.
            let tail_bytes =
                tail_bytes(&self.content[self.offset..self.cursor], available);
            self.offset = self.cursor - tail_bytes;
            assert!(self.cursor >= self.offset);

        }

        // If we have too much space
        if self.content[self.offset..].width() < self.last_length {
            let tail_bytes = tail_bytes(&self.content, self.last_length - 1);
            self.offset = self.content.len() - tail_bytes;
        }

        EventResult::Consumed(None)
    }
}

// Return the number of bytes, from the end of text,
// which constitute the longest tail that fits in the given width.
fn tail_bytes(text: &str, width: usize) -> usize {
    text.graphemes(true)
        .rev()
        .scan(0, |w, g| {
            *w += g.width();
            if *w > width {
                None
            } else {
                Some(g)
            }
        })
        .map(|g| g.len())
        .fold(0, |a, b| a + b)
}