├── .gitignore ├── screenshot.png ├── src ├── widget │ ├── builtin │ │ ├── mod.rs │ │ ├── logviewer.rs │ │ ├── lineedit.rs │ │ ├── promptline.rs │ │ └── table.rs │ ├── mod.rs │ └── widget.rs ├── lib.rs ├── base │ ├── mod.rs │ ├── grapheme_cluster.rs │ ├── terminal.rs │ ├── window.rs │ ├── style.rs │ └── basic_types.rs ├── container │ └── boxdrawing.rs └── input │ └── mod.rs ├── Cargo.toml ├── LICENSE ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftilde/unsegen/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/widget/builtin/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains several basic widgets that are built into the core library. 2 | pub mod lineedit; 3 | pub mod logviewer; 4 | pub mod promptline; 5 | pub mod table; 6 | pub mod textedit; 7 | 8 | pub use self::lineedit::*; 9 | pub use self::logviewer::*; 10 | pub use self::promptline::*; 11 | pub use self::table::*; 12 | pub use self::textedit::*; 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unsegen" 3 | version = "0.3.1" 4 | authors = ["ftilde "] 5 | 6 | description = "Another tui library" 7 | documentation = "https://docs.rs/unsegen" 8 | repository = "https://github.com/ftilde/unsegen" 9 | readme = "README.md" 10 | license = "MIT" 11 | keywords = ["terminal", "tui"] 12 | 13 | [dependencies] 14 | ndarray = "0.8" 15 | nix = "0.24" 16 | raw_tty = "0.1" 17 | smallvec = "1.8" 18 | termion = "1.5" 19 | unicode-segmentation = "1.0" 20 | unicode-width = "0.1" 21 | ropey = "1.3" 22 | 23 | [dev-dependencies] 24 | rand = "0.4" 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `unsegen` is a library facilitating the creation of text user interface (TUI) applications akin to ncurses. 2 | //! 3 | //! Detailed examples can be found at the root of each of the four main modules. 4 | #[macro_use] 5 | extern crate ndarray; 6 | extern crate nix; 7 | extern crate raw_tty; 8 | extern crate ropey; 9 | extern crate smallvec; 10 | extern crate termion; 11 | extern crate unicode_segmentation; 12 | extern crate unicode_width; 13 | 14 | #[deny(missing_docs)] 15 | pub mod base; 16 | #[deny(missing_docs)] 17 | pub mod container; 18 | #[deny(missing_docs)] 19 | pub mod input; 20 | #[deny(missing_docs)] 21 | pub mod widget; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ftilde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/base/mod.rs: -------------------------------------------------------------------------------- 1 | //! Basic terminal rendering including Terminal setup, "slicing" using Windows, and formatted 2 | //! writing to Windows using Cursors. 3 | //! 4 | //! # Example: 5 | //! ```no_run 6 | //! use unsegen::base::*; 7 | //! use std::io::stdout; 8 | //! use std::fmt::Write; 9 | //! 10 | //! let stdout = stdout(); 11 | //! let mut term = Terminal::new(stdout.lock()).unwrap(); 12 | //! 13 | //! { 14 | //! let win = term.create_root_window(); 15 | //! let (left, mut right) = win.split(ColIndex::new(5)).unwrap(); 16 | //! 17 | //! let (mut top, mut bottom) = left.split(RowIndex::new(2)).unwrap(); 18 | //! 19 | //! top.fill(GraphemeCluster::try_from('X').unwrap()); 20 | //! 21 | //! bottom.modify_default_style(StyleModifier::new().fg_color(Color::Green)); 22 | //! bottom.fill(GraphemeCluster::try_from('O').unwrap()); 23 | //! 24 | //! let mut cursor = Cursor::new(&mut right) 25 | //! .position(ColIndex::new(1), RowIndex::new(2)) 26 | //! .wrapping_mode(WrappingMode::Wrap) 27 | //! .style_modifier(StyleModifier::new().bold(true).bg_color(Color::Red)); 28 | //! 29 | //! writeln!(cursor, "Hi there!").unwrap(); 30 | //! } 31 | //! term.present(); 32 | //! ``` 33 | 34 | pub mod basic_types; 35 | pub mod cursor; 36 | pub mod grapheme_cluster; 37 | pub mod style; 38 | pub mod terminal; 39 | pub mod window; 40 | 41 | pub use self::basic_types::*; 42 | pub use self::cursor::*; 43 | pub use self::grapheme_cluster::*; 44 | pub use self::style::*; 45 | pub use self::terminal::*; 46 | pub use self::window::*; 47 | -------------------------------------------------------------------------------- /src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | //! Widget abstraction and some basic Widgets useful for creating basic building blocks of text 2 | //! user interfaces. 3 | //! 4 | //! # Example: 5 | //! ```no_run 6 | //! use unsegen::base::*; 7 | //! use unsegen::widget::*; 8 | //! use unsegen::widget::builtin::*; 9 | //! use std::io::stdout; 10 | //! 11 | //! struct MyWidget { 12 | //! prompt: PromptLine, 13 | //! buffer: LogViewer, 14 | //! } 15 | //! 16 | //! impl MyWidget { 17 | //! fn as_widget<'a>(&'a self) -> impl Widget + 'a { 18 | //! VLayout::new().alternating(StyleModifier::new().invert(true)) 19 | //! .widget("Some text on top") 20 | //! .widget(self.prompt.as_widget()) 21 | //! .widget(self.buffer.as_widget()) 22 | //! .widget("Some text below") 23 | //! } 24 | //! } 25 | //! 26 | //! 27 | //! fn main() { 28 | //! let stdout = stdout(); 29 | //! let mut term = Terminal::new(stdout.lock()).unwrap(); 30 | //! let mut widget = MyWidget { 31 | //! prompt: PromptLine::with_prompt(" > ".to_owned()), 32 | //! buffer: LogViewer::new(), 33 | //! }; 34 | //! 35 | //! loop { 36 | //! // Put application logic here: read input, chain behavior, react to other stuff 37 | //! { 38 | //! let win = term.create_root_window(); 39 | //! widget.as_widget().draw(win, RenderingHints::new().active(true)); 40 | //! } 41 | //! term.present(); 42 | //! } 43 | //! } 44 | //! ``` 45 | pub mod builtin; 46 | pub mod layouts; 47 | pub mod widget; 48 | 49 | pub use self::layouts::*; 50 | pub use self::widget::*; 51 | use super::base::*; 52 | 53 | /// Count the number of grapheme clusters in the given string. 54 | /// 55 | /// A thin convenience wrapper around unicode_segmentation. 56 | pub fn count_grapheme_clusters(text: &str) -> usize { 57 | use unicode_segmentation::UnicodeSegmentation; 58 | text.graphemes(true).count() 59 | } 60 | 61 | /// Calculate the (monospace) width of the given string. 62 | /// 63 | /// A thin convenience wrapper around unicode_width. 64 | pub fn text_width(text: &str) -> Width { 65 | use unicode_width::UnicodeWidthStr; 66 | Width::new(UnicodeWidthStr::width(text) as _).unwrap() 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unsegen 2 | 3 | [![](https://img.shields.io/crates/v/unsegen.svg)](https://crates.io/crates/unsegen/) 4 | [![](https://docs.rs/unsegen/badge.svg)](https://docs.rs/unsegen/) 5 | [![](https://img.shields.io/crates/l/unsegen.svg)]() 6 | 7 | `unsegen` is a library facilitating the creation of text user interface (TUI) applications akin to ncurses. 8 | Currently, `unsegen` only provides a Rust interface. 9 | 10 | ## Overview 11 | 12 | The library consists of four modules: 13 | 14 | * base: Basic terminal rendering including `Terminal` setup, "slicing" using `Windows`, and formatted writing to `Windows` using `Cursors`. 15 | * widget: `Widget` abstraction and some basic `Widget`s useful for creating basic building blocks of text user interfaces. 16 | * input: Raw terminal input events, common abstractions for application (component) `Behavior` and means to easily distribute events. 17 | * container: Higher level window manager like functionality using `Container`s as the combination of widget and input concepts. 18 | 19 | The following libraries are built on top of unsegen and provide higher level functionality: 20 | 21 | * [unsegen_jsonviewer](https://crates.io/crates/unsegen_jsonviewer) provides an interactive widget that can be used to display json values. 22 | * [unsegen_pager](https://crates.io/crates/unsegen_pager) provides a memory or file backed line buffer viewer with syntax highlighting and line decorations. 23 | * [unsegen_signals](https://crates.io/crates/unsegen_signals) uses unsegen's input module to raise signals on the usual key combinations (e.g., SIGINT on CTRL-C). 24 | * [unsegen_terminal](https://crates.io/crates/unsegen_terminal) provides a pseudoterminal that can be easily integrated into applications using unsegen. 25 | 26 | ## Getting Started 27 | 28 | `unsegen` is [available on crates.io](https://crates.io/crates/unsegen). You can install it by adding this line to your `Cargo.toml`: 29 | 30 | ```toml 31 | unsegen = "0.3.1" 32 | ``` 33 | 34 | ## Screenshots 35 | 36 | Here is a screenshot of [ugdb](https://github.com/ftilde/ugdb), which is built on top of `unsegen`. 37 | 38 | ![](screenshot.png) 39 | 40 | ## Examples 41 | 42 | There are examples at the top of each main modules' documentation (i.e., [base](https://docs.rs/unsegen/0.1.1/unsegen/base/index.html), [input](https://docs.rs/unsegen/0.1.1/unsegen/input/index.html), [widget](https://docs.rs/unsegen/0.1.1/unsegen/widget/index.html), and [container](https://docs.rs/unsegen/0.1.1/unsegen/container/index.html)) which should be sufficient to get you going. 43 | 44 | For a fully fledged application using `unsegen`, you can have a look at [ugdb](https://github.com/ftilde/ugdb), which was developed alongside `unsegen` and the primary motivation for it. 45 | 46 | ## Some notes on implementation details 47 | 48 | For simplicity, layouting is done in every draw call. 49 | This, in conjunction with recursive calls to calculate space demand of widgets, leads to not-so-great asymptotic runtime. 50 | However, I found this not to be a problem in practice so far. 51 | If this is problematic for, please file an issue. 52 | There are workarounds (caching the `draw`-result of widgets) for which convenient wrappers can be implemented in the library, but have not so far. 53 | 54 | ## Licensing 55 | 56 | `unsegen` is released under the MIT license. 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All breaking changes are marked with [BC] and potentially require API consumer changes after updating to the respective version. 4 | 5 | ## [0.3.1] - 2025-03-09 6 | ### Added 7 | - Add `WithHints` and WidgetExt `with_hints`. 8 | - Add `Cursor::move_to_bottom`, `Cursor::target`. 9 | - Add `TextEdit` widget which allows multi line editing. 10 | ### Changed 11 | - Avoid writing unchanged lines to terminal. 12 | ### Fixed 13 | - Make Table::rows take an immutable self reference. 14 | - Fix `Cursor::move_left` if position is outside of window. 15 | 16 | ## [0.3.0] - 2021-06-03 17 | ### Added 18 | - Implement `Widget` for strings (for `Borrow`). 19 | - Add `WidgetExt` with some convenience methods to change Widget behavior. 20 | - Implement `PromptLine` search functionality. 21 | - Add `chain_and_then`, `if_consumed` and `if_not_consumed` methods to `InputChain` for handling side effects. 22 | - Add `set_layout` method to change the layout of an existing `ContainerManager`. 23 | ### Changed 24 | - Change `Widget` semantics to be short-lived. 25 | - Replace `{Horizontal,Vertical}Layout` with `{H,V}Layout` which are short-lived and implement Widget. [BC] 26 | - Make `Table` scroll properly when window is too small to show whole table. 27 | - Add `BehaviorContext` to `TableRow` trait. This allows passing parameters to the `behavior` of a column. [BC] 28 | - Make `layout_linearly` take `weights` parameter. [BC] 29 | - Require weights for nodes in `HSplit` and `VSplit`. [BC] 30 | - `ContainerProvider::Index` and `Layout` must now implement `std::fmt::Debug`. [BC] 31 | - Rename `ContainerProvider::Parameters` to `ContainerProvider::Context`. [BC] 32 | 33 | ## [0.2.5] - 2020-11-14 34 | ### Fixed 35 | - Fix build for ppc targets (thanks to ericonr). 36 | ### Added 37 | - Implement `Scrollable` for `Table`. 38 | 39 | ## [0.2.4] - 2020-07-15 40 | ### Fixed 41 | - Bold style not reseting on some terminals. 42 | 43 | ## [0.2.3] - 2020-01-02 44 | ### Added 45 | - Add `Terminal::on_main_screen` for executing a function in a "normal" terminal state. 46 | - Add `Table::current_row` for immutably accessing the currently selected row. 47 | ### Fixed 48 | - Make layouting fairer in cases where minimum demand cannot be met. 49 | 50 | ## [0.2.2] - 2019-10-24 51 | ### Added 52 | - Add `LineEdit::cursor_pos` to retrieve (byte) cursor position. 53 | - Add `LineEdit::set_cursor_pos` to set (byte) cursor position. 54 | - Implement `Deref` for PromptLine. 55 | - Add `Behavior` implementation for slices of `ToEvent`s. 56 | - Add `From` and `From>` implementations to InputChain. 57 | ### Fixed 58 | - Fix erasing characters in `LineEdit`. 59 | 60 | ## [0.2.1] - 2019-07-21 61 | ### Fixed 62 | - Fix wrapping cursor outside of visible window. 63 | 64 | ## [0.2.0] - 2019-07-20 65 | ### Added 66 | - Add Default variant to base::Color enum. [BC] 67 | ### Changed 68 | - Change Default::default of base::Style to return default foreground and background Colors. 69 | - All methods of base::{Style,Text}FormatModifier take self by value. [BC] 70 | - All methods of base::Terminal propagate IO errors to the caller instead of panicking on failure. [BC] 71 | - The output sink type of a base::Terminal is now required to be a std::unix::io::AsRawFd. [BC] 72 | 73 | ## [0.1.2] - 2019-04-04 74 | ### Added 75 | - Add add_{vertical/horizontal} methods to Demand2D. 76 | ### Changed 77 | - Allow construction of Terminals from arbitrary `io::Write`s. 78 | 79 | ## [0.1.1] - 2019-03-23 80 | ### Fixed 81 | - Correctly specified MIT license. 82 | 83 | ## [0.1.0] - 2019-03-23 84 | ### Added 85 | - Initial release. 86 | -------------------------------------------------------------------------------- /src/container/boxdrawing.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for unicode box characters 2 | use base::GraphemeCluster; 3 | 4 | /// Components of unicode box characters. A single character can contain up to 4 segments. 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | #[allow(missing_docs)] 7 | pub enum LineSegment { 8 | Up, 9 | Down, 10 | Right, 11 | Left, 12 | } 13 | impl LineSegment { 14 | /// c.f. CELL_TO_CHAR lookup table 15 | fn to_u8(self) -> u8 { 16 | match self { 17 | LineSegment::Up => 0b00000001, 18 | LineSegment::Down => 0b00000100, 19 | LineSegment::Right => 0b00010000, 20 | LineSegment::Left => 0b01000000, 21 | } 22 | } 23 | } 24 | 25 | /// The type of a segment of a unicode box character 26 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 27 | #[allow(missing_docs)] 28 | pub enum LineType { 29 | None, 30 | Thin, 31 | Thick, 32 | } 33 | impl LineType { 34 | /// c.f. CELL_TO_CHAR lookup table 35 | fn to_u8(self) -> u8 { 36 | match self { 37 | LineType::None => 0b00, 38 | LineType::Thin => 0b01, 39 | LineType::Thick => 0b10, 40 | } 41 | } 42 | } 43 | 44 | /// A single box character, initially empty. 45 | #[derive(Copy, Clone, Debug)] 46 | pub struct LineCell { 47 | components: u8, 48 | } 49 | 50 | impl LineCell { 51 | /// Create an empty box drawing cell (i.e., a space character) 52 | /// Add segments using `set`. 53 | pub fn empty() -> Self { 54 | LineCell { components: 0 } 55 | } 56 | 57 | /// Convert the cell to a grapheme cluster (always safe). 58 | pub fn to_grapheme_cluster(self) -> GraphemeCluster { 59 | GraphemeCluster::try_from(CELL_TO_CHAR[self.components as usize]) 60 | .expect("CELL_TO_CHAR elements are single clusters") 61 | } 62 | 63 | /// Set one of the four segments of the cell to the specified type. 64 | pub fn set(&mut self, segment: LineSegment, ltype: LineType) -> &mut Self { 65 | let segment = segment.to_u8(); 66 | let ltype = ltype.to_u8(); 67 | let other_component_mask = !(segment * 0b11); 68 | self.components = (self.components & other_component_mask) | segment * ltype; 69 | self 70 | } 71 | } 72 | 73 | #[cfg_attr(rustfmt, rustfmt_skip)] 74 | const CELL_TO_CHAR: [char; 256] = [ 75 | ' ', '╵', '╹', '╳', 76 | '╷', '│', '╿', '╳', 77 | '╻', '╽', '┃', '╳', 78 | '╳', '╳', '╳', '╳', 79 | '╶', '└', '┖', '╳', 80 | '┌', '├', '┞', '╳', 81 | '┎', '┟', '┠', '╳', 82 | '╳', '╳', '╳', '╳', 83 | '╺', '┕', '┗', '╳', 84 | '┍', '┝', '┡', '╳', 85 | '┏', '┢', '┣', '╳', 86 | '╳', '╳', '╳', '╳', 87 | '╳', '╳', '╳', '╳', 88 | '╳', '╳', '╳', '╳', 89 | '╳', '╳', '╳', '╳', 90 | '╳', '╳', '╳', '╳', 91 | '╴', '┘', '┚', '╳', 92 | '┐', '┤', '┦', '╳', 93 | '┒', '┧', '┨', '╳', 94 | '╳', '╳', '╳', '╳', 95 | '─', '┴', '┸', '╳', 96 | '┬', '┼', '╀', '╳', 97 | '┰', '╁', '╂', '╳', 98 | '╳', '╳', '╳', '╳', 99 | '╼', '┶', '┺', '╳', 100 | '┮', '┾', '╄', '╳', 101 | '┲', '╆', '╊', '╳', 102 | '╳', '╳', '╳', '╳', 103 | '╳', '╳', '╳', '╳', 104 | '╳', '╳', '╳', '╳', 105 | '╳', '╳', '╳', '╳', 106 | '╳', '╳', '╳', '╳', 107 | '╸', '┙', '┛', '╳', 108 | '┑', '┥', '┩', '╳', 109 | '┓', '┪', '┫', '╳', 110 | '╳', '╳', '╳', '╳', 111 | '╾', '┵', '┹', '╳', 112 | '┭', '┽', '╃', '╳', 113 | '┱', '╅', '╉', '╳', 114 | '╳', '╳', '╳', '╳', 115 | '━', '┷', '┻', '╳', 116 | '┯', '┿', '╇', '╳', 117 | '┳', '╈', '╋', '╳', 118 | '╳', '╳', '╳', '╳', 119 | '╳', '╳', '╳', '╳', 120 | '╳', '╳', '╳', '╳', 121 | '╳', '╳', '╳', '╳', 122 | '╳', '╳', '╳', '╳', 123 | '╳', '╳', '╳', '╳', 124 | '╳', '╳', '╳', '╳', 125 | '╳', '╳', '╳', '╳', 126 | '╳', '╳', '╳', '╳', 127 | '╳', '╳', '╳', '╳', 128 | '╳', '╳', '╳', '╳', 129 | '╳', '╳', '╳', '╳', 130 | '╳', '╳', '╳', '╳', 131 | '╳', '╳', '╳', '╳', 132 | '╳', '╳', '╳', '╳', 133 | '╳', '╳', '╳', '╳', 134 | '╳', '╳', '╳', '╳', 135 | '╳', '╳', '╳', '╳', 136 | '╳', '╳', '╳', '╳', 137 | '╳', '╳', '╳', '╳', 138 | '╳', '╳', '╳', '╳', 139 | ]; 140 | -------------------------------------------------------------------------------- /src/widget/builtin/logviewer.rs: -------------------------------------------------------------------------------- 1 | //! A scrollable, append-only buffer of lines. 2 | use base::basic_types::*; 3 | use base::{Cursor, Window, WrappingMode}; 4 | use input::{OperationResult, Scrollable}; 5 | use std::fmt; 6 | use std::ops::Range; 7 | use widget::{Demand, Demand2D, RenderingHints, Widget}; 8 | 9 | /// A scrollable, append-only buffer of lines. 10 | pub struct LogViewer { 11 | storage: Vec, // Invariant: always holds at least one line, does not contain newlines 12 | scrollback_position: Option, 13 | scroll_step: usize, 14 | } 15 | 16 | impl LogViewer { 17 | /// Create an empty `LogViewer`. Add lines by writing to the viewer as `std::io::Write`. 18 | pub fn new() -> Self { 19 | let mut storage = Vec::new(); 20 | storage.push(String::new()); //Fullfil invariant (at least one line) 21 | LogViewer { 22 | storage: storage, 23 | scrollback_position: None, 24 | scroll_step: 1, 25 | } 26 | } 27 | 28 | fn num_lines_stored(&self) -> usize { 29 | self.storage.len() // Per invariant: no newlines in storage 30 | } 31 | 32 | fn current_line_index(&self) -> LineIndex { 33 | self.scrollback_position.unwrap_or(LineIndex::new( 34 | self.num_lines_stored().checked_sub(1).unwrap_or(0), 35 | )) 36 | } 37 | 38 | /// Note: Do not insert newlines into the string using this 39 | fn active_line_mut(&mut self) -> &mut String { 40 | self.storage 41 | .last_mut() 42 | .expect("Invariant: At least one line") 43 | } 44 | 45 | fn view(&self, range: Range) -> &[String] { 46 | &self.storage[range.start.raw_value()..range.end.raw_value()] 47 | } 48 | 49 | /// Prepare for drawing as a `Widget`. 50 | pub fn as_widget<'a>(&'a self) -> impl Widget + 'a { 51 | LogViewerWidget { inner: self } 52 | } 53 | } 54 | 55 | impl fmt::Write for LogViewer { 56 | fn write_str(&mut self, s: &str) -> fmt::Result { 57 | let mut s = s.to_owned(); 58 | 59 | while let Some(newline_offset) = s.find('\n') { 60 | let mut line: String = s.drain(..(newline_offset + 1)).collect(); 61 | line.pop(); //Remove the \n 62 | self.active_line_mut().push_str(&line); 63 | self.storage.push(String::new()); 64 | } 65 | self.active_line_mut().push_str(&s); 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl Scrollable for LogViewer { 71 | fn scroll_forwards(&mut self) -> OperationResult { 72 | let current = self.current_line_index(); 73 | let candidate = current + self.scroll_step; 74 | self.scrollback_position = if candidate.raw_value() < self.num_lines_stored() { 75 | Some(candidate) 76 | } else { 77 | None 78 | }; 79 | if self.scrollback_position.is_some() { 80 | Ok(()) 81 | } else { 82 | Err(()) 83 | } 84 | } 85 | fn scroll_backwards(&mut self) -> OperationResult { 86 | let current = self.current_line_index(); 87 | let op_res = if current.raw_value() != 0 { 88 | Ok(()) 89 | } else { 90 | Err(()) 91 | }; 92 | self.scrollback_position = Some( 93 | current 94 | .checked_sub(self.scroll_step) 95 | .unwrap_or(LineIndex::new(0)), 96 | ); 97 | op_res 98 | } 99 | fn scroll_to_beginning(&mut self) -> OperationResult { 100 | if Some(LineIndex::new(0)) == self.scrollback_position { 101 | Err(()) 102 | } else { 103 | self.scrollback_position = Some(LineIndex::new(0)); 104 | Ok(()) 105 | } 106 | } 107 | fn scroll_to_end(&mut self) -> OperationResult { 108 | if self.scrollback_position.is_none() { 109 | Err(()) 110 | } else { 111 | self.scrollback_position = None; 112 | Ok(()) 113 | } 114 | } 115 | } 116 | 117 | struct LogViewerWidget<'a> { 118 | inner: &'a LogViewer, 119 | } 120 | 121 | impl<'a> Widget for LogViewerWidget<'a> { 122 | fn space_demand(&self) -> Demand2D { 123 | Demand2D { 124 | width: Demand::at_least(1), 125 | height: Demand::at_least(1), 126 | } 127 | } 128 | fn draw(&self, mut window: Window, _: RenderingHints) { 129 | let height = window.get_height(); 130 | if height == 0 { 131 | return; 132 | } 133 | 134 | // TODO: This does not work well when lines are wrapped, but we may want scrolling farther 135 | // than 1 line per event 136 | // self.scroll_step = ::std::cmp::max(1, height.checked_sub(1).unwrap_or(1)); 137 | 138 | let y_start = height - 1; 139 | let mut cursor = Cursor::new(&mut window) 140 | .position(ColIndex::new(0), y_start.from_origin()) 141 | .wrapping_mode(WrappingMode::Wrap); 142 | let end_line = self.inner.current_line_index(); 143 | let start_line = 144 | LineIndex::new(end_line.raw_value().checked_sub(height.into()).unwrap_or(0)); 145 | for line in self.inner.view(start_line..(end_line + 1)).iter().rev() { 146 | let num_auto_wraps = cursor.num_expected_wraps(&line) as i32; 147 | cursor.move_by(ColDiff::new(0), RowDiff::new(-num_auto_wraps)); 148 | cursor.writeln(&line); 149 | cursor.move_by(ColDiff::new(0), RowDiff::new(-num_auto_wraps) - 2); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/base/grapheme_cluster.rs: -------------------------------------------------------------------------------- 1 | //! Types related to grapheme cluster in utf8 encoding. 2 | 3 | use smallvec::SmallVec; 4 | use std::str::FromStr; 5 | use unicode_segmentation::{Graphemes, UnicodeSegmentation}; 6 | 7 | /// A single grapheme cluster encoded in utf8. It may consist of multiple bytes or even multiple chars. For details 8 | /// on what a grapheme cluster is, read [this](http://utf8everywhere.org/) or similar. 9 | #[derive(Clone, Debug, PartialEq)] 10 | pub struct GraphemeCluster { 11 | // Invariant: the contents of bytes is always valid utf8! 12 | bytes: SmallVec<[u8; 16]>, 13 | } 14 | 15 | impl GraphemeCluster { 16 | /// Get the underlying grapheme cluster as a String slice. 17 | /// 18 | /// # Examples: 19 | /// 20 | /// ``` 21 | /// use unsegen::base::GraphemeCluster; 22 | /// assert_eq!(GraphemeCluster::try_from('a').unwrap().as_str(), "a"); 23 | /// ``` 24 | pub fn as_str<'a>(&'a self) -> &'a str { 25 | // This is safe because bytes is always valid utf8. 26 | unsafe { ::std::str::from_utf8_unchecked(&self.bytes) } 27 | } 28 | 29 | /// Helper: Create grapheme cluster from bytes. slice MUST be a single valid utf8 grapheme 30 | /// cluster. 31 | fn from_bytes(slice: &[u8]) -> Self { 32 | let vec = SmallVec::from_slice(slice); 33 | GraphemeCluster { bytes: vec } 34 | } 35 | 36 | /// Create a grapheme cluster from something string-like. string MUST be a single grapheme 37 | /// cluster. 38 | pub(in base) fn from_str_unchecked>(string: S) -> Self { 39 | Self::from_bytes(&string.as_ref().as_bytes()[..]) 40 | } 41 | 42 | /// Create an empty (not actually real) grapheme cluster. This is used to pad cells in terminal 43 | /// window grids and not visible or usable outside. 44 | pub(in base) fn empty() -> Self { 45 | Self::from_str_unchecked("") 46 | } 47 | 48 | /// Add other to the current grapheme cluster. other MUST have a width of zero. 49 | pub(in base) fn merge_zero_width(&mut self, other: Self) { 50 | assert!(other.width() == 0, "Invalid merge"); 51 | self.bytes.extend_from_slice(&other.bytes[..]); 52 | } 53 | 54 | /// Safely create a single space character (i.e., 0x20) grapheme cluster. 55 | /// 56 | /// # Examples: 57 | /// 58 | /// ``` 59 | /// use unsegen::base::GraphemeCluster; 60 | /// assert_eq!(GraphemeCluster::space().as_str(), " "); 61 | /// ``` 62 | pub fn space() -> Self { 63 | Self::from_str_unchecked(" ") 64 | } 65 | 66 | /// Replace the current cluster with a single space character (i.e., 0x20) 67 | /// 68 | /// # Examples: 69 | /// 70 | /// ``` 71 | /// use unsegen::base::GraphemeCluster; 72 | /// let mut cluster = GraphemeCluster::try_from('a').unwrap(); 73 | /// cluster.clear(); 74 | /// assert_eq!(cluster.as_str(), " "); 75 | /// ``` 76 | pub fn clear(&mut self) { 77 | *self = Self::space(); 78 | } 79 | 80 | /// Try to create a grapheme cluster from a character. If c is not a single grapheme cluster, a 81 | /// GraphemeClusterError is returned. 82 | /// 83 | /// # Examples: 84 | /// 85 | /// ``` 86 | /// use unsegen::base::GraphemeCluster; 87 | /// assert_eq!(GraphemeCluster::try_from('a').unwrap().as_str(), "a"); 88 | /// ``` 89 | pub fn try_from(c: char) -> Result { 90 | Self::from_str(c.to_string().as_ref()) 91 | } 92 | 93 | /// Retrieve all grapheme clusters from the given string. 94 | /// 95 | /// # Examples: 96 | /// 97 | /// ``` 98 | /// use unsegen::base::GraphemeCluster; 99 | /// let mut clusters = GraphemeCluster::all_from_str("ab d"); 100 | /// assert_eq!(clusters.next(), Some(GraphemeCluster::try_from('a').unwrap())); 101 | /// assert_eq!(clusters.next(), Some(GraphemeCluster::try_from('b').unwrap())); 102 | /// assert_eq!(clusters.next(), Some(GraphemeCluster::try_from(' ').unwrap())); 103 | /// assert_eq!(clusters.next(), Some(GraphemeCluster::try_from('d').unwrap())); 104 | /// assert_eq!(clusters.next(), None); 105 | /// ``` 106 | pub fn all_from_str<'a>(string: &'a str) -> GraphemeClusterIter<'a> { 107 | GraphemeClusterIter::new(string) 108 | } 109 | 110 | /// Calculate the unicode width of the given grapheme cluster. 111 | /// 112 | /// # Examples: 113 | /// 114 | /// ``` 115 | /// use unsegen::base::GraphemeCluster; 116 | /// assert_eq!(GraphemeCluster::try_from('a').unwrap().width(), 1); 117 | /// ``` 118 | pub fn width(&self) -> usize { 119 | ::unicode_width::UnicodeWidthStr::width(self.as_str()) 120 | } 121 | } 122 | 123 | /// An iterator over a sequence of grapheme clusters 124 | pub struct GraphemeClusterIter<'a> { 125 | graphemes: Graphemes<'a>, 126 | } 127 | 128 | impl<'a> GraphemeClusterIter<'a> { 129 | fn new(string: &'a str) -> Self { 130 | GraphemeClusterIter { 131 | graphemes: string.graphemes(true), 132 | } 133 | } 134 | } 135 | 136 | impl<'a> Iterator for GraphemeClusterIter<'a> { 137 | type Item = GraphemeCluster; 138 | fn next(&mut self) -> Option { 139 | self.graphemes.next().map(|s| 140 | // We trust the implementation of unicode_segmentation 141 | GraphemeCluster::from_str_unchecked(s)) 142 | } 143 | } 144 | 145 | /// An error associated with the creation of GraphemeCluster from arbitrary strings. 146 | #[derive(Debug)] 147 | #[allow(missing_docs)] 148 | pub enum GraphemeClusterError { 149 | MultipleGraphemeClusters, 150 | NoGraphemeCluster, 151 | } 152 | 153 | /* 154 | FIXME: TryFrom is still unstable: https://github.com/rust-lang/rust/issues/33417 155 | impl TryFrom for GraphemeCluster { 156 | type Err = GraphemeClusterError; 157 | fn try_from(text: char) -> Result { 158 | let mut clusters = text.graphemes(true); 159 | let res = if let Some(cluster) = clusters.next() { 160 | Self::from_str_unchecked(cluster) 161 | } else { 162 | Err(GraphemeClusterError::NoGraphemeCluster); 163 | }; 164 | } 165 | } 166 | */ 167 | 168 | impl FromStr for GraphemeCluster { 169 | type Err = GraphemeClusterError; 170 | fn from_str(text: &str) -> Result { 171 | let mut clusters = GraphemeCluster::all_from_str(text); 172 | let res = clusters 173 | .next() 174 | .ok_or(GraphemeClusterError::NoGraphemeCluster); 175 | if clusters.next().is_none() { 176 | res 177 | } else { 178 | Err(GraphemeClusterError::MultipleGraphemeClusters) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/widget/builtin/lineedit.rs: -------------------------------------------------------------------------------- 1 | //! A user-editable line of text. 2 | use base::basic_types::*; 3 | use base::{BoolModifyMode, Cursor, StyleModifier, Window}; 4 | use input::{Editable, Navigatable, OperationResult, Writable}; 5 | use unicode_segmentation::UnicodeSegmentation; 6 | use widget::{ 7 | count_grapheme_clusters, text_width, Blink, Demand, Demand2D, RenderingHints, Widget, 8 | }; 9 | 10 | /// A user-editable line of text. 11 | /// 12 | /// In addition to the current text, the `LineEdit` has a concept of a cursor whose position can 13 | /// change, but is always on a grapheme cluster in the current text. 14 | pub struct LineEdit { 15 | text: String, 16 | cursor_pos: usize, 17 | } 18 | 19 | impl LineEdit { 20 | /// Create empty LineEdit 21 | pub fn new() -> Self { 22 | LineEdit { 23 | text: String::new(), 24 | cursor_pos: 0, 25 | } 26 | } 27 | 28 | /// Get the current content. 29 | pub fn get(&self) -> &str { 30 | &self.text 31 | } 32 | 33 | /// Set (and overwrite) the current content. The cursor will be placed at the very end of the 34 | /// line. 35 | pub fn set(&mut self, text: impl Into) { 36 | self.text = text.into(); 37 | self.move_cursor_to_end_of_line(); 38 | } 39 | 40 | /// Move the cursor to the end, i.e., *behind* the last grapheme cluster. 41 | pub fn move_cursor_to_end_of_line(&mut self) { 42 | self.cursor_pos = count_grapheme_clusters(&self.text) as usize; 43 | } 44 | 45 | /// Move the cursor to the beginning, i.e., *onto* the first grapheme cluster. 46 | pub fn move_cursor_to_beginning_of_line(&mut self) { 47 | self.cursor_pos = 0; 48 | } 49 | 50 | /// Move the cursor one grapheme cluster to the right if possible. 51 | pub fn move_cursor_right(&mut self) -> Result<(), ()> { 52 | let new_pos = self.cursor_pos + 1; 53 | if new_pos <= count_grapheme_clusters(&self.text) as usize { 54 | self.cursor_pos = new_pos; 55 | Ok(()) 56 | } else { 57 | Err(()) 58 | } 59 | } 60 | 61 | /// Move the cursor one grapheme cluster to the left if possible. 62 | pub fn move_cursor_left(&mut self) -> Result<(), ()> { 63 | if self.cursor_pos > 0 { 64 | self.cursor_pos -= 1; 65 | Ok(()) 66 | } else { 67 | Err(()) 68 | } 69 | } 70 | 71 | /// Insert text directly *before* the current cursor position 72 | pub fn insert(&mut self, text: &str) { 73 | self.text = { 74 | let grapheme_iter = self.text.graphemes(true); 75 | grapheme_iter 76 | .clone() 77 | .take(self.cursor_pos) 78 | .chain(Some(text)) 79 | .chain(grapheme_iter.skip(self.cursor_pos)) 80 | .collect() 81 | }; 82 | } 83 | 84 | /// Returns the byte position of the cursor in the current text (obtainable by `get`) 85 | pub fn cursor_pos(&self) -> usize { 86 | self.text 87 | .grapheme_indices(true) 88 | .nth(self.cursor_pos) 89 | .map(|(index, _)| index) 90 | .unwrap_or_else(|| self.text.len()) 91 | } 92 | 93 | /// Set the cursor by specifying its position as the byte position in the displayed string. 94 | /// 95 | /// If the byte position does not correspond to (the start of) a grapheme cluster in the string 96 | /// or the end of the string, an error is returned and the cursor position is left unchanged. 97 | /// 98 | /// # Examples: 99 | /// ``` 100 | /// use unsegen::widget::builtin::LineEdit; 101 | /// 102 | /// let mut l = LineEdit::new(); 103 | /// l.set("löl"); 104 | /// assert!(l.set_cursor_pos(0).is_ok()); // |löl 105 | /// assert!(l.set_cursor_pos(1).is_ok()); // l|öl 106 | /// assert!(l.set_cursor_pos(2).is_err()); 107 | /// assert!(l.set_cursor_pos(3).is_ok()); // lö|l 108 | /// assert!(l.set_cursor_pos(4).is_ok()); // löl| 109 | /// assert!(l.set_cursor_pos(5).is_err()); 110 | /// ``` 111 | pub fn set_cursor_pos(&mut self, pos: usize) -> Result<(), ()> { 112 | if let Some(grapheme_index) = self 113 | .text 114 | .grapheme_indices(true) 115 | .enumerate() 116 | .find(|(_, (byte_index, _))| *byte_index == pos) 117 | .map(|(grapheme_index, _)| grapheme_index) 118 | .or_else(|| { 119 | if pos == self.text.len() { 120 | Some(count_grapheme_clusters(&self.text)) 121 | } else { 122 | None 123 | } 124 | }) 125 | { 126 | self.cursor_pos = grapheme_index; 127 | Ok(()) 128 | } else { 129 | Err(()) 130 | } 131 | } 132 | 133 | /// Erase the grapheme cluster at the specified (grapheme cluster) position. 134 | fn erase_symbol_at(&mut self, pos: usize) -> Result<(), ()> { 135 | if pos < count_grapheme_clusters(&self.text) { 136 | self.text = self 137 | .text 138 | .graphemes(true) 139 | .enumerate() 140 | .filter_map(|(i, s)| if i != pos { Some(s) } else { None }) 141 | .collect(); 142 | Ok(()) 143 | } else { 144 | Err(()) 145 | } 146 | } 147 | 148 | /// Prepare for drawing as a `Widget`. 149 | pub fn as_widget<'a>(&'a self) -> LineEditWidget<'a> { 150 | LineEditWidget { 151 | lineedit: self, 152 | cursor_style_active_blink_on: StyleModifier::new().invert(BoolModifyMode::Toggle), 153 | cursor_style_active_blink_off: StyleModifier::new(), 154 | cursor_style_inactive: StyleModifier::new().underline(true), 155 | } 156 | } 157 | } 158 | 159 | /// Note that there is no concept of moving up or down for a `LineEdit`. 160 | impl Navigatable for LineEdit { 161 | fn move_up(&mut self) -> OperationResult { 162 | Err(()) 163 | } 164 | fn move_down(&mut self) -> OperationResult { 165 | Err(()) 166 | } 167 | fn move_left(&mut self) -> OperationResult { 168 | self.move_cursor_left() 169 | } 170 | fn move_right(&mut self) -> OperationResult { 171 | self.move_cursor_right() 172 | } 173 | } 174 | 175 | impl Writable for LineEdit { 176 | fn write(&mut self, c: char) -> OperationResult { 177 | if c == '\n' { 178 | Err(()) 179 | } else { 180 | self.insert(&c.to_string()); 181 | self.move_cursor_right() 182 | } 183 | } 184 | } 185 | 186 | impl Editable for LineEdit { 187 | fn delete_forwards(&mut self) -> OperationResult { 188 | //i.e., "del" key 189 | let to_erase = self.cursor_pos; 190 | self.erase_symbol_at(to_erase) 191 | } 192 | fn delete_backwards(&mut self) -> OperationResult { 193 | //i.e., "backspace" 194 | if self.cursor_pos > 0 { 195 | let to_erase = self.cursor_pos - 1; 196 | let _ = self.erase_symbol_at(to_erase); 197 | let _ = self.move_cursor_left(); 198 | Ok(()) 199 | } else { 200 | Err(()) 201 | } 202 | } 203 | fn go_to_beginning_of_line(&mut self) -> OperationResult { 204 | self.move_cursor_to_beginning_of_line(); 205 | Ok(()) 206 | } 207 | fn go_to_end_of_line(&mut self) -> OperationResult { 208 | self.move_cursor_to_end_of_line(); 209 | Ok(()) 210 | } 211 | fn clear(&mut self) -> OperationResult { 212 | if self.text.is_empty() { 213 | Err(()) 214 | } else { 215 | self.text.clear(); 216 | self.cursor_pos = 0; 217 | Ok(()) 218 | } 219 | } 220 | } 221 | 222 | /// A `Widget` representing a `LineEdit` 223 | /// 224 | /// It allows for customization of cursor styles. 225 | pub struct LineEditWidget<'a> { 226 | lineedit: &'a LineEdit, 227 | cursor_style_active_blink_on: StyleModifier, 228 | cursor_style_active_blink_off: StyleModifier, 229 | cursor_style_inactive: StyleModifier, 230 | } 231 | 232 | impl<'a> LineEditWidget<'a> { 233 | /// Define the style that the cursor will be drawn with on the "on" tick when the widget is 234 | /// active. 235 | pub fn cursor_blink_on(mut self, style: StyleModifier) -> Self { 236 | self.cursor_style_active_blink_on = style; 237 | self 238 | } 239 | 240 | /// Define the style that the cursor will be drawn with on the "off" tick when the widget is 241 | /// active. 242 | pub fn cursor_blink_off(mut self, style: StyleModifier) -> Self { 243 | self.cursor_style_active_blink_off = style; 244 | self 245 | } 246 | 247 | /// Define the style that the cursor will be drawn with when the widget is inactive. 248 | pub fn cursor_inactive(mut self, style: StyleModifier) -> Self { 249 | self.cursor_style_inactive = style; 250 | self 251 | } 252 | } 253 | 254 | impl<'a> Widget for LineEditWidget<'a> { 255 | fn space_demand(&self) -> Demand2D { 256 | Demand2D { 257 | width: Demand::at_least(text_width(&self.lineedit.text) + 1), 258 | height: Demand::exact(1), 259 | } 260 | } 261 | fn draw(&self, mut window: Window, hints: RenderingHints) { 262 | let (maybe_cursor_pos_offset, maybe_after_cursor_offset) = { 263 | let mut grapheme_indices = self.lineedit.text.grapheme_indices(true); 264 | let cursor_cluster = grapheme_indices.nth(self.lineedit.cursor_pos as usize); 265 | let next_cluster = grapheme_indices.next(); 266 | ( 267 | cursor_cluster.map(|c: (usize, &str)| c.0), 268 | next_cluster.map(|c: (usize, &str)| c.0), 269 | ) 270 | }; 271 | let right_padding = 1; 272 | let text_width_before_cursor = text_width( 273 | &self.lineedit.text[0..maybe_after_cursor_offset.unwrap_or(self.lineedit.text.len())], 274 | ); 275 | let draw_cursor_start_pos = ::std::cmp::min( 276 | ColIndex::new(0), 277 | (window.get_width() - text_width_before_cursor - right_padding).from_origin(), 278 | ); 279 | 280 | let cursor_style = match (hints.active, hints.blink) { 281 | (true, Blink::On) => self.cursor_style_active_blink_on, 282 | (true, Blink::Off) => self.cursor_style_active_blink_off, 283 | (false, _) => self.cursor_style_inactive, 284 | }; 285 | 286 | let mut cursor = Cursor::new(&mut window).position(draw_cursor_start_pos, RowIndex::new(0)); 287 | if let Some(cursor_pos_offset) = maybe_cursor_pos_offset { 288 | let (until_cursor, from_cursor) = self.lineedit.text.split_at(cursor_pos_offset); 289 | cursor.write(until_cursor); 290 | if let Some(after_cursor_offset) = maybe_after_cursor_offset { 291 | let (cursor_str, after_cursor) = 292 | from_cursor.split_at(after_cursor_offset - cursor_pos_offset); 293 | { 294 | let mut cursor = cursor.save().style_modifier(); 295 | cursor.apply_style_modifier(cursor_style); 296 | cursor.write(cursor_str); 297 | } 298 | cursor.write(after_cursor); 299 | } else { 300 | let mut cursor = cursor.save().style_modifier(); 301 | cursor.apply_style_modifier(cursor_style); 302 | cursor.write(from_cursor); 303 | } 304 | } else { 305 | cursor.write(&self.lineedit.text); 306 | { 307 | let mut cursor = cursor.save().style_modifier(); 308 | cursor.apply_style_modifier(cursor_style); 309 | cursor.write(" "); 310 | } 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/base/terminal.rs: -------------------------------------------------------------------------------- 1 | //! The entry point module for presenting data to the terminal. 2 | //! 3 | //! To get started, create a terminal, create a root window from it, use it to render stuff to the 4 | //! terminal and finally present the terminal content to the physical terminal. 5 | //! 6 | //! # Examples: 7 | //! 8 | //! ```no_run 9 | //! use unsegen::base::Terminal; 10 | //! use std::io::stdout; 11 | //! let stdout = stdout(); 12 | //! let mut term = Terminal::new(stdout.lock()).unwrap(); 13 | //! 14 | //! let mut done = false; 15 | //! while !done { 16 | //! // Process data, update data structures 17 | //! done = true; // or whatever condition you like 18 | //! 19 | //! { 20 | //! let win = term.create_root_window(); 21 | //! // use win to draw something 22 | //! } 23 | //! term.present(); 24 | //! 25 | //! } 26 | //! ``` 27 | use base::{Height, Style, Width, Window, WindowBuffer}; 28 | use ndarray::Axis; 29 | use raw_tty::TtyWithGuard; 30 | use std::io; 31 | use std::io::{StdoutLock, Write}; 32 | use std::os::unix::io::AsRawFd; 33 | use termion; 34 | 35 | use nix::sys::signal::{killpg, pthread_sigmask, SigSet, SigmaskHow, SIGCONT, SIGTSTP}; 36 | use nix::unistd::getpgrp; 37 | 38 | /// A type providing an interface to the underlying physical terminal. 39 | /// This also provides the entry point for any rendering to the terminal buffer. 40 | pub struct Terminal<'a, T = StdoutLock<'a>> 41 | where 42 | T: AsRawFd + Write, 43 | { 44 | values: WindowBuffer, 45 | old_values: WindowBuffer, 46 | terminal: TtyWithGuard, 47 | size_has_changed_since_last_present: bool, 48 | bell_to_emit: bool, 49 | _phantom: ::std::marker::PhantomData<&'a ()>, 50 | } 51 | 52 | impl<'a, T: Write + AsRawFd> Terminal<'a, T> { 53 | /// Create a new terminal. The terminal takes control of the provided io sink (usually stdout) 54 | /// and performs all output on it. 55 | /// 56 | /// If the terminal cannot be created (e.g., because the provided io sink does not allow for 57 | /// setting up raw mode), the error is returned. 58 | pub fn new(sink: T) -> io::Result { 59 | let mut terminal = TtyWithGuard::new(sink)?; 60 | terminal.set_raw_mode()?; 61 | let mut term = Terminal { 62 | values: WindowBuffer::new(Width::new(0).unwrap(), Height::new(0).unwrap()), 63 | old_values: WindowBuffer::new(Width::new(0).unwrap(), Height::new(0).unwrap()), 64 | terminal, 65 | size_has_changed_since_last_present: true, 66 | bell_to_emit: false, 67 | _phantom: Default::default(), 68 | }; 69 | term.enter_tui()?; 70 | Ok(term) 71 | } 72 | 73 | /// This method is intended to be called when the process received a SIGTSTP. 74 | /// 75 | /// The terminal state is restored, and the process is actually stopped within this function. 76 | /// When the process then receives a SIGCONT it sets up the terminal state as expected again 77 | /// and returns from the function. 78 | /// 79 | /// The usual way to deal with SIGTSTP (and signals in general) is to block them and `waidpid` 80 | /// for them in a separate thread which sends the events into some fifo. The fifo can be polled 81 | /// in an event loop. Then, if in the main event loop a SIGTSTP turns up, *this* function 82 | /// should be called. 83 | pub fn handle_sigtstp(&mut self) -> io::Result<()> { 84 | self.leave_tui()?; 85 | 86 | let mut stop_and_cont = SigSet::empty(); 87 | stop_and_cont.add(SIGCONT); 88 | stop_and_cont.add(SIGTSTP); 89 | 90 | // 1. Unblock SIGTSTP and SIGCONT, so that we actually stop when we receive another SIGTSTP 91 | pthread_sigmask(SigmaskHow::SIG_UNBLOCK, Some(&stop_and_cont), None)?; 92 | 93 | // 2. Reissue SIGTSTP (this time to whole the process group!)... 94 | killpg(getpgrp(), SIGTSTP)?; 95 | // ... and stop! 96 | // Now we are waiting for a SIGCONT. 97 | 98 | // 3. Once we receive a SIGCONT we block SIGTSTP and SIGCONT again and resume. 99 | pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&stop_and_cont), None)?; 100 | 101 | self.enter_tui() 102 | } 103 | 104 | /// Set up the terminal for "full screen" work (i.e., hide cursor, switch to alternate screen). 105 | fn enter_tui(&mut self) -> io::Result<()> { 106 | write!( 107 | self.terminal, 108 | "{}{}", 109 | termion::screen::ToAlternateScreen, 110 | termion::cursor::Hide 111 | )?; 112 | self.terminal.set_raw_mode()?; 113 | self.terminal.flush()?; 114 | Ok(()) 115 | } 116 | 117 | /// Restore terminal from "full screen" (i.e., show cursor again, switch to main screen). 118 | fn leave_tui(&mut self) -> io::Result<()> { 119 | write!( 120 | self.terminal, 121 | "{}{}", 122 | termion::screen::ToMainScreen, 123 | termion::cursor::Show 124 | )?; 125 | self.terminal.modify_mode(|m| m)?; //Restore saved mode 126 | self.terminal.flush()?; 127 | Ok(()) 128 | } 129 | 130 | /// Temporarily switch back to main terminal screen, restore terminal state, then execute `f` 131 | /// and subsequently switch back to tui mode again. 132 | /// 133 | /// In other words: Execute a function `f` in "normal" terminal mode. This can be useful if the 134 | /// application executes a subprocess that is expected to take control of the tty temporarily. 135 | pub fn on_main_screen R>(&mut self, f: F) -> io::Result { 136 | self.leave_tui()?; 137 | let res = f(); 138 | self.enter_tui()?; 139 | Ok(res) 140 | } 141 | 142 | /// Create a root window that covers the whole terminal grid. 143 | /// 144 | /// Use the buffer to manipulate the current window buffer and use present subsequently to 145 | /// write out the buffer to the actual terminal. 146 | pub fn create_root_window(&mut self) -> Window { 147 | let (x, y) = termion::terminal_size().expect("get terminal size"); 148 | let x = Width::new(x as i32).unwrap(); 149 | let y = Height::new(y as i32).unwrap(); 150 | if x != self.values.as_window().get_width() || y != self.values.as_window().get_height() { 151 | self.size_has_changed_since_last_present = true; 152 | self.values = WindowBuffer::new(x, y); 153 | } else { 154 | self.values.as_window().clear(); 155 | } 156 | 157 | self.values.as_window() 158 | } 159 | 160 | /// Emit a bell character ('\a') on the next call to `present`. 161 | /// 162 | /// This will usually set an urgent hint on the terminal emulator, so it is useful to draw 163 | /// attention to the application. 164 | pub fn emit_bell(&mut self) { 165 | self.bell_to_emit = true; 166 | } 167 | 168 | /// Present the current buffer content to the actual terminal. 169 | pub fn present(&mut self) { 170 | let mut current_style = Style::default(); 171 | 172 | let mut num_potentially_unchanged_lines = self.old_values.storage().dim().0; 173 | 174 | if self.size_has_changed_since_last_present { 175 | write!(self.terminal, "{}", termion::clear::All).expect("clear"); 176 | self.size_has_changed_since_last_present = false; 177 | num_potentially_unchanged_lines = 0; 178 | } 179 | if self.bell_to_emit { 180 | write!(self.terminal, "\x07").expect("emit bell"); 181 | self.bell_to_emit = false; 182 | } 183 | for (y, line) in self.values.storage().axis_iter(Axis(0)).enumerate() { 184 | if y < num_potentially_unchanged_lines 185 | && self.old_values.storage().subview(Axis(0), y) == line 186 | { 187 | continue; 188 | } 189 | write!( 190 | self.terminal, 191 | "{}", 192 | termion::cursor::Goto(1, (y + 1) as u16) 193 | ) 194 | .expect("move cursor"); 195 | let mut buffer = String::with_capacity(line.len()); 196 | for c in line.iter() { 197 | if c.style != current_style { 198 | current_style.set_terminal_attributes(&mut self.terminal); 199 | write!(self.terminal, "{}", buffer).expect("write buffer"); 200 | buffer.clear(); 201 | current_style = c.style; 202 | } 203 | let grapheme_cluster = match c.grapheme_cluster.as_str() { 204 | c @ "\t" | c @ "\n" | c @ "\r" | c @ "\0" => { 205 | panic!("Invalid grapheme cluster written to terminal: {:?}", c) 206 | } 207 | x => x, 208 | }; 209 | buffer.push_str(grapheme_cluster); 210 | } 211 | current_style.set_terminal_attributes(&mut self.terminal); 212 | write!(self.terminal, "{}", buffer).expect("write leftover buffer contents"); 213 | } 214 | let _ = self.terminal.flush(); 215 | self.old_values = self.values.clone(); 216 | } 217 | } 218 | 219 | impl<'a, T: Write + AsRawFd> Drop for Terminal<'a, T> { 220 | fn drop(&mut self) { 221 | let _ = self.leave_tui(); 222 | } 223 | } 224 | 225 | /// Contains a FakeTerminal useful for tests 226 | pub mod test { 227 | use super::super::{ 228 | GraphemeCluster, Height, Style, StyleModifier, StyledGraphemeCluster, Width, Window, 229 | WindowBuffer, 230 | }; 231 | 232 | /// A fake terminal that can be used in tests to create windows and compare the resulting 233 | /// contents to the expected contents of windows. 234 | #[derive(PartialEq)] 235 | pub struct FakeTerminal { 236 | values: WindowBuffer, 237 | } 238 | impl FakeTerminal { 239 | /// Create a window with the specified (width, height). 240 | pub fn with_size((w, h): (u32, u32)) -> Self { 241 | FakeTerminal { 242 | values: WindowBuffer::new( 243 | Width::new(w as i32).unwrap(), 244 | Height::new(h as i32).unwrap(), 245 | ), 246 | } 247 | } 248 | 249 | /// Create a fake terminal from a format string that looks roughly like this: 250 | /// 251 | /// "1 1 2 2 3 3 4 4" 252 | /// 253 | /// Spaces and newlines are ignored and the string is coerced into the specified size. 254 | /// 255 | /// The following characters have a special meaning: 256 | /// * - toggle bold style 257 | pub fn from_str( 258 | (w, h): (u32, u32), 259 | description: &str, 260 | ) -> Result { 261 | let mut tiles = Vec::::new(); 262 | let mut style = Style::plain(); 263 | for c in GraphemeCluster::all_from_str(description) { 264 | if c.as_str() == "*" { 265 | style = StyleModifier::new() 266 | .bold(crate::base::BoolModifyMode::Toggle) 267 | .apply(style); 268 | continue; 269 | } 270 | if c.as_str() == " " || c.as_str() == "\n" { 271 | continue; 272 | } 273 | tiles.push(StyledGraphemeCluster::new(c, style)); 274 | } 275 | Ok(FakeTerminal { 276 | values: WindowBuffer::from_storage(::ndarray::Array2::from_shape_vec( 277 | (h as usize, w as usize), 278 | tiles, 279 | )?), 280 | }) 281 | } 282 | 283 | /// Test if the terminal contents look like the given format string. 284 | /// The rows are separated by a "|". 285 | /// 286 | /// # Examples: 287 | /// 288 | /// ``` 289 | /// use unsegen::base::terminal::test::FakeTerminal; 290 | /// use unsegen::base::GraphemeCluster; 291 | /// 292 | /// let mut term = FakeTerminal::with_size((2,3)); 293 | /// { 294 | /// let mut win = term.create_root_window(); 295 | /// win.fill(GraphemeCluster::try_from('_').unwrap()); 296 | /// } 297 | /// 298 | /// term.assert_looks_like("__|__|__"); 299 | /// ``` 300 | pub fn assert_looks_like(&self, string_description: &str) { 301 | assert_eq!(format!("{:?}", self), string_description); 302 | } 303 | 304 | /// Create a root window that covers the whole terminal grid. 305 | pub fn create_root_window(&mut self) -> Window { 306 | self.values.as_window() 307 | } 308 | } 309 | 310 | impl ::std::fmt::Debug for FakeTerminal { 311 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 312 | let raw_values = self.values.storage(); 313 | for r in 0..raw_values.dim().0 { 314 | for c in 0..raw_values.dim().1 { 315 | let c = raw_values.get((r, c)).expect("debug: in bounds"); 316 | if c.style.format().bold { 317 | write!(f, "*{}*", c.grapheme_cluster.as_str())?; 318 | } else { 319 | write!(f, "{}", c.grapheme_cluster.as_str())?; 320 | } 321 | } 322 | if r != raw_values.dim().0 - 1 { 323 | write!(f, "|")?; 324 | } 325 | } 326 | Ok(()) 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/widget/widget.rs: -------------------------------------------------------------------------------- 1 | //! The `Widget` abstraction and some related types. 2 | use base::basic_types::*; 3 | use base::{Cursor, Window, WrappingMode}; 4 | use std::cmp::max; 5 | use std::iter::Sum; 6 | use std::marker::PhantomData; 7 | use std::ops::{Add, AddAssign}; 8 | 9 | /// A widget is something that can be drawn to a window. 10 | pub trait Widget { 11 | /// Return the current demand for (rectangular) screen estate. 12 | /// 13 | /// The callee may report different 14 | /// demands on subsequent calls. 15 | fn space_demand(&self) -> Demand2D; 16 | 17 | /// Draw the widget to the given window. 18 | /// 19 | /// There is no guarantee that the window is of the size 20 | /// requested in `space_demand`, it can be smaller than the minimum or larger than the maximum 21 | /// (if specified). However, in general, the layouting algorithm tries to honor the demand of 22 | /// the widget. 23 | /// 24 | /// The hints give the widget some useful information on how to render. 25 | fn draw(&self, window: Window, hints: RenderingHints); 26 | } 27 | 28 | /// An extension trait to Widget which access to convenience methods that alters the behavior of 29 | /// the wrapped widgets. 30 | pub trait WidgetExt: Widget + Sized { 31 | /// Center the widget according to the specified maximum demand within the supplied window. 32 | /// This is only useful if the widget has a defined maximum size and the window is larger than 33 | /// that. 34 | fn centered(self) -> Centered { 35 | Centered(self) 36 | } 37 | 38 | /// Alter the window before letting the widget draw itself in it. 39 | fn with_window Window>(self, f: F) -> WithWindow { 40 | WithWindow(self, f) 41 | } 42 | 43 | /// Alter the rendering hints before rendering. 44 | /// to assume all space in the window or to artificially restrict the size of a widget. 45 | fn with_hints RenderingHints>(self, f: F) -> WithHints { 46 | WithHints(self, f) 47 | } 48 | 49 | /// Alter the reported demand of the widget. This can be useful, for example, to force a widget 50 | /// to assume all space in the window or to artificially restrict the size of a widget. 51 | fn with_demand Demand2D>(self, f: F) -> WithDemand { 52 | WithDemand(self, f) 53 | } 54 | } 55 | 56 | impl WidgetExt for W {} 57 | 58 | /// Center the widget according to the specified maximum demand within the supplied window. 59 | /// This is only useful if the widget has a defined maximum size and the window is larger than 60 | /// that. 61 | /// 62 | /// This wrapper can be created using `WidgetExt::centered`. 63 | pub struct Centered(W); 64 | 65 | impl Widget for Centered { 66 | fn space_demand(&self) -> Demand2D { 67 | self.0.space_demand() 68 | } 69 | fn draw(&self, mut window: Window, hints: RenderingHints) { 70 | let demand = self.space_demand(); 71 | 72 | let window_height = window.get_height(); 73 | let window_width = window.get_width(); 74 | 75 | let max_height = demand.height.max.unwrap_or(window.get_height()); 76 | let max_width = demand.width.max.unwrap_or(window.get_width()); 77 | 78 | let start_row = ((window_height - max_height) / 2) 79 | .from_origin() 80 | .positive_or_zero(); 81 | let start_col = ((window_width - max_width) / 2) 82 | .from_origin() 83 | .positive_or_zero(); 84 | let end_row = (start_row + max_height).min(window_height.from_origin()); 85 | let end_col = (start_col + max_width).min(window_width.from_origin()); 86 | 87 | let window = window.create_subwindow(start_col..end_col, start_row..end_row); 88 | self.0.draw(window, hints); 89 | } 90 | } 91 | 92 | /// Alter the window before letting the widget draw itself in it. 93 | /// 94 | /// This wrapper can be created using `WidgetExt::with_window`. 95 | pub struct WithWindow(W, F); 96 | 97 | impl Window> Widget for WithWindow { 98 | fn space_demand(&self) -> Demand2D { 99 | self.0.space_demand() 100 | } 101 | fn draw(&self, window: Window, hints: RenderingHints) { 102 | self.0.draw(self.1(window, hints), hints); 103 | } 104 | } 105 | 106 | /// Alter the rendering hints before drawing the wrapped widget. 107 | /// 108 | /// This wrapper can be created using `WidgetExt::with_hints`. 109 | pub struct WithHints(W, F); 110 | 111 | impl RenderingHints> Widget for WithHints { 112 | fn space_demand(&self) -> Demand2D { 113 | self.0.space_demand() 114 | } 115 | fn draw(&self, window: Window, hints: RenderingHints) { 116 | self.0.draw(window, self.1(hints)); 117 | } 118 | } 119 | 120 | /// Alter the reported demand of the widget. This can be useful, for example, to force a widget 121 | /// to assume all space in the window or to artificially restrict the size of a widget. 122 | /// 123 | /// This wrapper can be created using `WidgetExt::with_demand`. 124 | pub struct WithDemand(W, F); 125 | 126 | impl Demand2D> Widget for WithDemand { 127 | fn space_demand(&self) -> Demand2D { 128 | self.1(self.0.space_demand()) 129 | } 130 | fn draw(&self, window: Window, hints: RenderingHints) { 131 | self.0.draw(window, hints); 132 | } 133 | } 134 | 135 | impl> Widget for S { 136 | fn space_demand(&self) -> Demand2D { 137 | let mut width = 0; 138 | let mut height = 0; 139 | for line in self.as_ref().lines() { 140 | width = width.max(crate::widget::count_grapheme_clusters(line)); 141 | height += 1; 142 | } 143 | Demand2D { 144 | width: Demand::exact(width), 145 | height: Demand::exact(height), 146 | } 147 | } 148 | fn draw(&self, mut window: Window, _hints: RenderingHints) { 149 | let mut cursor = Cursor::new(&mut window).wrapping_mode(WrappingMode::Wrap); 150 | cursor.write(self.as_ref()); 151 | } 152 | } 153 | 154 | /// Hints that can be used by applications to control how Widgets are rendered and used by Widgets 155 | /// to deduce how to render to best show the current application state. 156 | #[derive(Clone, Copy, Debug)] 157 | pub struct RenderingHints { 158 | /// e.g., whether or not this Widget receives input 159 | pub active: bool, 160 | /// Periodic signal that can be used to e.g. let a cursor blink. 161 | pub blink: Blink, 162 | 163 | // Make users of the library unable to construct RenderingHints from members. 164 | // This way we can add members in a backwards compatible way in future versions. 165 | #[doc(hidden)] 166 | _do_not_construct: (), 167 | } 168 | 169 | impl Default for RenderingHints { 170 | fn default() -> Self { 171 | Self::new() 172 | } 173 | } 174 | 175 | impl RenderingHints { 176 | /// Construct a default hint object. 177 | pub fn new() -> Self { 178 | RenderingHints { 179 | active: true, 180 | blink: Blink::On, 181 | _do_not_construct: (), 182 | } 183 | } 184 | /// Hint on whether the widget is active, i.e., most of the time: It receives input. 185 | pub fn active(self, val: bool) -> Self { 186 | RenderingHints { 187 | active: val, 188 | ..self 189 | } 190 | } 191 | 192 | /// Use this to implement blinking effects for your widget. Usually, Blink can be expected to 193 | /// alternate every second or so. 194 | pub fn blink(self, val: Blink) -> Self { 195 | RenderingHints { blink: val, ..self } 196 | } 197 | } 198 | 199 | /// A value from a periodic boolean signal. 200 | /// 201 | /// Think of it like the state of an LED or cursor (block). 202 | #[derive(Clone, Copy, Debug)] 203 | #[allow(missing_docs)] 204 | pub enum Blink { 205 | On, 206 | Off, 207 | } 208 | 209 | impl Blink { 210 | /// Get the alternate on/off value. 211 | pub fn toggled(self) -> Self { 212 | match self { 213 | Blink::On => Blink::Off, 214 | Blink::Off => Blink::On, 215 | } 216 | } 217 | 218 | /// Change to the alternate on/off value. 219 | pub fn toggle(&mut self) { 220 | *self = self.toggled(); 221 | } 222 | } 223 | 224 | /// A one dimensional description of spatial demand of a widget. 225 | /// 226 | /// A Demand always has a minimum (although it may be zero) and may have a maximum. It is required 227 | /// that the minimum is smaller or equal to the maximum (if present). 228 | #[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Debug)] 229 | #[allow(missing_docs)] 230 | pub struct Demand { 231 | pub min: PositiveAxisDiff, 232 | pub max: Option>, 233 | _dim: PhantomData, 234 | } 235 | 236 | impl Add> for Demand { 237 | type Output = Self; 238 | fn add(self, rhs: Self) -> Self::Output { 239 | Demand { 240 | min: self.min + rhs.min, 241 | max: if let (Some(l), Some(r)) = (self.max, rhs.max) { 242 | Some(l + r) 243 | } else { 244 | None 245 | }, 246 | _dim: Default::default(), 247 | } 248 | } 249 | } 250 | impl AddAssign for Demand { 251 | fn add_assign(&mut self, rhs: Self) { 252 | *self = *self + rhs 253 | } 254 | } 255 | impl Sum for Demand { 256 | fn sum(iter: I) -> Self 257 | where 258 | I: Iterator, 259 | { 260 | iter.fold(Demand::exact(0), Demand::add) 261 | } 262 | } 263 | impl<'a, T: AxisDimension + PartialOrd + Ord> Sum<&'a Demand> for Demand { 264 | fn sum(iter: I) -> Demand 265 | where 266 | I: Iterator>, 267 | { 268 | iter.fold(Demand::zero(), |d1: Demand, d2: &Demand| d1 + *d2) 269 | } 270 | } 271 | 272 | impl Demand { 273 | /// A minimum and maximum demand of exactly 0. 274 | pub fn zero() -> Self { 275 | Self::exact(0) 276 | } 277 | 278 | /// A minimum and maximum demand of exactly the specified amount. 279 | pub fn exact> + Copy>(size: I) -> Self { 280 | Demand { 281 | min: size.into(), 282 | max: Some(size.into()), 283 | _dim: Default::default(), 284 | } 285 | } 286 | /// An specified minimum demand, but no defined maximum. 287 | pub fn at_least> + Copy>(size: I) -> Self { 288 | Demand { 289 | min: size.into(), 290 | max: None, 291 | _dim: Default::default(), 292 | } 293 | } 294 | /// A specified range of acceptable values between minimum and maximum. 295 | pub fn from_to> + Copy>(min: I, max: I) -> Self { 296 | assert!(min.into() <= max.into(), "Invalid min/max"); 297 | Demand { 298 | min: min.into(), 299 | max: Some(max.into()), 300 | _dim: Default::default(), 301 | } 302 | } 303 | 304 | /// Compute the composed maximum of two Demands. This is especially useful when building tables 305 | /// for example. 306 | /// 307 | /// # Examples: 308 | /// ``` 309 | /// use unsegen::widget::Demand; 310 | /// use unsegen::base::*; 311 | /// 312 | /// let d1 = Demand::::exact(5); 313 | /// let d2 = Demand::::at_least(0); 314 | /// 315 | /// assert_eq!(d1.max(d2), Demand::::at_least(5)); 316 | /// ``` 317 | pub fn max(&self, other: Self) -> Self { 318 | Demand { 319 | min: max(self.min, other.min), 320 | max: if let (Some(l), Some(r)) = (self.max, other.max) { 321 | Some(max(l, r)) 322 | } else { 323 | None 324 | }, 325 | _dim: Default::default(), 326 | } 327 | } 328 | 329 | /// Replace self with the maximum of self and other (see `Demand::max`). 330 | pub fn max_assign(&mut self, other: Self) { 331 | *self = self.max(other); 332 | } 333 | } 334 | 335 | /// Horizontal Demand 336 | pub type ColDemand = Demand; 337 | /// Vertical Demand 338 | pub type RowDemand = Demand; 339 | 340 | /// A two dimensional (rectangular) Demand (composed of `Demand`s for columns and rows). 341 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 342 | #[allow(missing_docs)] 343 | pub struct Demand2D { 344 | pub width: ColDemand, 345 | pub height: RowDemand, 346 | } 347 | 348 | impl Demand2D { 349 | /// Combine two `Demand2D`s by accumulating the height and making the width accommodate both. 350 | /// 351 | /// This is useful two compute the combined `Demand2D` of two widgets arranged on top of each 352 | /// other. 353 | /// 354 | /// # Examples: 355 | /// ``` 356 | /// use unsegen::base::*; 357 | /// use unsegen::widget::*; 358 | /// 359 | /// let d1 = Demand2D { 360 | /// width: ColDemand::exact(5), 361 | /// height: RowDemand::exact(5), 362 | /// }; 363 | /// let d2 = Demand2D { 364 | /// width: ColDemand::at_least(2), 365 | /// height: RowDemand::from_to(3, 5), 366 | /// }; 367 | /// 368 | /// assert_eq!( 369 | /// d1.add_vertical(d2), 370 | /// Demand2D { 371 | /// width: ColDemand::at_least(5), 372 | /// height: RowDemand::from_to(8, 10), 373 | /// } 374 | /// ); 375 | /// ``` 376 | pub fn add_vertical(self, other: Self) -> Self { 377 | Demand2D { 378 | width: self.width.max(other.width), 379 | height: self.height + other.height, 380 | } 381 | } 382 | 383 | /// Combine two `Demand2D`s by accumulating the width and making the height accommodate both. 384 | /// 385 | /// This is useful two compute the combined `Demand2D` of two widgets arranged on top of each 386 | /// other. 387 | /// 388 | /// # Examples: 389 | /// ``` 390 | /// use unsegen::base::*; 391 | /// use unsegen::widget::*; 392 | /// 393 | /// let d1 = Demand2D { 394 | /// width: ColDemand::exact(5), 395 | /// height: RowDemand::exact(5), 396 | /// }; 397 | /// let d2 = Demand2D { 398 | /// width: ColDemand::at_least(2), 399 | /// height: RowDemand::from_to(3, 5), 400 | /// }; 401 | /// 402 | /// assert_eq!( 403 | /// d1.add_horizontal(d2), 404 | /// Demand2D { 405 | /// width: ColDemand::at_least(7), 406 | /// height: RowDemand::exact(5), 407 | /// } 408 | /// ); 409 | /// ``` 410 | pub fn add_horizontal(self, other: Self) -> Self { 411 | Demand2D { 412 | width: self.width + other.width, 413 | height: self.height.max(other.height), 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/base/window.rs: -------------------------------------------------------------------------------- 1 | //! Types associated with Windows, i.e., rectangular views into a terminal buffer. 2 | use super::{CursorTarget, GraphemeCluster, Style, StyleModifier}; 3 | use base::basic_types::*; 4 | use base::cursor::{UNBOUNDED_HEIGHT, UNBOUNDED_WIDTH}; 5 | use ndarray::{Array, ArrayViewMut, Axis, Ix, Ix2}; 6 | use std::cmp::max; 7 | use std::fmt; 8 | use std::ops::{Bound, RangeBounds}; 9 | 10 | /// A GraphemeCluster with an associated style. 11 | #[derive(Clone, Debug, PartialEq)] 12 | #[allow(missing_docs)] 13 | pub struct StyledGraphemeCluster { 14 | pub grapheme_cluster: GraphemeCluster, 15 | pub style: Style, 16 | } 17 | 18 | impl StyledGraphemeCluster { 19 | /// Create a StyledGraphemeCluster from its components. 20 | pub fn new(grapheme_cluster: GraphemeCluster, style: Style) -> Self { 21 | StyledGraphemeCluster { 22 | grapheme_cluster: grapheme_cluster, 23 | style: style, 24 | } 25 | } 26 | } 27 | 28 | impl Default for StyledGraphemeCluster { 29 | fn default() -> Self { 30 | Self::new(GraphemeCluster::space(), Style::default()) 31 | } 32 | } 33 | 34 | pub(in base) type CharMatrix = Array; 35 | 36 | /// An owned buffer representing a Window. 37 | #[derive(PartialEq, Clone)] 38 | pub struct WindowBuffer { 39 | storage: CharMatrix, 40 | } 41 | 42 | impl WindowBuffer { 43 | /// Create a new WindowBuffer with the specified width and height. 44 | pub fn new(width: Width, height: Height) -> Self { 45 | WindowBuffer { 46 | storage: CharMatrix::default(Ix2(height.into(), width.into())), 47 | } 48 | } 49 | 50 | /// Create a WindowBuffer directly from a CharMatrix struct. 51 | pub(in base) fn from_storage(storage: CharMatrix) -> Self { 52 | WindowBuffer { storage: storage } 53 | } 54 | 55 | /// View the WindowBuffer as a Window. 56 | /// Use this method if you want to modify the contents of the buffer. 57 | pub fn as_window<'a>(&'a mut self) -> Window<'a> { 58 | Window::new(self.storage.view_mut()) 59 | } 60 | 61 | /// Get the underlying CharMatrix storage. 62 | pub(in base) fn storage(&self) -> &CharMatrix { 63 | &self.storage 64 | } 65 | } 66 | 67 | type CharMatrixView<'w> = ArrayViewMut<'w, StyledGraphemeCluster, Ix2>; 68 | 69 | /// A rectangular view into a terminal buffer, i.e., a grid of grapheme clusters. 70 | /// 71 | /// Moreover, a window always has a default style that is applied to all characters that are 72 | /// written to it. By default this is a "plain" style that does not change color or text format. 73 | /// 74 | /// Side note: Grapheme clusters do not always have a width of a singular cell, and thus 75 | /// things can quite complicated. Therefore, Multi-width characters occupy more than once cell in a 76 | /// single window. If any of the cells is overwritten, potentially left-over cells are overwritten 77 | /// with space characters. 78 | pub struct Window<'w> { 79 | values: CharMatrixView<'w>, 80 | default_style: Style, 81 | } 82 | 83 | impl<'w> ::std::fmt::Debug for Window<'w> { 84 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | let w: usize = self.get_width().into(); 86 | let h: usize = self.get_height().into(); 87 | write!(f, "Window {{ w: {}, h: {} }}", w, h) 88 | } 89 | } 90 | 91 | impl<'w> Window<'w> { 92 | /// Create a window from the underlying CharMatrixView and set a default (non-modifying) style. 93 | fn new(values: CharMatrixView<'w>) -> Self { 94 | Window { 95 | values: values, 96 | default_style: Style::default(), 97 | } 98 | } 99 | 100 | /// Get the extent of the window in the specified dimension (i.e., its width or height) 101 | pub fn get_extent(&self) -> PositiveAxisDiff { 102 | PositiveAxisDiff::new(D::get_dimension_value(self.values.dim()) as i32).unwrap() 103 | } 104 | 105 | /// Get the width (i.e., number of columns) that the window occupies. 106 | pub fn get_width(&self) -> Width { 107 | self.get_extent::() 108 | } 109 | 110 | /// Get the height (i.e., number of rows) that the window occupies. 111 | pub fn get_height(&self) -> Height { 112 | self.get_extent::() 113 | } 114 | 115 | /// Create a subview of the window. 116 | /// 117 | /// # Examples: 118 | /// ``` 119 | /// # use unsegen::base::terminal::test::FakeTerminal; 120 | /// # let mut term = FakeTerminal::with_size((5,5)); 121 | /// use unsegen::base::{ColIndex, RowIndex, GraphemeCluster}; 122 | /// 123 | /// let mut win = term.create_root_window(); 124 | /// { 125 | /// let mut w = win.create_subwindow( 126 | /// ColIndex::new(0) .. ColIndex::new(2), 127 | /// RowIndex::new(2) .. RowIndex::new(4) 128 | /// ); 129 | /// w.fill(GraphemeCluster::try_from('A').unwrap()); 130 | /// } 131 | /// { 132 | /// let mut w = win.create_subwindow( 133 | /// ColIndex::new(2) .. ColIndex::new(4), 134 | /// RowIndex::new(0) .. RowIndex::new(2) 135 | /// ); 136 | /// w.fill(GraphemeCluster::try_from('B').unwrap()); 137 | /// } 138 | /// ``` 139 | /// 140 | /// # Panics: 141 | /// 142 | /// Panics on invalid ranges, i.e., if: 143 | /// start > end, start < 0, or end > [width of the window] 144 | pub fn create_subwindow<'a, WX: RangeBounds, WY: RangeBounds>( 145 | &'a mut self, 146 | x_range: WX, 147 | y_range: WY, 148 | ) -> Window<'a> { 149 | let x_range_start = match x_range.start_bound() { 150 | Bound::Unbounded => ColIndex::new(0), 151 | Bound::Included(i) => *i, 152 | Bound::Excluded(i) => *i - 1, 153 | }; 154 | let x_range_end = match x_range.end_bound() { 155 | Bound::Unbounded => self.get_width().from_origin(), 156 | Bound::Included(i) => *i - 1, 157 | Bound::Excluded(i) => *i, 158 | }; 159 | let y_range_start = match y_range.start_bound() { 160 | Bound::Unbounded => RowIndex::new(0), 161 | Bound::Included(i) => *i, 162 | Bound::Excluded(i) => *i - 1, 163 | }; 164 | let y_range_end = match y_range.end_bound() { 165 | Bound::Unbounded => self.get_height().from_origin(), 166 | Bound::Included(i) => *i - 1, 167 | Bound::Excluded(i) => *i, 168 | }; 169 | assert!(x_range_start <= x_range_end, "Invalid x_range: start > end"); 170 | assert!(y_range_start <= y_range_end, "Invalid y_range: start > end"); 171 | assert!( 172 | x_range_end <= self.get_width().from_origin(), 173 | "Invalid x_range: end > width" 174 | ); 175 | assert!(x_range_start >= 0, "Invalid x_range: start < 0"); 176 | assert!( 177 | y_range_end <= self.get_height().from_origin(), 178 | "Invalid y_range: end > height" 179 | ); 180 | assert!(y_range_start >= 0, "Invalid y_range: start < 0"); 181 | 182 | let sub_mat = self.values.slice_mut(s![ 183 | y_range_start.into()..y_range_end.into(), 184 | x_range_start.into()..x_range_end.into() 185 | ]); 186 | Window { 187 | values: sub_mat, 188 | default_style: self.default_style, 189 | } 190 | } 191 | 192 | /// Split the window horizontally or vertically into two halves. 193 | /// 194 | /// If the split position is invalid (i.e., larger than the height/width of the window or 195 | /// negative), the original window is returned untouched as the error value. split_pos defines 196 | /// the first row of the second window. 197 | /// 198 | /// # Examples: 199 | /// ``` 200 | /// use unsegen::base::*; 201 | /// 202 | /// let mut wb = WindowBuffer::new(Width::new(5).unwrap(), Height::new(5).unwrap()); 203 | /// { 204 | /// let win = wb.as_window(); 205 | /// let (w1, w2) = win.split(RowIndex::new(3)).unwrap(); 206 | /// assert_eq!(w1.get_height(), Height::new(3).unwrap()); 207 | /// assert_eq!(w1.get_width(), Width::new(5).unwrap()); 208 | /// assert_eq!(w2.get_height(), Height::new(2).unwrap()); 209 | /// assert_eq!(w2.get_width(), Width::new(5).unwrap()); 210 | /// } 211 | /// { 212 | /// let win = wb.as_window(); 213 | /// let (w1, w2) = win.split(ColIndex::new(3)).unwrap(); 214 | /// assert_eq!(w1.get_height(), Height::new(5).unwrap()); 215 | /// assert_eq!(w1.get_width(), Width::new(3).unwrap()); 216 | /// assert_eq!(w2.get_height(), Height::new(5).unwrap()); 217 | /// assert_eq!(w2.get_width(), Width::new(2).unwrap()); 218 | /// } 219 | /// ``` 220 | pub fn split(self, split_pos: AxisIndex) -> Result<(Self, Self), Self> { 221 | if (self.get_extent() + PositiveAxisDiff::::new(1).unwrap()) 222 | .origin_range_contains(split_pos) 223 | { 224 | let (first_mat, second_mat) = self 225 | .values 226 | .split_at(Axis(D::NDARRAY_AXIS_NUMBER), split_pos.raw_value() as Ix); 227 | let w_u = Window { 228 | values: first_mat, 229 | default_style: self.default_style, 230 | }; 231 | let w_d = Window { 232 | values: second_mat, 233 | default_style: self.default_style, 234 | }; 235 | Ok((w_u, w_d)) 236 | } else { 237 | Err(self) 238 | } 239 | } 240 | 241 | /// Fill the window with the specified GraphemeCluster. 242 | /// 243 | /// The style is defined by the default style of the window. 244 | /// If the grapheme cluster is wider than 1 cell, any left over cells are filled with space 245 | /// characters. 246 | /// 247 | /// # Examples: 248 | /// ``` 249 | /// use unsegen::base::*; 250 | /// let mut wb = WindowBuffer::new(Width::new(5).unwrap(), Height::new(5).unwrap()); 251 | /// wb.as_window().fill(GraphemeCluster::try_from('X').unwrap()); 252 | /// // Every cell of wb now contains an 'X'. 253 | /// 254 | /// wb.as_window().fill(GraphemeCluster::try_from('山').unwrap()); 255 | /// // Every row of wb now contains two '山', while the last column cotains spaces. 256 | /// ``` 257 | pub fn fill(&mut self, c: GraphemeCluster) { 258 | let cluster_width = c.width(); 259 | let template = StyledGraphemeCluster::new(c, self.default_style); 260 | let empty = StyledGraphemeCluster::new(GraphemeCluster::empty(), self.default_style); 261 | let space = StyledGraphemeCluster::new(GraphemeCluster::space(), self.default_style); 262 | let w: i32 = self.get_width().into(); 263 | let right_border = (w - (w % cluster_width as i32)) as usize; 264 | for ((_, x), cell) in self.values.indexed_iter_mut() { 265 | if x >= right_border.into() { 266 | *cell = space.clone(); 267 | } else if x % cluster_width == 0 { 268 | *cell = template.clone(); 269 | } else { 270 | *cell = empty.clone(); 271 | } 272 | } 273 | } 274 | 275 | /// Fill the window with space characters. 276 | /// 277 | /// The style (i.e., the background color) is defined by the default style of the window. 278 | /// 279 | /// # Examples: 280 | /// ``` 281 | /// use unsegen::base::*; 282 | /// let mut wb = WindowBuffer::new(Width::new(5).unwrap(), Height::new(5).unwrap()); 283 | /// wb.as_window().clear(); 284 | /// // Every cell of wb now contains a ' '. 285 | /// ``` 286 | pub fn clear(&mut self) { 287 | self.fill(GraphemeCluster::space()); 288 | } 289 | 290 | /// Specify the new default style of the window. This style will be applied to all grapheme 291 | /// clusters written to the window. 292 | /// 293 | /// # Examples: 294 | /// ``` 295 | /// use unsegen::base::*; 296 | /// let mut wb = WindowBuffer::new(Width::new(5).unwrap(), Height::new(5).unwrap()); 297 | /// let mut win = wb.as_window(); 298 | /// win.set_default_style(StyleModifier::new().fg_color(Color::Red).bg_color(Color::Blue).apply_to_default()); 299 | /// win.clear(); 300 | /// // wb is now cleared and has a blue background. 301 | /// ``` 302 | pub fn set_default_style(&mut self, style: Style) { 303 | self.default_style = style; 304 | } 305 | 306 | /// Specify the new default style of the window. This style will be applied to all grapheme 307 | /// clusters written to the window. 308 | /// 309 | /// # Examples: 310 | /// ``` 311 | /// use unsegen::base::*; 312 | /// let mut wb = WindowBuffer::new(Width::new(5).unwrap(), Height::new(5).unwrap()); 313 | /// let mut win = wb.as_window(); 314 | /// win.set_default_style(StyleModifier::new().fg_color(Color::Red).bg_color(Color::Blue).apply_to_default()); 315 | /// win.clear(); 316 | /// // wb is now cleared an has a blue background. 317 | /// 318 | /// win.modify_default_style(StyleModifier::new().bg_color(Color::Yellow)); 319 | /// win.clear(); 320 | /// // wb is now cleared an has a yellow background. 321 | /// 322 | /// assert_eq!(*win.default_style(), 323 | /// StyleModifier::new().fg_color(Color::Red).bg_color(Color::Yellow).apply_to_default()) 324 | /// ``` 325 | pub fn modify_default_style(&mut self, modifier: StyleModifier) { 326 | modifier.modify(&mut self.default_style); 327 | } 328 | 329 | /// Get the current default style of the window. 330 | /// 331 | /// Change the default style using modify_default_style or set_default_style. 332 | pub fn default_style(&self) -> &Style { 333 | &self.default_style 334 | } 335 | } 336 | 337 | impl<'a> CursorTarget for Window<'a> { 338 | fn get_width(&self) -> Width { 339 | self.get_width() 340 | } 341 | fn get_height(&self) -> Height { 342 | self.get_height() 343 | } 344 | fn get_cell_mut(&mut self, x: ColIndex, y: RowIndex) -> Option<&mut StyledGraphemeCluster> { 345 | if x < 0 || y < 0 { 346 | None 347 | } else { 348 | let x: isize = x.into(); 349 | let y: isize = y.into(); 350 | self.values.get_mut((y as usize, x as usize)) 351 | } 352 | } 353 | fn get_cell(&self, x: ColIndex, y: RowIndex) -> Option<&StyledGraphemeCluster> { 354 | if x < 0 || y < 0 { 355 | None 356 | } else { 357 | let x: isize = x.into(); 358 | let y: isize = y.into(); 359 | self.values.get((y as usize, x as usize)) 360 | } 361 | } 362 | fn get_default_style(&self) -> Style { 363 | self.default_style 364 | } 365 | } 366 | 367 | /// A dummy window that does not safe any of the content written to it, but records the maximal 368 | /// coordinates used. 369 | /// 370 | /// This is therefore suitable for determining the required space demand of a 371 | /// widget if nothing or not enough is known about the content of a widget. 372 | pub struct ExtentEstimationWindow { 373 | some_value: StyledGraphemeCluster, 374 | default_style: Style, 375 | width: Width, 376 | extent_x: Width, 377 | extent_y: Height, 378 | } 379 | 380 | impl ExtentEstimationWindow { 381 | /// Create an ExtentEstimationWindow with a fixed width 382 | pub fn with_width(width: Width) -> Self { 383 | let style = Style::default(); 384 | ExtentEstimationWindow { 385 | some_value: StyledGraphemeCluster::new(GraphemeCluster::space().into(), style), 386 | default_style: style, 387 | width: width, 388 | extent_x: Width::new(0).unwrap(), 389 | extent_y: Height::new(0).unwrap(), 390 | } 391 | } 392 | 393 | /// Create an ExtentEstimationWindow with an unbounded width 394 | pub fn unbounded() -> Self { 395 | Self::with_width(Width::new(UNBOUNDED_WIDTH).unwrap()) 396 | } 397 | 398 | /// Get the width of the window required to display the contents written to the window. 399 | pub fn extent_x(&self) -> Width { 400 | self.extent_x 401 | } 402 | 403 | /// Get the height of the window required to display the contents written to the window. 404 | pub fn extent_y(&self) -> Height { 405 | self.extent_y 406 | } 407 | 408 | fn reset_value(&mut self) { 409 | self.some_value = 410 | StyledGraphemeCluster::new(GraphemeCluster::space().into(), self.default_style); 411 | } 412 | } 413 | 414 | impl CursorTarget for ExtentEstimationWindow { 415 | fn get_width(&self) -> Width { 416 | self.width 417 | } 418 | fn get_height(&self) -> Height { 419 | Height::new(UNBOUNDED_HEIGHT).unwrap() 420 | } 421 | fn get_cell_mut(&mut self, x: ColIndex, y: RowIndex) -> Option<&mut StyledGraphemeCluster> { 422 | self.extent_x = max(self.extent_x, (x.diff_to_origin() + 1).positive_or_zero()); 423 | self.extent_y = max(self.extent_y, (y.diff_to_origin() + 1).positive_or_zero()); 424 | self.reset_value(); 425 | if x < self.width.from_origin() { 426 | Some(&mut self.some_value) 427 | } else { 428 | None 429 | } 430 | } 431 | 432 | fn get_cell(&self, x: ColIndex, _: RowIndex) -> Option<&StyledGraphemeCluster> { 433 | if x < self.width.from_origin() { 434 | Some(&self.some_value) 435 | } else { 436 | None 437 | } 438 | } 439 | fn get_default_style(&self) -> Style { 440 | self.default_style 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/widget/builtin/promptline.rs: -------------------------------------------------------------------------------- 1 | //! A widget implementing "readline"-like functionality. 2 | use super::super::{HLayout, Widget}; 3 | use super::LineEdit; 4 | use input::{Editable, Navigatable, OperationResult, Scrollable, Writable}; 5 | use std::ops::{Deref, DerefMut}; 6 | 7 | /// A widget implementing "readline"-like functionality. 8 | /// 9 | /// Basically a more sophisticated version of `LineEdit` with history. 10 | pub struct PromptLine { 11 | edit_prompt: String, 12 | scroll_prompt: String, 13 | search_prompt: String, 14 | #[allow(missing_docs)] 15 | pub line: LineEdit, 16 | history: Vec, 17 | state: State, 18 | } 19 | 20 | enum State { 21 | Editing, 22 | Scrollback { 23 | active_line: String, 24 | pos: usize, 25 | }, //invariant: pos < history.len() 26 | Searching { 27 | search_pattern: String, 28 | pos: Option, // Found? -> some 29 | }, //invariant: pos < history.len() 30 | } 31 | 32 | fn search_prev( 33 | current: Option, 34 | history: &Vec, 35 | search_pattern: &str, 36 | ) -> Option { 37 | if search_pattern.is_empty() { 38 | return None; 39 | } 40 | let current = current.unwrap_or(history.len()); 41 | assert!(current <= history.len()); 42 | history[..current] 43 | .iter() 44 | .enumerate() 45 | .rev() 46 | .find(|(_, line)| line.contains(search_pattern)) 47 | .map(|(i, _)| i) 48 | } 49 | fn search_next( 50 | current: Option, 51 | history: &Vec, 52 | search_pattern: &str, 53 | ) -> Option { 54 | if search_pattern.is_empty() { 55 | return None; 56 | } 57 | let start = current.map(|c| c + 1).unwrap_or(0); 58 | history[start..] 59 | .iter() 60 | .position(|line| line.contains(search_pattern)) 61 | .map(|v| v + start) 62 | } 63 | 64 | impl PromptLine { 65 | /// Construct a PromptLine with the given symbol that will be displayed left of the `LineEdit` 66 | /// for user interaction. 67 | pub fn with_prompt(prompt: String) -> Self { 68 | PromptLine { 69 | edit_prompt: prompt.clone(), 70 | scroll_prompt: prompt.clone(), 71 | search_prompt: prompt.clone(), 72 | line: LineEdit::new(), 73 | history: Vec::new(), 74 | state: State::Editing, 75 | } 76 | } 77 | 78 | /// Change the symbol left of the user editable section (for editing, search and scrolling). 79 | pub fn set_prompt(&mut self, prompt: String) { 80 | self.edit_prompt = prompt.clone(); 81 | self.search_prompt = prompt.clone(); 82 | self.scroll_prompt = prompt; 83 | self.update_display(); 84 | } 85 | 86 | /// Change the symbol left of the user editable section (only for edit operations). 87 | pub fn set_edit_prompt(&mut self, prompt: String) { 88 | self.edit_prompt = prompt; 89 | self.update_display(); 90 | } 91 | 92 | /// Change the symbol left of the user editable section (only while searching). 93 | pub fn set_search_prompt(&mut self, prompt: String) { 94 | self.search_prompt = prompt; 95 | self.update_display(); 96 | } 97 | 98 | /// Change the symbol left of the user editable section (only while scrolling). 99 | pub fn set_scroll_prompt(&mut self, prompt: String) { 100 | self.scroll_prompt = prompt; 101 | self.update_display(); 102 | } 103 | 104 | /// Get the `n`'th line from the history. 105 | pub fn previous_line(&self, n: usize) -> Option<&str> { 106 | self.history 107 | .get(self.history.len().checked_sub(n).unwrap_or(0)) 108 | .map(String::as_str) 109 | } 110 | 111 | /// Get the current content of the `LineEdit` 112 | pub fn active_line(&self) -> &str { 113 | self.line.get() 114 | } 115 | 116 | /// Mark the current content as "accepted", e.g., if the user has entered and submitted a command. 117 | /// 118 | /// This adds the current line to the front of the history buffer. 119 | pub fn finish_line(&mut self) -> &str { 120 | if self.history.is_empty() 121 | || self.line.get() != self.history.last().expect("history is not empty").as_str() 122 | { 123 | self.history.push(self.line.get().to_owned()); 124 | } 125 | self.state = State::Editing; 126 | self.update_display(); 127 | let _ = self.line.clear(); 128 | &self.history[self.history.len() - 1] 129 | } 130 | 131 | /// Switch state to "searching". 132 | /// 133 | /// This means that that character inputs will instead be consumed for the search pattern and 134 | /// up/down naviation will cycle through matching items from the history. 135 | pub fn enter_search(&mut self) { 136 | let mut tmp = State::Editing; 137 | std::mem::swap(&mut tmp, &mut self.state); 138 | let (pos, search_pattern) = match tmp { 139 | State::Editing => (None, "".to_owned()), 140 | State::Searching { 141 | pos, 142 | search_pattern, 143 | } => (pos, search_pattern), 144 | State::Scrollback { pos, .. } => (Some(pos), "".to_owned()), 145 | }; 146 | let pos = search_prev(pos, &self.history, &search_pattern); 147 | self.state = State::Searching { 148 | pos, 149 | search_pattern, 150 | }; 151 | self.update_display(); 152 | } 153 | 154 | /// Set the line content according to the current scrollback position 155 | fn update_display(&mut self) { 156 | match &mut self.state { 157 | State::Editing => {} 158 | State::Scrollback { pos, .. } => { 159 | self.line.set(&self.history[*pos]); 160 | } 161 | State::Searching { pos, .. } => { 162 | if let Some(p) = pos { 163 | self.line.set(&self.history[*p]); 164 | } else { 165 | self.line.set(""); 166 | } 167 | } 168 | } 169 | } 170 | 171 | /// An edit operation changes the state from "we are looking through history" to "we are 172 | /// editing a complete new line". 173 | fn note_edit_operation(&mut self, res: OperationResult) -> OperationResult { 174 | if res.is_ok() { 175 | self.state = State::Editing; 176 | self.update_display(); 177 | } 178 | res 179 | } 180 | 181 | fn searching(&self) -> bool { 182 | if let State::Searching { .. } = self.state { 183 | true 184 | } else { 185 | false 186 | } 187 | } 188 | 189 | /// Prepare for drawing as a `Widget`. 190 | pub fn as_widget<'a>(&'a self) -> impl Widget + 'a { 191 | let prompt = match &self.state { 192 | State::Editing => self.edit_prompt.clone(), 193 | State::Scrollback { .. } => self.scroll_prompt.clone(), 194 | State::Searching { search_pattern, .. } => { 195 | format!("{}\"{}\": ", self.search_prompt, search_pattern) 196 | } 197 | }; 198 | HLayout::new().widget(prompt).widget(self.line.as_widget()) 199 | } 200 | } 201 | 202 | impl Scrollable for PromptLine { 203 | fn scroll_forwards(&mut self) -> OperationResult { 204 | let result; 205 | let mut tmp = State::Editing; 206 | std::mem::swap(&mut tmp, &mut self.state); 207 | self.state = match tmp { 208 | State::Editing => { 209 | result = Err(()); 210 | State::Editing 211 | } 212 | State::Scrollback { 213 | active_line, 214 | mut pos, 215 | } => { 216 | result = Ok(()); 217 | if pos + 1 < self.history.len() { 218 | pos += 1; 219 | State::Scrollback { pos, active_line } 220 | } else { 221 | self.line.set(&active_line); 222 | State::Editing 223 | } 224 | } 225 | State::Searching { 226 | search_pattern, 227 | pos, 228 | } => { 229 | let pos = search_next(pos, &self.history, &search_pattern); 230 | result = pos.map(|_| ()).ok_or(()); 231 | 232 | State::Searching { 233 | search_pattern, 234 | pos, 235 | } 236 | } 237 | }; 238 | self.update_display(); 239 | result 240 | } 241 | fn scroll_backwards(&mut self) -> OperationResult { 242 | let result; 243 | let mut tmp = State::Editing; 244 | std::mem::swap(&mut tmp, &mut self.state); 245 | self.state = match tmp { 246 | State::Editing => { 247 | if self.history.len() > 0 { 248 | result = Ok(()); 249 | State::Scrollback { 250 | active_line: self.line.get().to_owned(), 251 | pos: self.history.len() - 1, 252 | } 253 | } else { 254 | result = Err(()); 255 | State::Editing 256 | } 257 | } 258 | State::Scrollback { 259 | active_line, 260 | mut pos, 261 | } => { 262 | if pos > 0 { 263 | pos -= 1; 264 | result = Ok(()); 265 | } else { 266 | result = Err(()); 267 | } 268 | State::Scrollback { active_line, pos } 269 | } 270 | State::Searching { 271 | search_pattern, 272 | pos, 273 | } => { 274 | let pos = search_prev(pos, &self.history, &search_pattern); 275 | result = pos.map(|_| ()).ok_or(()); 276 | 277 | State::Searching { 278 | search_pattern, 279 | pos, 280 | } 281 | } 282 | }; 283 | self.update_display(); 284 | result 285 | } 286 | fn scroll_to_beginning(&mut self) -> OperationResult { 287 | let result; 288 | let mut tmp = State::Editing; 289 | std::mem::swap(&mut tmp, &mut self.state); 290 | self.state = match tmp { 291 | State::Editing | State::Scrollback { .. } => { 292 | if self.history.len() > 0 { 293 | result = Ok(()); 294 | State::Scrollback { 295 | active_line: self.line.get().to_owned(), 296 | pos: 0, 297 | } 298 | } else { 299 | result = Err(()); 300 | State::Editing 301 | } 302 | } 303 | State::Searching { search_pattern, .. } => { 304 | let pos = search_next(None, &self.history, &search_pattern); 305 | result = pos.map(|_| ()).ok_or(()); 306 | 307 | State::Searching { 308 | search_pattern, 309 | pos, 310 | } 311 | } 312 | }; 313 | self.update_display(); 314 | result 315 | } 316 | fn scroll_to_end(&mut self) -> OperationResult { 317 | let result; 318 | let mut tmp = State::Editing; 319 | std::mem::swap(&mut tmp, &mut self.state); 320 | self.state = match tmp { 321 | State::Editing => { 322 | result = Err(()); 323 | State::Editing 324 | } 325 | State::Scrollback { active_line, .. } => { 326 | result = Ok(()); 327 | self.line.set(&active_line); 328 | State::Editing 329 | } 330 | State::Searching { search_pattern, .. } => { 331 | let pos = search_prev(None, &self.history, &search_pattern); 332 | result = pos.map(|_| ()).ok_or(()); 333 | 334 | State::Searching { 335 | search_pattern, 336 | pos, 337 | } 338 | } 339 | }; 340 | self.update_display(); 341 | result 342 | } 343 | } 344 | impl Navigatable for PromptLine { 345 | fn move_up(&mut self) -> OperationResult { 346 | self.scroll_backwards() 347 | } 348 | fn move_down(&mut self) -> OperationResult { 349 | self.scroll_forwards() 350 | } 351 | fn move_left(&mut self) -> OperationResult { 352 | if self.searching() { 353 | self.state = State::Editing; 354 | self.update_display(); 355 | Ok(()) 356 | } else { 357 | self.line.move_left() 358 | } 359 | } 360 | fn move_right(&mut self) -> OperationResult { 361 | if self.searching() { 362 | self.state = State::Editing; 363 | self.update_display(); 364 | Ok(()) 365 | } else { 366 | self.line.move_right() 367 | } 368 | } 369 | } 370 | 371 | impl Writable for PromptLine { 372 | fn write(&mut self, c: char) -> OperationResult { 373 | let res = match &mut self.state { 374 | State::Editing | State::Scrollback { .. } => { 375 | let res = self.line.write(c); 376 | self.note_edit_operation(res) 377 | } 378 | State::Searching { 379 | search_pattern, 380 | pos, 381 | .. 382 | } => match c { 383 | '\n' => Err(()), 384 | o => { 385 | search_pattern.push(o); 386 | *pos = search_prev(pos.map(|p| p + 1), &self.history, &search_pattern); 387 | pos.map(|_| ()).ok_or(()) 388 | } 389 | }, 390 | }; 391 | self.update_display(); 392 | res 393 | } 394 | } 395 | 396 | impl Editable for PromptLine { 397 | fn delete_forwards(&mut self) -> OperationResult { 398 | let res = self.line.delete_forwards(); 399 | self.note_edit_operation(res) 400 | } 401 | fn delete_backwards(&mut self) -> OperationResult { 402 | let res = match &mut self.state { 403 | State::Editing | State::Scrollback { .. } => { 404 | let res = self.line.delete_backwards(); 405 | self.note_edit_operation(res) 406 | } 407 | State::Searching { 408 | search_pattern, 409 | pos, 410 | } => { 411 | if search_pattern.pop().is_some() { 412 | *pos = search_prev(pos.map(|p| p + 1), &self.history, &search_pattern); 413 | } else { 414 | self.state = State::Editing; 415 | } 416 | Ok(()) 417 | } 418 | }; 419 | self.update_display(); 420 | res 421 | } 422 | fn go_to_beginning_of_line(&mut self) -> OperationResult { 423 | let res = self.line.go_to_beginning_of_line(); 424 | self.note_edit_operation(res) 425 | } 426 | fn go_to_end_of_line(&mut self) -> OperationResult { 427 | let res = self.line.go_to_end_of_line(); 428 | self.note_edit_operation(res) 429 | } 430 | fn clear(&mut self) -> OperationResult { 431 | let res = match &mut self.state { 432 | State::Editing | State::Scrollback { .. } => { 433 | let res = self.line.clear(); 434 | self.note_edit_operation(res) 435 | } 436 | State::Searching { .. } => { 437 | self.state = State::Editing; 438 | Ok(()) 439 | } 440 | }; 441 | self.update_display(); 442 | res 443 | } 444 | } 445 | 446 | impl Deref for PromptLine { 447 | type Target = LineEdit; 448 | 449 | fn deref(&self) -> &Self::Target { 450 | &self.line 451 | } 452 | } 453 | 454 | impl DerefMut for PromptLine { 455 | fn deref_mut(&mut self) -> &mut Self::Target { 456 | &mut self.line 457 | } 458 | } 459 | 460 | #[cfg(test)] 461 | mod test { 462 | use super::*; 463 | 464 | #[test] 465 | fn test_search_prev() { 466 | let history = vec![ 467 | "".to_string(), 468 | "abc".to_string(), 469 | "foo".to_string(), 470 | "".to_string(), 471 | "foo".to_string(), 472 | ]; 473 | 474 | assert_eq!(search_prev(Some(0), &history, ""), None); 475 | assert_eq!(search_prev(Some(1), &history, "a"), None); 476 | assert_eq!(search_prev(Some(5), &history, "foo"), Some(4)); 477 | assert_eq!(search_prev(Some(4), &history, "foo"), Some(2)); 478 | assert_eq!(search_prev(Some(2), &history, "foo"), None); 479 | assert_eq!(search_prev(Some(5), &history, "foo2"), None); 480 | assert_eq!(search_prev(None, &history, ""), None); 481 | } 482 | 483 | #[test] 484 | fn test_search_next() { 485 | let history = vec![ 486 | "".to_string(), 487 | "abc".to_string(), 488 | "foo".to_string(), 489 | "".to_string(), 490 | "foo".to_string(), 491 | ]; 492 | 493 | assert_eq!(search_next(Some(4), &history, ""), None); 494 | assert_eq!(search_next(Some(0), &history, "a"), Some(1)); 495 | assert_eq!(search_next(Some(0), &history, "foo"), Some(2)); 496 | assert_eq!(search_next(Some(2), &history, "foo"), Some(4)); 497 | assert_eq!(search_next(Some(4), &history, "foo"), None); 498 | assert_eq!(search_next(Some(0), &history, "foo2"), None); 499 | assert_eq!(search_next(None, &history, ""), None); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/base/style.rs: -------------------------------------------------------------------------------- 1 | //! Types related to the visual representation (i.e., style) of text when drawn to the terminal. 2 | //! This includes formatting (bold, italic, ...) and colors. 3 | use std::io::Write; 4 | use termion; 5 | 6 | /// Specifies how text is written to the terminal. 7 | /// Specified attributes include "bold", "italic", "invert", and "underline" and can be combined 8 | /// freely. 9 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 10 | #[allow(missing_docs)] 11 | pub struct TextFormat { 12 | pub bold: bool, 13 | pub italic: bool, 14 | pub invert: bool, 15 | pub underline: bool, 16 | 17 | // Make users of the library unable to construct Textformat from members. 18 | // This way we can add members in a backwards compatible way in future versions. 19 | #[doc(hidden)] 20 | _do_not_construct: (), 21 | } 22 | 23 | impl TextFormat { 24 | /// Set the attributes of the given ANSI terminal to match the current TextFormat. 25 | fn set_terminal_attributes(self, terminal: &mut W) { 26 | if self.bold { 27 | write!(terminal, "{}", termion::style::Bold).expect("set bold style"); 28 | } 29 | 30 | if self.italic { 31 | write!(terminal, "{}", termion::style::Italic).expect("set italic style"); 32 | } 33 | 34 | if self.invert { 35 | write!(terminal, "{}", termion::style::Invert).expect("set invert style"); 36 | } 37 | 38 | if self.underline { 39 | write!(terminal, "{}", termion::style::Underline).expect("set underline style"); 40 | } 41 | } 42 | } 43 | 44 | impl Default for TextFormat { 45 | fn default() -> Self { 46 | TextFormat { 47 | bold: false, 48 | italic: false, 49 | invert: false, 50 | underline: false, 51 | _do_not_construct: (), 52 | } 53 | } 54 | } 55 | 56 | /// Specifies how to modify a bool value. 57 | /// 58 | /// (In essence, specifies one of all possible unary boolean functions.) 59 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 60 | #[allow(missing_docs)] 61 | pub enum BoolModifyMode { 62 | True, 63 | False, 64 | Toggle, 65 | LeaveUnchanged, 66 | } 67 | 68 | impl BoolModifyMode { 69 | /// Combine the current value with that of the argument so that the application of the returned 70 | /// value is always equivalent to first applying other and then applying self to a bool. 71 | /// 72 | /// # Examples: 73 | /// ``` 74 | /// use unsegen::base::BoolModifyMode; 75 | /// 76 | /// assert_eq!(BoolModifyMode::LeaveUnchanged.on_top_of(BoolModifyMode::False), 77 | /// BoolModifyMode::False); 78 | /// assert_eq!(BoolModifyMode::True.on_top_of(BoolModifyMode::Toggle /*or any other value*/), 79 | /// BoolModifyMode::True); 80 | /// assert_eq!(BoolModifyMode::Toggle.on_top_of(BoolModifyMode::Toggle), 81 | /// BoolModifyMode::LeaveUnchanged); 82 | /// ``` 83 | /// 84 | pub fn on_top_of(self, other: Self) -> Self { 85 | match (self, other) { 86 | (BoolModifyMode::True, _) => BoolModifyMode::True, 87 | (BoolModifyMode::False, _) => BoolModifyMode::False, 88 | (BoolModifyMode::Toggle, BoolModifyMode::True) => BoolModifyMode::False, 89 | (BoolModifyMode::Toggle, BoolModifyMode::False) => BoolModifyMode::True, 90 | (BoolModifyMode::Toggle, BoolModifyMode::Toggle) => BoolModifyMode::LeaveUnchanged, 91 | (BoolModifyMode::Toggle, BoolModifyMode::LeaveUnchanged) => BoolModifyMode::Toggle, 92 | (BoolModifyMode::LeaveUnchanged, m) => m, 93 | } 94 | } 95 | 96 | /// Modify the target bool according to the modification mode. 97 | /// 98 | /// # Examples: 99 | /// ``` 100 | /// use unsegen::base::BoolModifyMode; 101 | /// 102 | /// let mut b = true; 103 | /// BoolModifyMode::False.modify(&mut b); 104 | /// assert_eq!(b, false); 105 | /// 106 | /// let mut b = false; 107 | /// BoolModifyMode::LeaveUnchanged.modify(&mut b); 108 | /// assert_eq!(b, false); 109 | /// 110 | /// let mut b = false; 111 | /// BoolModifyMode::Toggle.modify(&mut b); 112 | /// assert_eq!(b, true); 113 | /// ``` 114 | /// 115 | pub fn modify(self, target: &mut bool) { 116 | match self { 117 | BoolModifyMode::True => *target = true, 118 | BoolModifyMode::False => *target = false, 119 | BoolModifyMode::Toggle => *target ^= true, 120 | BoolModifyMode::LeaveUnchanged => {} 121 | } 122 | } 123 | } 124 | 125 | impl ::std::convert::From for BoolModifyMode { 126 | fn from(on: bool) -> Self { 127 | if on { 128 | BoolModifyMode::True 129 | } else { 130 | BoolModifyMode::False 131 | } 132 | } 133 | } 134 | 135 | /// Specifies how to modify a text format value. 136 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 137 | #[allow(missing_docs)] 138 | pub struct TextFormatModifier { 139 | pub bold: BoolModifyMode, 140 | pub italic: BoolModifyMode, 141 | pub invert: BoolModifyMode, 142 | pub underline: BoolModifyMode, 143 | 144 | // Make users of the library unable to construct TextFormatModifier from members. 145 | // This way we can add members in a backwards compatible way in future versions. 146 | #[doc(hidden)] 147 | _do_not_construct: (), 148 | } 149 | 150 | impl TextFormatModifier { 151 | /// Construct a new (not actually) modifier, that leaves all properties unchanged. 152 | pub fn new() -> Self { 153 | TextFormatModifier { 154 | bold: BoolModifyMode::LeaveUnchanged, 155 | italic: BoolModifyMode::LeaveUnchanged, 156 | invert: BoolModifyMode::LeaveUnchanged, 157 | underline: BoolModifyMode::LeaveUnchanged, 158 | _do_not_construct: (), 159 | } 160 | } 161 | /// Set the bold property of the TextFormatModifier 162 | /// 163 | /// # Examples: 164 | /// ``` 165 | /// use unsegen::base::{TextFormatModifier, BoolModifyMode}; 166 | /// 167 | /// assert_eq!(TextFormatModifier::new().bold(BoolModifyMode::Toggle).bold, 168 | /// BoolModifyMode::Toggle); 169 | /// ``` 170 | pub fn bold>(mut self, val: M) -> Self { 171 | self.bold = val.into(); 172 | self 173 | } 174 | 175 | /// Set the italic property of the TextFormatModifier 176 | /// 177 | /// # Examples: 178 | /// ``` 179 | /// use unsegen::base::{TextFormatModifier, BoolModifyMode}; 180 | /// 181 | /// assert_eq!(TextFormatModifier::new().italic(BoolModifyMode::True).italic, 182 | /// BoolModifyMode::True); 183 | /// ``` 184 | pub fn italic>(mut self, val: M) -> Self { 185 | self.italic = val.into(); 186 | self 187 | } 188 | 189 | /// Set the invert property of the TextFormatModifier 190 | /// 191 | /// # Examples: 192 | /// ``` 193 | /// use unsegen::base::{TextFormatModifier, BoolModifyMode}; 194 | /// 195 | /// assert_eq!(TextFormatModifier::new().invert(BoolModifyMode::False).invert, 196 | /// BoolModifyMode::False); 197 | /// ``` 198 | pub fn invert>(mut self, val: M) -> Self { 199 | self.invert = val.into(); 200 | self 201 | } 202 | 203 | /// Set underline invert property of the TextFormatModifier 204 | /// 205 | /// # Examples: 206 | /// ``` 207 | /// use unsegen::base::{TextFormatModifier, BoolModifyMode}; 208 | /// 209 | /// assert_eq!(TextFormatModifier::new().underline(BoolModifyMode::LeaveUnchanged).underline, 210 | /// BoolModifyMode::LeaveUnchanged); 211 | /// ``` 212 | pub fn underline>(mut self, val: M) -> Self { 213 | self.underline = val.into(); 214 | self 215 | } 216 | 217 | /// Combine the current value with that of the argument so that the application of the returned 218 | /// value is always equivalent to first applying other and then applying self to some TextFormat. 219 | /// 220 | /// # Examples: 221 | /// ``` 222 | /// use unsegen::base::{TextFormatModifier, TextFormat, BoolModifyMode}; 223 | /// 224 | /// let mut f1 = TextFormat::default(); 225 | /// let mut f2 = f1; 226 | /// 227 | /// let m1 = 228 | /// TextFormatModifier::new().italic(BoolModifyMode::Toggle).bold(true).underline(false); 229 | /// let m2 = TextFormatModifier::new().italic(true).bold(false); 230 | /// 231 | /// m1.on_top_of(m2).modify(&mut f1); 232 | /// 233 | /// m2.modify(&mut f2); 234 | /// m1.modify(&mut f2); 235 | /// 236 | /// assert_eq!(f1, f2); 237 | /// ``` 238 | /// 239 | pub fn on_top_of(self, other: TextFormatModifier) -> Self { 240 | TextFormatModifier { 241 | bold: self.bold.on_top_of(other.bold), 242 | italic: self.italic.on_top_of(other.italic), 243 | invert: self.invert.on_top_of(other.invert), 244 | underline: self.underline.on_top_of(other.underline), 245 | _do_not_construct: (), 246 | } 247 | } 248 | 249 | /// Modify the passed textformat according to the modification rules of self. 250 | pub fn modify(self, format: &mut TextFormat) { 251 | self.bold.modify(&mut format.bold); 252 | self.italic.modify(&mut format.italic); 253 | self.invert.modify(&mut format.invert); 254 | self.underline.modify(&mut format.underline); 255 | } 256 | } 257 | 258 | impl Default for TextFormatModifier { 259 | fn default() -> Self { 260 | TextFormatModifier::new() 261 | } 262 | } 263 | 264 | /// A color that can be displayed in terminal. 265 | /// 266 | /// Colors are either: 267 | /// - Default (i.e., terminal color is reset) 268 | /// - Named (Black, Yellow, LightRed, ...) 269 | /// - Ansi (8 bit) 270 | /// - or Rgb. 271 | /// 272 | /// Not all terminals may support Rgb, though. 273 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 274 | #[allow(missing_docs)] 275 | pub enum Color { 276 | Default, 277 | Rgb { r: u8, g: u8, b: u8 }, 278 | Ansi(u8), 279 | Black, 280 | Blue, 281 | Cyan, 282 | Green, 283 | Magenta, 284 | Red, 285 | White, 286 | Yellow, 287 | LightBlack, 288 | LightBlue, 289 | LightCyan, 290 | LightGreen, 291 | LightMagenta, 292 | LightRed, 293 | LightWhite, 294 | LightYellow, 295 | } 296 | 297 | impl Default for Color { 298 | fn default() -> Self { 299 | Color::Default 300 | } 301 | } 302 | 303 | impl Color { 304 | /// Construct an ansi color value from rgb values. 305 | /// r, g and b must all be < 6. 306 | pub fn ansi_rgb(r: u8, g: u8, b: u8) -> Self { 307 | assert!(r < 6, "Invalid red value"); 308 | assert!(g < 6, "Invalid green value"); 309 | assert!(b < 6, "Invalid blue value"); 310 | Color::Ansi(termion::color::AnsiValue::rgb(r, g, b).0) 311 | } 312 | 313 | /// Construct a gray value ansi color. 314 | /// v must be < 24. 315 | pub fn ansi_grayscale(v: u8 /* < 24 */) -> Self { 316 | assert!(v < 24, "Invalid gray value"); 317 | Color::Ansi(termion::color::AnsiValue::grayscale(v).0) 318 | } 319 | 320 | /// Set the forground color of the terminal. 321 | fn set_terminal_attributes_fg(self, terminal: &mut W) -> ::std::io::Result<()> { 322 | use termion::color::Fg as Target; 323 | match self { 324 | Color::Default => Ok(()), 325 | Color::Rgb { r, g, b } => write!(terminal, "{}", Target(termion::color::Rgb(r, g, b))), 326 | Color::Ansi(v) => write!(terminal, "{}", Target(termion::color::AnsiValue(v))), 327 | Color::Black => write!(terminal, "{}", Target(termion::color::Black)), 328 | Color::Blue => write!(terminal, "{}", Target(termion::color::Blue)), 329 | Color::Cyan => write!(terminal, "{}", Target(termion::color::Cyan)), 330 | Color::Magenta => write!(terminal, "{}", Target(termion::color::Magenta)), 331 | Color::Green => write!(terminal, "{}", Target(termion::color::Green)), 332 | Color::Red => write!(terminal, "{}", Target(termion::color::Red)), 333 | Color::White => write!(terminal, "{}", Target(termion::color::White)), 334 | Color::Yellow => write!(terminal, "{}", Target(termion::color::Yellow)), 335 | Color::LightBlack => write!(terminal, "{}", Target(termion::color::LightBlack)), 336 | Color::LightBlue => write!(terminal, "{}", Target(termion::color::LightBlue)), 337 | Color::LightCyan => write!(terminal, "{}", Target(termion::color::LightCyan)), 338 | Color::LightMagenta => write!(terminal, "{}", Target(termion::color::LightMagenta)), 339 | Color::LightGreen => write!(terminal, "{}", Target(termion::color::LightGreen)), 340 | Color::LightRed => write!(terminal, "{}", Target(termion::color::LightRed)), 341 | Color::LightWhite => write!(terminal, "{}", Target(termion::color::LightWhite)), 342 | Color::LightYellow => write!(terminal, "{}", Target(termion::color::LightYellow)), 343 | } 344 | } 345 | 346 | /// Set the background color of the terminal. 347 | fn set_terminal_attributes_bg(self, terminal: &mut W) -> ::std::io::Result<()> { 348 | use termion::color::Bg as Target; 349 | match self { 350 | Color::Default => Ok(()), 351 | Color::Rgb { r, g, b } => write!(terminal, "{}", Target(termion::color::Rgb(r, g, b))), 352 | Color::Ansi(v) => write!(terminal, "{}", Target(termion::color::AnsiValue(v))), 353 | Color::Black => write!(terminal, "{}", Target(termion::color::Black)), 354 | Color::Blue => write!(terminal, "{}", Target(termion::color::Blue)), 355 | Color::Cyan => write!(terminal, "{}", Target(termion::color::Cyan)), 356 | Color::Magenta => write!(terminal, "{}", Target(termion::color::Magenta)), 357 | Color::Green => write!(terminal, "{}", Target(termion::color::Green)), 358 | Color::Red => write!(terminal, "{}", Target(termion::color::Red)), 359 | Color::White => write!(terminal, "{}", Target(termion::color::White)), 360 | Color::Yellow => write!(terminal, "{}", Target(termion::color::Yellow)), 361 | Color::LightBlack => write!(terminal, "{}", Target(termion::color::LightBlack)), 362 | Color::LightBlue => write!(terminal, "{}", Target(termion::color::LightBlue)), 363 | Color::LightCyan => write!(terminal, "{}", Target(termion::color::LightCyan)), 364 | Color::LightMagenta => write!(terminal, "{}", Target(termion::color::LightMagenta)), 365 | Color::LightGreen => write!(terminal, "{}", Target(termion::color::LightGreen)), 366 | Color::LightRed => write!(terminal, "{}", Target(termion::color::LightRed)), 367 | Color::LightWhite => write!(terminal, "{}", Target(termion::color::LightWhite)), 368 | Color::LightYellow => write!(terminal, "{}", Target(termion::color::LightYellow)), 369 | } 370 | } 371 | } 372 | 373 | /// A style that defines how text is presented on the terminal. 374 | /// 375 | /// Use StyleModifier to modify the style from the default/plain state. 376 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] 377 | pub struct Style { 378 | fg_color: Color, 379 | bg_color: Color, 380 | format: TextFormat, 381 | } 382 | 383 | impl Style { 384 | /// Create a "standard" style, i.e., no fancy colors or text attributes. 385 | pub fn plain() -> Self { 386 | Self::default() 387 | } 388 | 389 | /// Access the `TextFormat` of the style 390 | pub fn format(&self) -> TextFormat { 391 | self.format 392 | } 393 | 394 | /// Set the attributes of the given ANSI terminal to match the current Style. 395 | pub(crate) fn set_terminal_attributes(self, terminal: &mut W) { 396 | // Since we cannot rely on NoBold reseting the bold style (see 397 | // https://en.wikipedia.org/wiki/Talk:ANSI_escape_code#SGR_21%E2%80%94%60Bold_off%60_not_widely_supported) 398 | // we first reset _all_ styles, then reapply anything that differs from the default. 399 | write!( 400 | terminal, 401 | "{}{}{}", 402 | termion::style::Reset, 403 | termion::color::Fg(termion::color::Reset), 404 | termion::color::Bg(termion::color::Reset) 405 | ) 406 | .expect("reset style"); 407 | 408 | self.fg_color 409 | .set_terminal_attributes_fg(terminal) 410 | .expect("write fg_color"); 411 | self.bg_color 412 | .set_terminal_attributes_bg(terminal) 413 | .expect("write bg_color"); 414 | self.format.set_terminal_attributes(terminal); 415 | } 416 | } 417 | 418 | /// Defines a set of modifications on a style. Multiple modifiers can be combined before applying 419 | /// them to a style. 420 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] 421 | pub struct StyleModifier { 422 | fg_color: Option, 423 | bg_color: Option, 424 | format: TextFormatModifier, 425 | } 426 | 427 | impl StyleModifier { 428 | /// Construct a new (not actually) modifier, that leaves all style properties unchanged. 429 | pub fn new() -> Self { 430 | Self::default() 431 | } 432 | 433 | /// Make the modifier change the foreground color to the specified value. 434 | pub fn fg_color(mut self, fg_color: Color) -> Self { 435 | self.fg_color = Some(fg_color); 436 | self 437 | } 438 | 439 | /// Make the modifier change the background color to the specified value. 440 | pub fn bg_color(mut self, bg_color: Color) -> Self { 441 | self.bg_color = Some(bg_color); 442 | self 443 | } 444 | 445 | /// Make the modifier change the textformat of the style to the specified value. 446 | pub fn format(mut self, format: TextFormatModifier) -> Self { 447 | self.format = format; 448 | self 449 | } 450 | 451 | /// Make the modifier change the bold property of the textformat of the style to the specified value. 452 | /// 453 | /// This is a shortcut for using `format` using a TextFormatModifier that changes the bold 454 | /// property. 455 | /// 456 | /// # Examples: 457 | /// ``` 458 | /// use unsegen::base::{StyleModifier, TextFormatModifier}; 459 | /// 460 | /// let s1 = StyleModifier::new().bold(true); 461 | /// let s2 = StyleModifier::new().format(TextFormatModifier::new().bold(true)); 462 | /// 463 | /// assert_eq!(s1, s2); 464 | /// ``` 465 | pub fn bold>(mut self, val: M) -> Self { 466 | self.format.bold = val.into(); 467 | self 468 | } 469 | 470 | /// Make the modifier change the italic property of the textformat of the style to the specified value. 471 | /// 472 | /// This is a shortcut for using `format` using a TextFormatModifier that changes the italic 473 | /// property. 474 | /// 475 | /// # Examples: 476 | /// ``` 477 | /// use unsegen::base::{StyleModifier, TextFormatModifier}; 478 | /// 479 | /// let s1 = StyleModifier::new().italic(true); 480 | /// let s2 = StyleModifier::new().format(TextFormatModifier::new().italic(true)); 481 | /// 482 | /// assert_eq!(s1, s2); 483 | /// ``` 484 | pub fn italic>(mut self, val: M) -> Self { 485 | self.format.italic = val.into(); 486 | self 487 | } 488 | 489 | /// Make the modifier change the invert property of the textformat of the style to the specified value. 490 | /// 491 | /// This is a shortcut for using `format` using a TextFormatModifier that changes the invert 492 | /// property. 493 | /// 494 | /// # Examples: 495 | /// ``` 496 | /// use unsegen::base::{StyleModifier, TextFormatModifier}; 497 | /// 498 | /// let s1 = StyleModifier::new().invert(true); 499 | /// let s2 = StyleModifier::new().format(TextFormatModifier::new().invert(true)); 500 | /// 501 | /// assert_eq!(s1, s2); 502 | /// ``` 503 | pub fn invert>(mut self, val: M) -> Self { 504 | self.format.invert = val.into(); 505 | self 506 | } 507 | 508 | /// Make the modifier change the underline property of the textformat of the style to the specified value. 509 | /// 510 | /// This is a shortcut for using `format` using a TextFormatModifier that changes the underline 511 | /// property. 512 | /// 513 | /// # Examples: 514 | /// ``` 515 | /// use unsegen::base::{StyleModifier, TextFormatModifier}; 516 | /// 517 | /// let s1 = StyleModifier::new().underline(true); 518 | /// let s2 = StyleModifier::new().format(TextFormatModifier::new().underline(true)); 519 | /// 520 | /// assert_eq!(s1, s2); 521 | /// ``` 522 | pub fn underline>(mut self, val: M) -> Self { 523 | self.format.underline = val.into(); 524 | self 525 | } 526 | 527 | /// Combine the current value with that of the argument so that the application of the returned 528 | /// value is always equivalent to first applying other and then applying self to some Style. 529 | /// 530 | /// # Examples: 531 | /// ``` 532 | /// use unsegen::base::*; 533 | /// 534 | /// let mut s1 = Style::default(); 535 | /// let mut s2 = s1; 536 | /// 537 | /// let m1 = 538 | /// StyleModifier::new().fg_color(Color::Red).italic(BoolModifyMode::Toggle).bold(true).underline(false); 539 | /// let m2 = StyleModifier::new().bg_color(Color::Blue).italic(true).bold(false); 540 | /// 541 | /// m1.on_top_of(m2).modify(&mut s1); 542 | /// 543 | /// m2.modify(&mut s2); 544 | /// m1.modify(&mut s2); 545 | /// 546 | /// assert_eq!(s1, s2); 547 | /// ``` 548 | /// 549 | pub fn on_top_of(self, other: StyleModifier) -> Self { 550 | StyleModifier { 551 | fg_color: self.fg_color.or(other.fg_color), 552 | bg_color: self.bg_color.or(other.bg_color), 553 | format: self.format.on_top_of(other.format), 554 | } 555 | } 556 | 557 | /// Apply the modifier to a default (i.e., empty) Style. In a way, this converts the 558 | /// StyleModifier to a Style. 559 | /// 560 | /// # Examples: 561 | /// ``` 562 | /// use unsegen::base::*; 563 | /// 564 | /// let m = StyleModifier::new().fg_color(Color::Red).italic(true); 565 | /// let mut style = Style::default(); 566 | /// 567 | /// assert_eq!(m.apply(style), m.apply_to_default()); 568 | /// ``` 569 | pub fn apply_to_default(self) -> Style { 570 | let mut style = Style::default(); 571 | self.modify(&mut style); 572 | style 573 | } 574 | 575 | /// Apply this modifier to a given style and return the result. This is essentially a 576 | /// convenience wrapper around modify, which clones the Style. 577 | /// 578 | /// # Examples: 579 | /// ``` 580 | /// use unsegen::base::*; 581 | /// 582 | /// let m = StyleModifier::new().fg_color(Color::Red).italic(BoolModifyMode::Toggle); 583 | /// let mut style = Style::default(); 584 | /// let style2 = m.apply(style); 585 | /// m.modify(&mut style); 586 | /// 587 | /// assert_eq!(style, style2); 588 | /// ``` 589 | pub fn apply(self, mut style: Style) -> Style { 590 | self.modify(&mut style); 591 | style 592 | } 593 | 594 | /// Modify the given style according to the properties of this modifier. 595 | pub fn modify(self, style: &mut Style) { 596 | if let Some(fg) = self.fg_color { 597 | style.fg_color = fg; 598 | } 599 | if let Some(bg) = self.bg_color { 600 | style.bg_color = bg; 601 | } 602 | self.format.modify(&mut style.format); 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /src/base/basic_types.rs: -------------------------------------------------------------------------------- 1 | //! Basic numeric semantic wrapper types for use in other parts of the library. 2 | use std::cmp::Ordering; 3 | use std::fmt; 4 | use std::iter::Sum; 5 | use std::marker::PhantomData; 6 | use std::ops::{Add, AddAssign, Div, Mul, Neg, Range, Rem, Sub, SubAssign}; 7 | 8 | /// AxisIndex (the base for ColIndex or RowIndex) is a signed integer coordinate (i.e., a 9 | /// coordinate of a point on the terminal cell grid) 10 | #[derive(Copy, Clone, Debug, Ord, Eq)] 11 | pub struct AxisIndex { 12 | val: i32, 13 | _dim: PhantomData, 14 | } 15 | 16 | impl AxisIndex { 17 | /// Create a new AxisIndex from an i32. Any i32 value is valid. 18 | pub fn new(v: i32) -> Self { 19 | AxisIndex { 20 | val: v, 21 | _dim: Default::default(), 22 | } 23 | } 24 | 25 | /// Unpack the AxisDiff to receive the raw i32 value. 26 | pub fn raw_value(self) -> i32 { 27 | self.into() 28 | } 29 | 30 | /// Calculate the origin of the Index to the origin of the coordinate grid (i.e., 0). 31 | /// Technically this just converts an AxisIndex into an AxisDiff, but is semantically more 32 | /// explicit. 33 | /// 34 | /// # Examples: 35 | /// 36 | /// ``` 37 | /// use unsegen::base::{ColIndex, ColDiff}; 38 | /// assert_eq!(ColIndex::new(27).diff_to_origin(), ColDiff::new(27)); 39 | /// ``` 40 | pub fn diff_to_origin(self) -> AxisDiff { 41 | AxisDiff::new(self.val) 42 | } 43 | 44 | /// Clamp the value into a positive or zero range 45 | /// 46 | /// # Examples: 47 | /// 48 | /// ``` 49 | /// use unsegen::base::ColIndex; 50 | /// assert_eq!(ColIndex::new(27).positive_or_zero(), ColIndex::new(27)); 51 | /// assert_eq!(ColIndex::new(0).positive_or_zero(), ColIndex::new(0)); 52 | /// assert_eq!(ColIndex::new(-37).positive_or_zero(), ColIndex::new(0)); 53 | /// ``` 54 | pub fn positive_or_zero(self) -> AxisIndex { 55 | AxisIndex::new(self.val.max(0)) 56 | } 57 | } 58 | 59 | impl From for AxisIndex { 60 | fn from(v: i32) -> Self { 61 | AxisIndex::new(v) 62 | } 63 | } 64 | impl Into for AxisIndex { 65 | fn into(self) -> i32 { 66 | self.val 67 | } 68 | } 69 | impl Into for AxisIndex { 70 | fn into(self) -> isize { 71 | self.val as isize 72 | } 73 | } 74 | impl>> Add for AxisIndex { 75 | type Output = Self; 76 | fn add(self, rhs: I) -> Self { 77 | AxisIndex::new(self.val + rhs.into().val) 78 | } 79 | } 80 | impl>> AddAssign for AxisIndex { 81 | fn add_assign(&mut self, rhs: I) { 82 | *self = *self + rhs; 83 | } 84 | } 85 | impl>> Sub for AxisIndex { 86 | type Output = Self; 87 | fn sub(self, rhs: I) -> Self { 88 | AxisIndex::new(self.val - rhs.into().val) 89 | } 90 | } 91 | impl>> SubAssign for AxisIndex { 92 | fn sub_assign(&mut self, rhs: I) { 93 | *self = *self - rhs; 94 | } 95 | } 96 | impl Sub for AxisIndex { 97 | type Output = AxisDiff; 98 | fn sub(self, rhs: Self) -> Self::Output { 99 | AxisDiff::new(self.val - rhs.val) 100 | } 101 | } 102 | impl>> Rem for AxisIndex { 103 | type Output = Self; 104 | 105 | fn rem(self, modulus: I) -> Self { 106 | Self::new(self.val % modulus.into().val) 107 | } 108 | } 109 | impl> + Copy> PartialEq for AxisIndex { 110 | fn eq(&self, other: &I) -> bool { 111 | let copy = *other; 112 | self.val == copy.into().val 113 | } 114 | } 115 | impl> + Copy> PartialOrd for AxisIndex { 116 | fn partial_cmp(&self, other: &I) -> Option { 117 | let copy = *other; 118 | Some(self.val.cmp(©.into().val)) 119 | } 120 | } 121 | impl Neg for AxisIndex { 122 | type Output = Self; 123 | 124 | fn neg(self) -> Self::Output { 125 | AxisIndex::new(-self.val) 126 | } 127 | } 128 | 129 | /// Wrapper for Ranges of AxisIndex to make them iterable. 130 | /// This should be removed once [#42168](https://github.com/rust-lang/rust/issues/42168) is stabilized. 131 | pub struct IndexRange(pub Range>); 132 | 133 | impl Iterator for IndexRange { 134 | type Item = AxisIndex; 135 | fn next(&mut self) -> Option { 136 | if self.0.start < self.0.end { 137 | let res = self.0.start; 138 | self.0.start += 1; 139 | Some(res) 140 | } else { 141 | None 142 | } 143 | } 144 | } 145 | 146 | /// AxisDiff (the base for ColDiff or RowDiff) specifies a difference between two coordinate points 147 | /// on a terminal grid. (i.e., a coordinate of a vector on the terminal cell grid) 148 | #[derive(Copy, Clone, Debug, Ord, Eq)] 149 | pub struct AxisDiff { 150 | val: i32, 151 | _dim: PhantomData, 152 | } 153 | 154 | impl AxisDiff { 155 | /// Create a new AxisDiff from an i32. Any i32 value is valid. 156 | pub fn new(v: i32) -> Self { 157 | AxisDiff { 158 | val: v, 159 | _dim: Default::default(), 160 | } 161 | } 162 | 163 | /// Unpack the AxisDiff to receive the raw i32 value. 164 | pub fn raw_value(self) -> i32 { 165 | self.into() 166 | } 167 | 168 | /// Calculate the AxisIndex that has the specified AxisDiff to the origin (i.e., 0). 169 | /// Technically this just converts an AxisIndex into an AxisDiff, but is semantically more 170 | /// explicit. 171 | /// 172 | /// # Examples: 173 | /// 174 | /// ``` 175 | /// use unsegen::base::{ColIndex, ColDiff}; 176 | /// assert_eq!(ColDiff::new(27).from_origin(), ColIndex::new(27)); 177 | /// ``` 178 | pub fn from_origin(self) -> AxisIndex { 179 | AxisIndex::new(self.val) 180 | } 181 | 182 | /// Try to convert the current value into a PositiveAxisDiff. 183 | /// If the conversion fails, the original value is returned. 184 | /// 185 | /// # Examples: 186 | /// 187 | /// ``` 188 | /// use unsegen::base::{ColDiff, Width}; 189 | /// assert_eq!(ColDiff::new(27).try_into_positive(), Ok(Width::new(27).unwrap())); 190 | /// assert_eq!(ColDiff::new(0).try_into_positive(), Ok(Width::new(0).unwrap())); 191 | /// assert_eq!(ColDiff::new(-37).try_into_positive(), Err(ColDiff::new(-37))); 192 | /// ``` 193 | pub fn try_into_positive(self) -> Result, Self> { 194 | PositiveAxisDiff::new(self.val).map_err(|()| self) 195 | } 196 | 197 | /// Convert the current value into a PositiveAxisDiff by taking the absolute value of the axis 198 | /// difference. 199 | /// 200 | /// # Examples: 201 | /// 202 | /// ``` 203 | /// use unsegen::base::{ColDiff, Width}; 204 | /// assert_eq!(ColDiff::new(27).abs(), Width::new(27).unwrap()); 205 | /// assert_eq!(ColDiff::new(0).abs(), Width::new(0).unwrap()); 206 | /// assert_eq!(ColDiff::new(-37).abs(), Width::new(37).unwrap()); 207 | /// ``` 208 | pub fn abs(self) -> PositiveAxisDiff { 209 | PositiveAxisDiff::new_unchecked(self.val.abs()) 210 | } 211 | 212 | /// Clamp the value into a positive or zero range 213 | /// 214 | /// # Examples: 215 | /// 216 | /// ``` 217 | /// use unsegen::base::ColDiff; 218 | /// assert_eq!(ColDiff::new(27).positive_or_zero(), ColDiff::new(27)); 219 | /// assert_eq!(ColDiff::new(0).positive_or_zero(), ColDiff::new(0)); 220 | /// assert_eq!(ColDiff::new(-37).positive_or_zero(), ColDiff::new(0)); 221 | /// ``` 222 | pub fn positive_or_zero(self) -> PositiveAxisDiff { 223 | PositiveAxisDiff::new_unchecked(self.val.max(0)) 224 | } 225 | } 226 | impl From for AxisDiff { 227 | fn from(v: i32) -> Self { 228 | AxisDiff::new(v) 229 | } 230 | } 231 | impl Into for AxisDiff { 232 | fn into(self) -> i32 { 233 | self.val 234 | } 235 | } 236 | impl>> Add for AxisDiff { 237 | type Output = Self; 238 | fn add(self, rhs: I) -> Self { 239 | AxisDiff::new(self.val + rhs.into().val) 240 | } 241 | } 242 | impl>> AddAssign for AxisDiff { 243 | fn add_assign(&mut self, rhs: I) { 244 | *self = *self + rhs; 245 | } 246 | } 247 | impl Mul for AxisDiff { 248 | type Output = Self; 249 | fn mul(self, rhs: i32) -> Self::Output { 250 | AxisDiff::new(self.val * rhs) 251 | } 252 | } 253 | impl Div for AxisDiff { 254 | type Output = AxisDiff; 255 | fn div(self, rhs: i32) -> Self::Output { 256 | AxisDiff::new(self.val / rhs) 257 | } 258 | } 259 | impl>> Sub for AxisDiff { 260 | type Output = Self; 261 | fn sub(self, rhs: I) -> Self { 262 | AxisDiff::new(self.val - rhs.into().val) 263 | } 264 | } 265 | impl>> SubAssign for AxisDiff { 266 | fn sub_assign(&mut self, rhs: I) { 267 | *self = *self - rhs; 268 | } 269 | } 270 | impl>> Rem for AxisDiff { 271 | type Output = Self; 272 | 273 | fn rem(self, modulus: I) -> Self { 274 | AxisDiff::new(self.val % modulus.into().val) 275 | } 276 | } 277 | impl> + Copy> PartialEq for AxisDiff { 278 | fn eq(&self, other: &I) -> bool { 279 | let copy = *other; 280 | self.val == copy.into().val 281 | } 282 | } 283 | impl> + Copy> PartialOrd for AxisDiff { 284 | fn partial_cmp(&self, other: &I) -> Option { 285 | let copy = *other; 286 | Some(self.val.cmp(©.into().val)) 287 | } 288 | } 289 | impl Neg for AxisDiff { 290 | type Output = Self; 291 | 292 | fn neg(self) -> Self::Output { 293 | AxisDiff::new(-self.val) 294 | } 295 | } 296 | 297 | /// PositiveAxisDiff (the base for Width or Height) specifies a non-negative (or absolute) 298 | /// difference between two coordinate points on a terminal grid. 299 | #[derive(Copy, Clone, Debug, Ord, Eq)] 300 | pub struct PositiveAxisDiff { 301 | val: i32, 302 | _dim: PhantomData, 303 | } 304 | 305 | impl PositiveAxisDiff { 306 | /// Create a new PositiveAxisDiff from an i32. 307 | /// If v < 0 the behavior is unspecified. 308 | /// 309 | /// # Examples: 310 | /// 311 | /// ``` 312 | /// use unsegen::base::Width; 313 | /// let _ = Width::new_unchecked(27); //Ok 314 | /// let _ = Width::new_unchecked(0); //Ok 315 | /// // let _ = Width::new_unchecked(-37); //Not allowed! 316 | /// ``` 317 | pub fn new_unchecked(v: i32) -> Self { 318 | assert!(v >= 0, "Invalid value for PositiveAxisDiff"); 319 | PositiveAxisDiff { 320 | val: v, 321 | _dim: Default::default(), 322 | } 323 | } 324 | 325 | /// Try to create a new PositiveAxisDiff from an i32. If v < 0 the behavior an error value is 326 | /// returned. 327 | /// 328 | /// # Examples: 329 | /// 330 | /// ``` 331 | /// use unsegen::base::Width; 332 | /// assert!(Width::new(27).is_ok()); 333 | /// assert!(Width::new(0).is_ok()); 334 | /// assert!(Width::new(-37).is_err()); 335 | /// ``` 336 | pub fn new(v: i32) -> Result { 337 | if v >= 0 { 338 | Ok(PositiveAxisDiff { 339 | val: v, 340 | _dim: Default::default(), 341 | }) 342 | } else { 343 | Err(()) 344 | } 345 | } 346 | 347 | /// Unpack the PositiveAxisDiff to receive the raw i32 value. 348 | pub fn raw_value(self) -> i32 { 349 | self.into() 350 | } 351 | 352 | /// Calculate the AxisIndex that has the specified PositiveAxisDiff to the origin (i.e., 0). 353 | /// Technically this just converts an AxisIndex into an PositiveAxisDiff, but is semantically 354 | /// more explicit. 355 | /// 356 | /// # Examples: 357 | /// 358 | /// ``` 359 | /// use unsegen::base::{ColIndex, Width}; 360 | /// assert_eq!(Width::new(27).unwrap().from_origin(), ColIndex::new(27)); 361 | /// ``` 362 | pub fn from_origin(self) -> AxisIndex { 363 | AxisIndex::new(self.val) 364 | } 365 | 366 | /// Check whether the given AxisIndex is in the range [0, self) 367 | /// 368 | /// # Examples: 369 | /// 370 | /// ``` 371 | /// use unsegen::base::{ColIndex, Width}; 372 | /// assert!(Width::new(37).unwrap().origin_range_contains(ColIndex::new(27))); 373 | /// assert!(Width::new(37).unwrap().origin_range_contains(ColIndex::new(0))); 374 | /// assert!(!Width::new(27).unwrap().origin_range_contains(ColIndex::new(27))); 375 | /// assert!(!Width::new(27).unwrap().origin_range_contains(ColIndex::new(37))); 376 | /// assert!(!Width::new(27).unwrap().origin_range_contains(ColIndex::new(-37))); 377 | /// ``` 378 | pub fn origin_range_contains(self, i: AxisIndex) -> bool { 379 | 0 <= i.val && i.val < self.val 380 | } 381 | 382 | /// Convert the positive axis index into an AxisDiff. 383 | /// 384 | /// # Examples: 385 | /// 386 | /// ``` 387 | /// use unsegen::base::{ColDiff, Width}; 388 | /// assert_eq!(Width::new(37).unwrap().to_signed(), ColDiff::new(37)); 389 | /// ``` 390 | pub fn to_signed(self) -> AxisDiff { 391 | AxisDiff::new(self.val) 392 | } 393 | } 394 | impl Into for PositiveAxisDiff { 395 | fn into(self) -> i32 { 396 | self.val 397 | } 398 | } 399 | impl Into for PositiveAxisDiff { 400 | fn into(self) -> usize { 401 | self.val as usize 402 | } 403 | } 404 | impl Into> for PositiveAxisDiff { 405 | fn into(self) -> AxisDiff { 406 | AxisDiff::new(self.val) 407 | } 408 | } 409 | impl>> Add for PositiveAxisDiff { 410 | type Output = Self; 411 | fn add(self, rhs: I) -> Self { 412 | PositiveAxisDiff::new_unchecked(self.val + rhs.into().val) 413 | } 414 | } 415 | impl>> AddAssign for PositiveAxisDiff { 416 | fn add_assign(&mut self, rhs: I) { 417 | *self = *self + rhs; 418 | } 419 | } 420 | impl Mul for PositiveAxisDiff { 421 | type Output = AxisDiff; 422 | fn mul(self, rhs: i32) -> Self::Output { 423 | AxisDiff::new(self.val * rhs) 424 | } 425 | } 426 | impl Mul for PositiveAxisDiff { 427 | type Output = Self; 428 | fn mul(self, rhs: usize) -> Self::Output { 429 | PositiveAxisDiff::new_unchecked(self.val * rhs as i32) 430 | } 431 | } 432 | impl Div for PositiveAxisDiff { 433 | type Output = AxisDiff; 434 | fn div(self, rhs: i32) -> Self::Output { 435 | AxisDiff::new(self.val / rhs) 436 | } 437 | } 438 | impl Div for PositiveAxisDiff { 439 | type Output = Self; 440 | fn div(self, rhs: usize) -> Self::Output { 441 | PositiveAxisDiff::new_unchecked(self.val / rhs as i32) 442 | } 443 | } 444 | impl>> Sub for PositiveAxisDiff { 445 | type Output = AxisDiff; 446 | fn sub(self, rhs: I) -> Self::Output { 447 | AxisDiff::new(self.val - rhs.into().val) 448 | } 449 | } 450 | impl>> Rem for PositiveAxisDiff { 451 | type Output = Self; 452 | fn rem(self, modulus: I) -> Self { 453 | PositiveAxisDiff::new_unchecked(self.val % modulus.into().val) 454 | } 455 | } 456 | impl> + Copy> PartialEq for PositiveAxisDiff { 457 | fn eq(&self, other: &I) -> bool { 458 | let copy = *other; 459 | self.val == copy.into().val 460 | } 461 | } 462 | impl> + Copy> PartialOrd for PositiveAxisDiff { 463 | fn partial_cmp(&self, other: &I) -> Option { 464 | let copy = *other; 465 | Some(self.val.cmp(©.into().val)) 466 | } 467 | } 468 | impl Sum for PositiveAxisDiff { 469 | fn sum(iter: I) -> Self 470 | where 471 | I: Iterator, 472 | { 473 | iter.fold(PositiveAxisDiff::new_unchecked(0), PositiveAxisDiff::add) 474 | } 475 | } 476 | impl<'a, T: 'a + AxisDimension + PartialOrd + Ord> Sum<&'a PositiveAxisDiff> 477 | for PositiveAxisDiff 478 | { 479 | fn sum(iter: I) -> Self 480 | where 481 | I: Iterator, 482 | { 483 | iter.into_iter().map(|i| *i).sum() 484 | } 485 | } 486 | impl From for PositiveAxisDiff { 487 | fn from(v: usize) -> Self { 488 | assert!( 489 | v < i32::max_value() as usize, 490 | "Invalid PositiveAxisDiff value" 491 | ); 492 | PositiveAxisDiff::new_unchecked(v as i32) 493 | } 494 | } 495 | impl Sum for AxisDiff { 496 | fn sum(iter: I) -> Self 497 | where 498 | I: Iterator, 499 | { 500 | iter.fold(AxisDiff::new(0), AxisDiff::add) 501 | } 502 | } 503 | impl<'a, T: 'a + AxisDimension + PartialOrd + Ord> Sum<&'a AxisDiff> for AxisDiff { 504 | fn sum(iter: I) -> Self 505 | where 506 | I: Iterator, 507 | { 508 | iter.into_iter().map(|i| *i).sum() 509 | } 510 | } 511 | 512 | // ---------------------------------------------------------------------------- 513 | // Concrete types for concrete dimensions ------------------------------------- 514 | // ---------------------------------------------------------------------------- 515 | 516 | /// Trait for all dimensions of a terminal grid. See RowDimension and ColDimension. 517 | /// You probably do not want to implement this trait yourself. 518 | pub trait AxisDimension: Copy { 519 | /// The equivalent ndarray dimension. (Used in `Axis(...)`) 520 | const NDARRAY_AXIS_NUMBER: usize; 521 | 522 | /// Get the value corresponding to this dimension from the tuple. 523 | fn get_dimension_value(val: (usize, usize)) -> usize; 524 | } 525 | 526 | /// The horizontal (i.e., x-) dimension of a terminal grid. See ColIndex, ColDiff, and Width. 527 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 528 | pub struct ColDimension; 529 | impl AxisDimension for ColDimension { 530 | const NDARRAY_AXIS_NUMBER: usize = 1; 531 | 532 | fn get_dimension_value(val: (usize, usize)) -> usize { 533 | val.1 534 | } 535 | } 536 | 537 | /// The vertical (i.e., y-) dimension of a terminal grid. See RowIndex, RowDiff and Height. 538 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 539 | pub struct RowDimension; 540 | impl AxisDimension for RowDimension { 541 | const NDARRAY_AXIS_NUMBER: usize = 0; 542 | 543 | fn get_dimension_value(val: (usize, usize)) -> usize { 544 | val.0 545 | } 546 | } 547 | 548 | /// An AxisIndex in x-dimension. 549 | pub type ColIndex = AxisIndex; 550 | 551 | /// An AxisDiff in x-dimension. 552 | pub type ColDiff = AxisDiff; 553 | 554 | /// A PositiveAxisDiff in x-dimension. 555 | pub type Width = PositiveAxisDiff; 556 | 557 | /// An AxisIndex in y-dimension. 558 | pub type RowIndex = AxisIndex; 559 | 560 | /// An AxisDiff in y-dimension. 561 | pub type RowDiff = AxisDiff; 562 | 563 | /// A PositiveAxisDiff in y-dimension. 564 | pub type Height = PositiveAxisDiff; 565 | 566 | // ---------------------------------------------------------------------------- 567 | // Wrapper types for line numbering ------------------------------------------- 568 | // ---------------------------------------------------------------------------- 569 | 570 | /// A type for enumerating lines by index (rather than by number), i.e., starting from 0. 571 | /// Conversions between LineNumber and LineIndex are always safe. 572 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Hash)] 573 | pub struct LineIndex(usize); 574 | impl LineIndex { 575 | /// Create a new LineIndex from a raw value. 576 | pub fn new(val: usize) -> Self { 577 | LineIndex(val) 578 | } 579 | 580 | /// Unpack the LineIndex and yield the underlying value. 581 | pub fn raw_value(self) -> usize { 582 | self.0 583 | } 584 | 585 | /// Checked integer subtraction. Computes self - rhs, returning None if the result is invalid 586 | /// (i.e., smaller than 0). 587 | /// 588 | /// # Examples: 589 | /// ``` 590 | /// use unsegen::base::LineIndex; 591 | /// assert_eq!(LineIndex::new(37).checked_sub(27), Some(LineIndex::new(10))); 592 | /// assert_eq!(LineIndex::new(27).checked_sub(37), None); 593 | /// ``` 594 | pub fn checked_sub(&self, rhs: usize) -> Option { 595 | let index = self.0; 596 | index.checked_sub(rhs).map(LineIndex) 597 | } 598 | } 599 | 600 | impl Into for LineIndex { 601 | fn into(self) -> usize { 602 | let LineIndex(index) = self; 603 | index 604 | } 605 | } 606 | 607 | impl From for LineIndex { 608 | fn from(LineNumber(raw_number): LineNumber) -> Self { 609 | // This is safe, as LineNumber (per invariant) is >= 1 610 | LineIndex::new(raw_number - 1) 611 | } 612 | } 613 | impl Add for LineIndex { 614 | type Output = Self; 615 | fn add(self, rhs: usize) -> Self { 616 | let raw_index: usize = self.into(); 617 | LineIndex::new(raw_index + rhs) 618 | } 619 | } 620 | impl AddAssign for LineIndex { 621 | fn add_assign(&mut self, rhs: usize) { 622 | *self = *self + rhs; 623 | } 624 | } 625 | impl Sub for LineIndex { 626 | type Output = Self; 627 | fn sub(self, rhs: usize) -> Self { 628 | let raw_index: usize = self.into(); 629 | LineIndex::new(raw_index - rhs) 630 | } 631 | } 632 | impl SubAssign for LineIndex { 633 | fn sub_assign(&mut self, rhs: usize) { 634 | *self = *self - rhs; 635 | } 636 | } 637 | impl fmt::Display for LineIndex { 638 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 639 | self.0.fmt(f) 640 | } 641 | } 642 | 643 | /// A type for enumerating lines by number (rather than by index), i.e., starting from 1. 644 | /// Conversions between LineNumber and LineIndex are always safe. 645 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Hash)] 646 | pub struct LineNumber(usize); //Invariant: value is always >= 1 647 | impl LineNumber { 648 | /// Create a new LineNumber from a raw value. 649 | /// 650 | /// # Panics: 651 | /// 652 | /// Panics if val is 0 (as line numbers start from 1). 653 | pub fn new(val: usize) -> Self { 654 | assert!(val > 0, "Invalid LineNumber: Number == 0"); 655 | LineNumber(val) 656 | } 657 | 658 | /// Unpack the LineNumber and yield the underlying value. 659 | pub fn raw_value(self) -> usize { 660 | self.0 661 | } 662 | 663 | /// Checked integer subtraction. Computes self - rhs, returning None if the result is invalid 664 | /// (i.e., smaller than 1). 665 | /// 666 | /// # Examples: 667 | /// ``` 668 | /// use unsegen::base::LineNumber; 669 | /// assert_eq!(LineNumber::new(37).checked_sub(27), Some(LineNumber::new(10))); 670 | /// assert_eq!(LineNumber::new(27).checked_sub(37), None); 671 | /// assert_eq!(LineNumber::new(1).checked_sub(1), None); 672 | /// assert_eq!(LineNumber::new(2).checked_sub(1), Some(LineNumber::new(1))); 673 | /// ``` 674 | pub fn checked_sub(&self, rhs: usize) -> Option { 675 | let index = self.0 - 1; // Safe according to invariant: self.0 >= 1 676 | index.checked_sub(rhs).map(|i| LineNumber(i + 1)) 677 | } 678 | } 679 | 680 | impl Into for LineNumber { 681 | fn into(self) -> usize { 682 | self.0 683 | } 684 | } 685 | impl From for LineNumber { 686 | fn from(LineIndex(raw_index): LineIndex) -> Self { 687 | LineNumber::new(raw_index + 1) 688 | } 689 | } 690 | impl Add for LineNumber { 691 | type Output = Self; 692 | fn add(self, rhs: usize) -> Self { 693 | let raw_number: usize = self.into(); 694 | LineNumber::new(raw_number + rhs) 695 | } 696 | } 697 | impl AddAssign for LineNumber { 698 | fn add_assign(&mut self, rhs: usize) { 699 | *self = *self + rhs; 700 | } 701 | } 702 | impl Sub for LineNumber { 703 | type Output = Self; 704 | fn sub(self, rhs: usize) -> Self { 705 | let raw_number: usize = self.into(); 706 | LineNumber::new(raw_number - rhs) 707 | } 708 | } 709 | impl SubAssign for LineNumber { 710 | fn sub_assign(&mut self, rhs: usize) { 711 | *self = *self - rhs; 712 | } 713 | } 714 | impl fmt::Display for LineNumber { 715 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 716 | self.0.fmt(f) 717 | } 718 | } 719 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | //! Raw terminal input events, common abstractions for application (component) behavior and means 2 | //! to easily distribute events. 3 | //! 4 | //! # Example: 5 | //! ``` 6 | //! use unsegen::input::*; 7 | //! use std::io::Read; 8 | //! 9 | //! struct Scroller { 10 | //! line_number: u32, 11 | //! end: u32, 12 | //! } 13 | //! 14 | //! impl Scrollable for Scroller { 15 | //! fn scroll_backwards(&mut self) -> OperationResult { 16 | //! if self.line_number > 0 { 17 | //! self.line_number -= 1; 18 | //! Ok(()) 19 | //! } else { 20 | //! Err(()) 21 | //! } 22 | //! } 23 | //! fn scroll_forwards(&mut self) -> OperationResult { 24 | //! if self.line_number < self.end - 1 { 25 | //! self.line_number += 1; 26 | //! Ok(()) 27 | //! } else { 28 | //! Err(()) 29 | //! } 30 | //! } 31 | //! } 32 | //! 33 | //! fn main() { 34 | //! let mut scroller = Scroller { 35 | //! line_number: 0, 36 | //! end: 5, 37 | //! }; 38 | //! 39 | //! // Read all inputs from something that implements Read 40 | //! for input in Input::read_all(&[b'1', b'2', b'3', b'4'][..]) { 41 | //! let input = input.unwrap(); 42 | //! 43 | //! // Define a chain of handlers for different kinds of events! 44 | //! // If a handler (Behavior) cannot process an input, it is passed down the chain. 45 | //! let leftover = input 46 | //! .chain((Key::Char('1'), || println!("Got a 1!"))) 47 | //! .chain( 48 | //! ScrollBehavior::new(&mut scroller) 49 | //! .backwards_on(Key::Char('2')) 50 | //! .forwards_on(Key::Char('3')), 51 | //! ) 52 | //! .chain(|i: Input| { 53 | //! if let Event::Key(Key::Char(c)) = i.event { 54 | //! println!("Got some char: {}", c); 55 | //! None // matches! event will be consumed 56 | //! } else { 57 | //! Some(i) 58 | //! } 59 | //! }) 60 | //! .finish(); 61 | //! if let Some(e) = leftover { 62 | //! println!("Could not handle input {:?}", e); 63 | //! } 64 | //! } 65 | //! 66 | //! // We could not scroll back first, but one line forwards later! 67 | //! assert_eq!(scroller.line_number, 1); 68 | //! } 69 | //! ``` 70 | 71 | use std::collections::HashSet; 72 | pub use termion::event::{Event, Key, MouseButton, MouseEvent}; 73 | use termion::input::{EventsAndRaw, TermReadEventsAndRaw}; 74 | 75 | use std::io; 76 | 77 | /// A structure corresponding to a single input event, e.g., a single keystroke or mouse event. 78 | /// 79 | /// In addition to the semantic Event enum itself, the raw bytes that created this event are 80 | /// available, as well. This is useful if the user wants to pass the input on to some other 81 | /// terminal-like abstraction under certain circumstances (e.g., when writing a terminal 82 | /// multiplexer). 83 | #[derive(Eq, PartialEq, Clone, Debug)] 84 | #[allow(missing_docs)] 85 | pub struct Input { 86 | pub event: Event, 87 | pub raw: Vec, 88 | } 89 | 90 | impl Input { 91 | /// Create an iterator that reads from the provided argument and converts the read bytes into 92 | /// a stream of `Input`s. 93 | /// 94 | /// Please note that the iterator blocks when no bytes are available from the `Read` source. 95 | pub fn read_all(read: R) -> InputIter { 96 | InputIter { 97 | inner: read.events_and_raw(), 98 | } 99 | } 100 | 101 | /// Begin matching and processing of the event. See `InputChain`. 102 | pub fn chain(self, behavior: B) -> InputChain { 103 | let chain_begin = InputChain { input: Some(self) }; 104 | chain_begin.chain(behavior) 105 | } 106 | 107 | /// Check whether this event is equal to the provided event-like argument. 108 | pub fn matches(&self, e: T) -> bool { 109 | self.event == e.to_event() 110 | } 111 | } 112 | 113 | /// An iterator of `Input` events. 114 | pub struct InputIter { 115 | inner: EventsAndRaw, 116 | } 117 | 118 | impl Iterator for InputIter { 119 | type Item = Result; 120 | 121 | fn next(&mut self) -> Option> { 122 | self.inner.next().map(|tuple| { 123 | tuple.map(|(event, raw)| Input { 124 | event: event, 125 | raw: raw, 126 | }) 127 | }) 128 | } 129 | } 130 | 131 | /// An intermediate element in a chain of `Behavior`s that are matched against the event and 132 | /// executed if applicable. 133 | /// 134 | /// # Examples: 135 | /// ``` 136 | /// use unsegen::input::*; 137 | /// 138 | /// let mut triggered_first = false; 139 | /// let mut triggered_second = false; 140 | /// let mut triggered_third = false; 141 | /// 142 | /// let input = Input { 143 | /// event: Event::Key(Key::Char('g')), 144 | /// raw: Vec::new(), //Incorrect, but does not matter for this example. 145 | /// }; 146 | /// 147 | /// let res = input 148 | /// .chain((Key::Char('f'), || triggered_first = true)) // does not match, passes event on 149 | /// .chain(|i: Input| if let Event::Key(Key::Char(_)) = i.event { 150 | /// triggered_second = true; 151 | /// None // matches! event will be consumed 152 | /// } else { 153 | /// Some(i) 154 | /// }) 155 | /// .chain((Key::Char('g'), || triggered_first = true)) // matches, but is not reached! 156 | /// .finish(); 157 | /// 158 | /// assert!(!triggered_first); 159 | /// assert!(triggered_second); 160 | /// assert!(!triggered_third); 161 | /// assert!(res.is_none()); 162 | /// ``` 163 | pub struct InputChain { 164 | input: Option, 165 | } 166 | 167 | impl InputChain { 168 | /// Add another behavior to the line of input processors that will try to consume the event one 169 | /// after another. 170 | pub fn chain(self, behavior: B) -> InputChain { 171 | if let Some(event) = self.input { 172 | InputChain { 173 | input: behavior.input(event), 174 | } 175 | } else { 176 | InputChain { input: None } 177 | } 178 | } 179 | 180 | /// Add another behavior to the line of input processors that will try to consume the event one 181 | /// after another. 182 | /// 183 | /// If this chain element consumes the input, `f` is executed. 184 | pub fn chain_and_then(self, behavior: B, f: impl FnOnce()) -> InputChain { 185 | if let Some(event) = self.input { 186 | let input = behavior.input(event); 187 | if input.is_none() { 188 | // Previously present, but now consumed 189 | f(); 190 | } 191 | InputChain { input } 192 | } else { 193 | InputChain { input: None } 194 | } 195 | } 196 | 197 | /// Unpack the final chain value. If the `Input` was consumed by some `Behavior`, the result 198 | /// will be None, otherwise the original `Input` will be returned. 199 | pub fn finish(self) -> Option { 200 | self.input 201 | } 202 | 203 | /// Execute the provided function only if the input was consumed previously in the chain. 204 | pub fn if_consumed(self, f: impl FnOnce()) -> Self { 205 | if self.input.is_none() { 206 | f() 207 | } 208 | self 209 | } 210 | 211 | /// Execute the provided function only if the input not was consumed previously in the chain. 212 | pub fn if_not_consumed(self, f: impl FnOnce()) -> Self { 213 | if self.input.is_some() { 214 | f() 215 | } 216 | self 217 | } 218 | } 219 | impl From for InputChain { 220 | fn from(input: Input) -> Self { 221 | InputChain { input: Some(input) } 222 | } 223 | } 224 | impl From> for InputChain { 225 | fn from(input: Option) -> Self { 226 | InputChain { input } 227 | } 228 | } 229 | 230 | /// Used conveniently supply `Event`-like arguments to a number of functions in the input module. 231 | /// For example, you can supply `Key::Up` instead of `Event::Key(Key::Up)`. 232 | /// 233 | /// Basically an `Into`, but we cannot use that as Event is a reexport of termion. 234 | pub trait ToEvent { 235 | /// Convert to an event. 236 | fn to_event(self) -> Event; 237 | } 238 | 239 | impl ToEvent for Key { 240 | fn to_event(self) -> Event { 241 | Event::Key(self) 242 | } 243 | } 244 | 245 | impl ToEvent for MouseEvent { 246 | fn to_event(self) -> Event { 247 | Event::Mouse(self) 248 | } 249 | } 250 | 251 | impl ToEvent for Event { 252 | fn to_event(self) -> Event { 253 | self 254 | } 255 | } 256 | 257 | /// Very thin wrapper around HashSet, mostly to conveniently insert `ToEvent`s. 258 | struct EventSet { 259 | events: HashSet, 260 | } 261 | impl EventSet { 262 | fn new() -> Self { 263 | EventSet { 264 | events: HashSet::new(), 265 | } 266 | } 267 | fn insert(&mut self, event: E) { 268 | self.events.insert(event.to_event()); 269 | } 270 | fn contains(&self, event: &Event) -> bool { 271 | self.events.contains(event) 272 | } 273 | } 274 | 275 | /// Something that reacts to input and possibly consumes it. 276 | /// 277 | /// An inplementor is free to check the `Input` for arbitrary criteria and return the input if not 278 | /// consumed. Note that the implementor should not _change_ the input event in that case. 279 | /// 280 | /// If the implementor somehow reacts to the input, it is generally a good idea to "consume" the 281 | /// value by returning None. This makes sure that subsequent `Behavior`s will not act. 282 | /// 283 | /// Another thing of note is that a Behavior is generally constructed on the fly and consumed in 284 | /// the `input` function! 285 | /// For specialised behavior that does not fit into the often used abstractions defined in this 286 | /// module (`Scrollable`, `Writable`, `Navigatable`, ...) the easiest way to construct a behavior 287 | /// is either using a `FnOnce(Input) -> Option` where the implementor has to decide whether 288 | /// the input matches the desired criteria or using a pair `(ToEvent, FnOnce())` where the function 289 | /// is only iff the `Input` to be processed matches the provided `Event`-like thing. 290 | pub trait Behavior { 291 | /// Receive, process and possibly consume the input. 292 | fn input(self, input: Input) -> Option; 293 | } 294 | 295 | impl Option> Behavior for F { 296 | fn input(self, input: Input) -> Option { 297 | self(input) 298 | } 299 | } 300 | 301 | impl Behavior for (E, F) { 302 | fn input(self, input: Input) -> Option { 303 | let (event, function) = self; 304 | if input.matches(event) { 305 | function(); 306 | None 307 | } else { 308 | Some(input) 309 | } 310 | } 311 | } 312 | 313 | impl Behavior for (&[E], F) { 314 | fn input(self, input: Input) -> Option { 315 | let (it, function) = self; 316 | for event in it { 317 | if input.matches(event.clone()) { 318 | function(); 319 | return None; 320 | } 321 | } 322 | Some(input) 323 | } 324 | } 325 | 326 | /// A common return type for Operations such as functions of `Scrollable`, `Writable`, 327 | /// `Navigatable`, etc. 328 | /// 329 | /// Ok(()) means: The input was processed successfully and should be consumed. 330 | /// Err(()) means: The input could not be processed and should be passed on to and processed by 331 | /// some other `Behavior`. 332 | pub type OperationResult = Result<(), ()>; 333 | fn pass_on_if_err(res: OperationResult, input: Input) -> Option { 334 | if res.is_err() { 335 | Some(input) 336 | } else { 337 | None 338 | } 339 | } 340 | 341 | // ScrollableBehavior ----------------------------------------------- 342 | 343 | /// Collection of triggers for functions of something `Scrollable` implementing `Behavior`. 344 | pub struct ScrollBehavior<'a, S: Scrollable + 'a> { 345 | scrollable: &'a mut S, 346 | to_beginning_on: EventSet, 347 | to_end_on: EventSet, 348 | backwards_on: EventSet, 349 | forwards_on: EventSet, 350 | } 351 | 352 | impl<'a, S: Scrollable> ScrollBehavior<'a, S> { 353 | /// Create the behavior to act on the provided ´Scrollable`. Add triggers using other functions! 354 | pub fn new(scrollable: &'a mut S) -> Self { 355 | ScrollBehavior { 356 | scrollable: scrollable, 357 | backwards_on: EventSet::new(), 358 | forwards_on: EventSet::new(), 359 | to_beginning_on: EventSet::new(), 360 | to_end_on: EventSet::new(), 361 | } 362 | } 363 | /// Make the behavior trigger the `scroll_to_beginning` function on the provided event. 364 | pub fn to_beginning_on(mut self, event: E) -> Self { 365 | self.to_beginning_on.insert(event); 366 | self 367 | } 368 | /// Make the behavior trigger the `scroll_to_end` function on the provided event. 369 | pub fn to_end_on(mut self, event: E) -> Self { 370 | self.to_end_on.insert(event); 371 | self 372 | } 373 | /// Make the behavior trigger the `scroll_backwards` function on the provided event. 374 | pub fn backwards_on(mut self, event: E) -> Self { 375 | self.backwards_on.insert(event); 376 | self 377 | } 378 | /// Make the behavior trigger the `scroll_forwards` function on the provided event. 379 | pub fn forwards_on(mut self, event: E) -> Self { 380 | self.forwards_on.insert(event); 381 | self 382 | } 383 | } 384 | 385 | impl<'a, S: Scrollable> Behavior for ScrollBehavior<'a, S> { 386 | fn input(self, input: Input) -> Option { 387 | if self.forwards_on.contains(&input.event) { 388 | pass_on_if_err(self.scrollable.scroll_forwards(), input) 389 | } else if self.backwards_on.contains(&input.event) { 390 | pass_on_if_err(self.scrollable.scroll_backwards(), input) 391 | } else if self.to_beginning_on.contains(&input.event) { 392 | pass_on_if_err(self.scrollable.scroll_to_beginning(), input) 393 | } else if self.to_end_on.contains(&input.event) { 394 | pass_on_if_err(self.scrollable.scroll_to_end(), input) 395 | } else { 396 | Some(input) 397 | } 398 | } 399 | } 400 | 401 | /// Something that can be scrolled. Use in conjunction with `ScrollBehavior` to manipulate when 402 | /// input arrives. 403 | /// 404 | /// Note that `scroll_to_beginning` and `scroll_to_end` should be implemented manually if a fast 405 | /// pass is available and performance is important. By default these functions call 406 | /// `scroll_backwards` and `scroll_forwards` respectively until they fail. 407 | #[allow(missing_docs)] 408 | pub trait Scrollable { 409 | fn scroll_backwards(&mut self) -> OperationResult; 410 | fn scroll_forwards(&mut self) -> OperationResult; 411 | fn scroll_to_beginning(&mut self) -> OperationResult { 412 | if self.scroll_backwards().is_err() { 413 | return Err(()); 414 | } else { 415 | while self.scroll_backwards().is_ok() {} 416 | Ok(()) 417 | } 418 | } 419 | fn scroll_to_end(&mut self) -> OperationResult { 420 | if self.scroll_forwards().is_err() { 421 | return Err(()); 422 | } else { 423 | while self.scroll_forwards().is_ok() {} 424 | Ok(()) 425 | } 426 | } 427 | } 428 | 429 | // WriteBehavior ------------------------------------------ 430 | 431 | /// Collection of triggers for functions of something `Writable` implementing `Behavior`. 432 | pub struct WriteBehavior<'a, W: Writable + 'a> { 433 | writable: &'a mut W, 434 | } 435 | impl<'a, W: Writable + 'a> WriteBehavior<'a, W> { 436 | /// Create a new Behavior for the `Writable`. 437 | pub fn new(writable: &'a mut W) -> Self { 438 | WriteBehavior { writable: writable } 439 | } 440 | } 441 | 442 | impl<'a, W: Writable + 'a> Behavior for WriteBehavior<'a, W> { 443 | fn input(self, input: Input) -> Option { 444 | if let Event::Key(Key::Char(c)) = input.event { 445 | pass_on_if_err(self.writable.write(c), input) 446 | } else { 447 | Some(input) 448 | } 449 | } 450 | } 451 | 452 | /// Something that can be written to in the sense of a text box, editor or text input. 453 | /// 454 | /// All inputs that correspond to keystrokes with a corresponding `char` representation will be 455 | /// converted and passed to the `Writable`. 456 | pub trait Writable { 457 | /// Process the provided char and report if it was processed successfully. 458 | fn write(&mut self, c: char) -> OperationResult; 459 | } 460 | 461 | // NavigateBehavior ------------------------------------------------ 462 | 463 | /// Collection of triggers for functions of something `Navigatable` implementing `Behavior`. 464 | pub struct NavigateBehavior<'a, N: Navigatable + 'a> { 465 | navigatable: &'a mut N, 466 | up_on: EventSet, 467 | down_on: EventSet, 468 | left_on: EventSet, 469 | right_on: EventSet, 470 | } 471 | 472 | impl<'a, N: Navigatable + 'a> NavigateBehavior<'a, N> { 473 | /// Create the behavior to act on the provided `Navigatable`. Add triggers using other functions! 474 | pub fn new(navigatable: &'a mut N) -> Self { 475 | NavigateBehavior { 476 | navigatable: navigatable, 477 | up_on: EventSet::new(), 478 | down_on: EventSet::new(), 479 | left_on: EventSet::new(), 480 | right_on: EventSet::new(), 481 | } 482 | } 483 | 484 | /// Make the behavior trigger the `move_up` function on the provided event. 485 | /// 486 | /// A typical candidate for `event` would be `Key::Up`. 487 | pub fn up_on(mut self, event: E) -> Self { 488 | self.up_on.insert(event); 489 | self 490 | } 491 | /// Make the behavior trigger the `move_down` function on the provided event. 492 | /// 493 | /// A typical candidate for `event` would be `Key::Down`. 494 | pub fn down_on(mut self, event: E) -> Self { 495 | self.down_on.insert(event); 496 | self 497 | } 498 | /// Make the behavior trigger the `move_left` function on the provided event. 499 | /// 500 | /// A typical candidate for `event` would be `Key::Left`. 501 | pub fn left_on(mut self, event: E) -> Self { 502 | self.left_on.insert(event); 503 | self 504 | } 505 | /// Make the behavior trigger the `move_right` function on the provided event. 506 | /// 507 | /// A typical candidate for `event` would be `Key::Right`. 508 | pub fn right_on(mut self, event: E) -> Self { 509 | self.right_on.insert(event); 510 | self 511 | } 512 | } 513 | 514 | impl<'a, N: Navigatable + 'a> Behavior for NavigateBehavior<'a, N> { 515 | fn input(self, input: Input) -> Option { 516 | if self.up_on.contains(&input.event) { 517 | pass_on_if_err(self.navigatable.move_up(), input) 518 | } else if self.down_on.contains(&input.event) { 519 | pass_on_if_err(self.navigatable.move_down(), input) 520 | } else if self.left_on.contains(&input.event) { 521 | pass_on_if_err(self.navigatable.move_left(), input) 522 | } else if self.right_on.contains(&input.event) { 523 | pass_on_if_err(self.navigatable.move_right(), input) 524 | } else { 525 | Some(input) 526 | } 527 | } 528 | } 529 | 530 | /// Something that can be navigated like a cursor in a text editor or character in a simple 2D 531 | /// game. 532 | #[allow(missing_docs)] 533 | pub trait Navigatable { 534 | fn move_up(&mut self) -> OperationResult; 535 | fn move_down(&mut self) -> OperationResult; 536 | fn move_left(&mut self) -> OperationResult; 537 | fn move_right(&mut self) -> OperationResult; 538 | } 539 | 540 | // EditBehavior --------------------------------------------------------- 541 | 542 | /// Collection of triggers for functions of something `Editable` implementing `Behavior`. 543 | pub struct EditBehavior<'a, E: Editable + 'a> { 544 | editable: &'a mut E, 545 | up_on: EventSet, 546 | down_on: EventSet, 547 | left_on: EventSet, 548 | right_on: EventSet, 549 | delete_forwards_on: EventSet, 550 | delete_backwards_on: EventSet, 551 | clear_on: EventSet, 552 | go_to_beginning_of_line_on: EventSet, 553 | go_to_end_of_line_on: EventSet, 554 | } 555 | 556 | impl<'a, E: Editable> EditBehavior<'a, E> { 557 | /// Create the behavior to act on the provided `Editable`. Add triggers using other functions! 558 | pub fn new(editable: &'a mut E) -> Self { 559 | EditBehavior { 560 | editable: editable, 561 | up_on: EventSet::new(), 562 | down_on: EventSet::new(), 563 | left_on: EventSet::new(), 564 | right_on: EventSet::new(), 565 | delete_forwards_on: EventSet::new(), 566 | delete_backwards_on: EventSet::new(), 567 | clear_on: EventSet::new(), 568 | go_to_beginning_of_line_on: EventSet::new(), 569 | go_to_end_of_line_on: EventSet::new(), 570 | } 571 | } 572 | 573 | /// Make the behavior trigger the `move_up` function on the provided event. 574 | /// 575 | /// A typical candidate for `event` would be `Key::Up`. 576 | pub fn up_on(mut self, event: T) -> Self { 577 | self.up_on.insert(event); 578 | self 579 | } 580 | /// Make the behavior trigger the `move_down` function on the provided event. 581 | /// 582 | /// A typical candidate for `event` would be `Key::Down`. 583 | pub fn down_on(mut self, event: T) -> Self { 584 | self.down_on.insert(event); 585 | self 586 | } 587 | /// Make the behavior trigger the `move_left` function on the provided event. 588 | /// 589 | /// A typical candidate for `event` would be `Key::Left`. 590 | pub fn left_on(mut self, event: T) -> Self { 591 | self.left_on.insert(event); 592 | self 593 | } 594 | /// Make the behavior trigger the `move_right` function on the provided event. 595 | /// 596 | /// A typical candidate for `event` would be `Key::Right`. 597 | pub fn right_on(mut self, event: T) -> Self { 598 | self.right_on.insert(event); 599 | self 600 | } 601 | /// Make the behavior trigger the `delete_forwards` function on the provided event. 602 | /// 603 | /// A typical candidate for `event` would be `Key::Delete`. 604 | pub fn delete_forwards_on(mut self, event: T) -> Self { 605 | self.delete_forwards_on.insert(event); 606 | self 607 | } 608 | /// Make the behavior trigger the `delete_backwards` function on the provided event. 609 | /// 610 | /// A typical candidate for `event` would be `Key::Backspace`. 611 | pub fn delete_backwards_on(mut self, event: T) -> Self { 612 | self.delete_backwards_on.insert(event); 613 | self 614 | } 615 | /// Make the behavior trigger the `clear` function on the provided event. 616 | pub fn clear_on(mut self, event: T) -> Self { 617 | self.clear_on.insert(event); 618 | self 619 | } 620 | /// Make the behavior trigger the `go_to_beginning_of_line` function on the provided event. 621 | /// 622 | /// A typical candidate for `event` would be `Key::Home`. 623 | pub fn go_to_beginning_of_line_on(mut self, event: T) -> Self { 624 | self.go_to_beginning_of_line_on.insert(event); 625 | self 626 | } 627 | /// Make the behavior trigger the `go_to_end_of_line_on` function on the provided event. 628 | /// 629 | /// A typical candidate for `event` would be `Key::End`. 630 | pub fn go_to_end_of_line_on(mut self, event: T) -> Self { 631 | self.go_to_end_of_line_on.insert(event); 632 | self 633 | } 634 | } 635 | 636 | impl<'a, E: Editable> Behavior for EditBehavior<'a, E> { 637 | fn input(self, input: Input) -> Option { 638 | if self.up_on.contains(&input.event) { 639 | pass_on_if_err(self.editable.move_up(), input) 640 | } else if self.down_on.contains(&input.event) { 641 | pass_on_if_err(self.editable.move_down(), input) 642 | } else if self.left_on.contains(&input.event) { 643 | pass_on_if_err(self.editable.move_left(), input) 644 | } else if self.right_on.contains(&input.event) { 645 | pass_on_if_err(self.editable.move_right(), input) 646 | } else if self.delete_forwards_on.contains(&input.event) { 647 | pass_on_if_err(self.editable.delete_forwards(), input) 648 | } else if self.delete_backwards_on.contains(&input.event) { 649 | pass_on_if_err(self.editable.delete_backwards(), input) 650 | } else if self.clear_on.contains(&input.event) { 651 | pass_on_if_err(self.editable.clear(), input) 652 | } else if self.go_to_beginning_of_line_on.contains(&input.event) { 653 | pass_on_if_err(self.editable.go_to_beginning_of_line(), input) 654 | } else if self.go_to_end_of_line_on.contains(&input.event) { 655 | pass_on_if_err(self.editable.go_to_end_of_line(), input) 656 | } else if let Event::Key(Key::Char(c)) = input.event { 657 | pass_on_if_err(self.editable.write(c), input) 658 | } else { 659 | Some(input) 660 | } 661 | } 662 | } 663 | 664 | /// Something that acts like a text editor. 665 | pub trait Editable: Navigatable + Writable { 666 | /// In the sense of pressing the "Delete" key. 667 | fn delete_forwards(&mut self) -> OperationResult; 668 | /// In the sense of pressing the "Backspace" key. 669 | fn delete_backwards(&mut self) -> OperationResult; 670 | /// In the sense of pressing the "Home" key. 671 | fn go_to_beginning_of_line(&mut self) -> OperationResult; 672 | /// In the sense of pressing the "End" key. 673 | fn go_to_end_of_line(&mut self) -> OperationResult; 674 | /// Remove all content. 675 | fn clear(&mut self) -> OperationResult; 676 | } 677 | -------------------------------------------------------------------------------- /src/widget/builtin/table.rs: -------------------------------------------------------------------------------- 1 | //! A table of widgets with static number of columns. 2 | //! 3 | //! Use by implementing `TableRow` and adding instances of that type to a `Table` using `rows_mut`. 4 | use base::basic_types::*; 5 | use base::{StyleModifier, Window}; 6 | use input::Scrollable; 7 | use input::{Behavior, Input, Navigatable, OperationResult}; 8 | use std::cell::Cell; 9 | use widget::{ 10 | layout_linearly, ColDemand, Demand, Demand2D, RenderingHints, RowDemand, SeparatingStyle, 11 | Widget, 12 | }; 13 | 14 | /// A single column in a `Table`. 15 | /// 16 | /// This does not store any data, but rather how to access a cell in a single column of a table 17 | /// and how it reacts to input. 18 | /// 19 | /// In a sense this is only necessary because we do not have variadic generics. 20 | pub struct Column { 21 | /// Immutable widget access. 22 | pub access: for<'a> fn(&'a T) -> Box, 23 | /// Input processing 24 | pub behavior: fn(&mut T, Input, &mut T::BehaviorContext) -> Option, 25 | } 26 | 27 | /// This trait both (statically) describes the layout of the table (`COLUMNS`) and represents a 28 | /// single row in the table. 29 | /// 30 | /// Implement this trait, if you want to create a `Table`! 31 | pub trait TableRow: 'static { 32 | /// Type that will be passed as a parameter to the `behavior` method of `Column`. 33 | type BehaviorContext; 34 | /// Define the behavior of individual columns of the table. 35 | const COLUMNS: &'static [Column]; 36 | 37 | /// Convenient access using `COLUMNS`. (Do not reimplement this.) 38 | fn num_columns() -> usize { 39 | Self::COLUMNS.len() 40 | } 41 | 42 | /// Calculate the vertical space demand of the current row. (Default: max of all cells.) 43 | fn height_demand(&self) -> RowDemand { 44 | let mut y_demand = Demand::zero(); 45 | for col in Self::COLUMNS.iter() { 46 | let demand2d = (col.access)(self).space_demand(); 47 | y_demand.max_assign(demand2d.height); 48 | } 49 | y_demand 50 | } 51 | } 52 | 53 | /// Mutable row access mapper to enforce invariants after mutation. 54 | pub struct RowsMut<'a, R: 'static + TableRow> { 55 | table: &'a mut Table, 56 | } 57 | 58 | impl<'a, R: 'static + TableRow> ::std::ops::Drop for RowsMut<'a, R> { 59 | fn drop(&mut self) { 60 | let _ = self.table.validate_row_pos(); 61 | } 62 | } 63 | 64 | impl<'a, R: 'static + TableRow> ::std::ops::Deref for RowsMut<'a, R> { 65 | type Target = Vec; 66 | fn deref(&self) -> &Self::Target { 67 | &self.table.rows 68 | } 69 | } 70 | 71 | impl<'a, R: 'static + TableRow> ::std::ops::DerefMut for RowsMut<'a, R> { 72 | fn deref_mut(&mut self) -> &mut Self::Target { 73 | &mut self.table.rows 74 | } 75 | } 76 | 77 | /// A table of widgets with static number of `Columns`. 78 | /// 79 | /// In order to create a table, you have to define a type for a row in the table and implement 80 | /// `TableRow` for it. Then add instances of that type using `rows_mut`. 81 | /// 82 | /// At any time, a single cell of the table is active. Send user input to the cell by adding the 83 | /// result of `current_cell_behavior()` to an `InputChain`. 84 | /// A table is also `Navigatable` by which the user can change which cell is the currently active 85 | /// one. 86 | pub struct Table { 87 | rows: Vec, 88 | row_pos: u32, 89 | col_pos: u32, 90 | last_draw_pos: Cell<(u32, RowIndex)>, 91 | } 92 | 93 | impl Table { 94 | /// Create an empty table and specify how rows/columns and the currently active cell will be 95 | /// distinguished. 96 | pub fn new() -> Self { 97 | Table { 98 | rows: Vec::new(), 99 | row_pos: 0, 100 | col_pos: 0, 101 | last_draw_pos: Cell::new((0, RowIndex::new(0))), 102 | } 103 | } 104 | 105 | /// Access the content of the table mutably. 106 | pub fn rows_mut<'a>(&'a mut self) -> RowsMut<'a, R> { 107 | RowsMut { table: self } 108 | } 109 | 110 | /// Access the content of the table immutably. 111 | pub fn rows(&self) -> &Vec { 112 | &self.rows 113 | } 114 | 115 | fn validate_row_pos(&mut self) -> Result<(), ()> { 116 | let max_pos = (self.rows.len() as u32).checked_sub(1).unwrap_or(0); 117 | if self.row_pos > max_pos { 118 | self.row_pos = max_pos; 119 | Err(()) 120 | } else { 121 | Ok(()) 122 | } 123 | } 124 | 125 | fn validate_col_pos(&mut self) -> Result<(), ()> { 126 | let max_pos = R::num_columns() as u32 - 1; 127 | if self.col_pos > max_pos { 128 | self.col_pos = max_pos; 129 | Err(()) 130 | } else { 131 | Ok(()) 132 | } 133 | } 134 | 135 | /// Get access to the currently active row. 136 | pub fn current_row(&self) -> Option<&R> { 137 | self.rows.get(self.row_pos as usize) 138 | } 139 | 140 | /// Get mutable access to the currently active row. 141 | pub fn current_row_mut(&mut self) -> Option<&mut R> { 142 | self.rows.get_mut(self.row_pos as usize) 143 | } 144 | 145 | /// Get the currently active column. 146 | pub fn current_col(&self) -> &'static Column { 147 | &R::COLUMNS[self.col_pos as usize] 148 | } 149 | 150 | fn pass_event_to_current_cell( 151 | &mut self, 152 | i: Input, 153 | p: &mut R::BehaviorContext, 154 | ) -> Option { 155 | let col_behavior = self.current_col().behavior; 156 | if let Some(row) = self.current_row_mut() { 157 | col_behavior(row, i, p) 158 | } else { 159 | Some(i) 160 | } 161 | } 162 | 163 | /// Create a `Behavior` which can be used to send input directly to the currently active cell 164 | /// by adding it to an `InputChain`. 165 | pub fn current_cell_behavior<'a, 'b>( 166 | &'a mut self, 167 | p: &'b mut R::BehaviorContext, 168 | ) -> CurrentCellBehavior<'a, 'b, R> { 169 | CurrentCellBehavior { table: self, p } 170 | } 171 | 172 | /// Prepare for drawing as a `Widget`. 173 | pub fn as_widget<'a>(&'a self) -> TableWidget<'a, R> { 174 | TableWidget { 175 | table: self, 176 | row_sep_style: SeparatingStyle::None, 177 | col_sep_style: SeparatingStyle::None, 178 | focused_style: StyleModifier::new(), 179 | min_context: 1, 180 | } 181 | } 182 | } 183 | 184 | /// Pass all behavior to the currently active cell. 185 | pub struct CurrentCellBehavior<'a, 'b, R: TableRow + 'static> { 186 | table: &'a mut Table, 187 | p: &'b mut R::BehaviorContext, 188 | } 189 | 190 | impl Behavior for CurrentCellBehavior<'_, '_, R> { 191 | fn input(self, i: Input) -> Option { 192 | self.table.pass_event_to_current_cell(i, self.p) 193 | } 194 | } 195 | 196 | /// A `Widget` representing a `LineEdit` 197 | /// 198 | /// It allows for customization of vertical/horizontal separation styles and style for the focused 199 | /// cell. 200 | pub struct TableWidget<'a, R: TableRow + 'static> { 201 | table: &'a Table, 202 | row_sep_style: SeparatingStyle, 203 | col_sep_style: SeparatingStyle, 204 | focused_style: StyleModifier, 205 | min_context: u32, 206 | } 207 | 208 | impl<'a, R: TableRow + 'static> TableWidget<'a, R> { 209 | /// Specify the style for visual vertical separation (default: None) 210 | pub fn row_separation(mut self, style: SeparatingStyle) -> Self { 211 | self.row_sep_style = style; 212 | self 213 | } 214 | 215 | /// Specify the style for visual horizontal separation (default: None) 216 | pub fn col_separation(mut self, style: SeparatingStyle) -> Self { 217 | self.col_sep_style = style; 218 | self 219 | } 220 | 221 | /// Specify the style override for the active cell (default: None) 222 | pub fn focused(mut self, style: StyleModifier) -> Self { 223 | self.focused_style = style; 224 | self 225 | } 226 | 227 | /// Specify the minimum number of rows shown below/above the active row (if possible). Default: 228 | /// 1 229 | pub fn min_context(mut self, rows: u32) -> Self { 230 | self.min_context = rows; 231 | self 232 | } 233 | 234 | fn layout_columns(&self, window: &Window) -> Box<[Width]> { 235 | let mut x_demands = vec![Demand::zero(); R::num_columns()]; 236 | for row in self.table.rows.iter() { 237 | for (col_num, col) in R::COLUMNS.iter().enumerate() { 238 | let demand2d = (col.access)(row).space_demand(); 239 | x_demands[col_num].max_assign(demand2d.width); 240 | } 241 | } 242 | let separator_width = self.col_sep_style.width(); 243 | let weights = std::iter::repeat(1.0) 244 | .take(x_demands.len()) 245 | .collect::>(); 246 | layout_linearly(window.get_width(), separator_width, &x_demands, &weights) 247 | } 248 | 249 | fn draw_row<'w>( 250 | &self, 251 | row: &R, 252 | row_index: u32, 253 | mut window: Window<'w>, 254 | column_widths: &[Width], 255 | hints: RenderingHints, 256 | ) { 257 | if let (1, &SeparatingStyle::AlternatingStyle(modifier)) = 258 | (row_index % 2, &self.row_sep_style) 259 | { 260 | window.modify_default_style(modifier); 261 | } 262 | 263 | let mut iter = R::COLUMNS 264 | .iter() 265 | .zip(column_widths.iter()) 266 | .enumerate() 267 | .peekable(); 268 | while let Some((col_index, (col, &pos))) = iter.next() { 269 | let (mut cell_window, r) = window 270 | .split(pos.from_origin()) 271 | .expect("valid split pos from layout"); 272 | window = r; 273 | 274 | if let (1, &SeparatingStyle::AlternatingStyle(modifier)) = 275 | (col_index % 2, &self.col_sep_style) 276 | { 277 | cell_window.modify_default_style(modifier); 278 | } 279 | 280 | let cell_draw_hints = 281 | if row_index == self.table.row_pos && col_index as u32 == self.table.col_pos { 282 | cell_window.modify_default_style(self.focused_style); 283 | hints 284 | } else { 285 | hints.active(false) 286 | }; 287 | 288 | cell_window.clear(); // Fill background using new style 289 | (col.access)(row).draw(cell_window, cell_draw_hints); 290 | if let (Some(_), &SeparatingStyle::Draw(ref c)) = (iter.peek(), &self.col_sep_style) { 291 | if window.get_width() > 0 { 292 | let (mut sep_window, r) = window 293 | .split(Width::from(c.width()).from_origin()) 294 | .expect("valid split pos from layout"); 295 | window = r; 296 | sep_window.fill(c.clone()); 297 | } 298 | } 299 | } 300 | } 301 | fn rows_space_demand(&self, rows: &[R]) -> Demand2D { 302 | let mut x_demands = vec![Demand::exact(0); R::num_columns()]; 303 | let mut y_demand = Demand::zero(); 304 | 305 | let mut row_iter = rows.iter().peekable(); 306 | while let Some(row) = row_iter.next() { 307 | let mut row_max_y = Demand::exact(0); 308 | for (col_num, col) in R::COLUMNS.iter().enumerate() { 309 | let demand2d = (col.access)(row).space_demand(); 310 | x_demands[col_num].max_assign(demand2d.width); 311 | row_max_y.max_assign(demand2d.height) 312 | } 313 | y_demand += row_max_y; 314 | if row_iter.peek().is_some() { 315 | y_demand += Demand::exact(self.row_sep_style.height()); 316 | } 317 | } 318 | 319 | //Account all separators between cols 320 | let x_demand = x_demands.iter().sum::() 321 | + ColDemand::exact( 322 | (self.col_sep_style.width() * (x_demands.len() as i32 - 1)).positive_or_zero(), 323 | ); 324 | Demand2D { 325 | width: x_demand, 326 | height: y_demand, 327 | } 328 | } 329 | } 330 | 331 | impl<'a, R: TableRow + 'static> Widget for TableWidget<'a, R> { 332 | fn space_demand(&self) -> Demand2D { 333 | self.rows_space_demand(&self.table.rows[..]) 334 | } 335 | fn draw(&self, window: Window, hints: RenderingHints) { 336 | fn split_top(window: Window, pos: RowIndex) -> (Window, Option) { 337 | match window.split(pos) { 338 | Ok((window, below)) => (window, Some(below)), 339 | Err(window) => (window, None), 340 | } 341 | } 342 | 343 | fn split_bottom(window: Window, pos: RowIndex) -> (Option, Window) { 344 | let split_pos = (window.get_height().from_origin() - pos).from_origin(); 345 | match window.split(split_pos) { 346 | Ok((above, window)) => (Some(above), window), 347 | Err(window) => (None, window), 348 | } 349 | } 350 | 351 | let separator_height = if let &SeparatingStyle::Draw(_) = &self.row_sep_style { 352 | Height::new_unchecked(1) 353 | } else { 354 | Height::new_unchecked(0) 355 | }; 356 | 357 | let max_height = window.get_height(); 358 | let row_height = |r: &R| r.height_demand().max.unwrap_or(max_height); //TODO: choose min or max here and below? 359 | let demand_height = |d: Demand2D| d.height.max.unwrap_or(max_height); 360 | 361 | let column_widths = self.layout_columns(&window); 362 | 363 | let current = if let Some(r) = self.table.current_row() { 364 | r 365 | } else { 366 | return; 367 | }; 368 | let current_row_height = row_height(current); 369 | let current_row_pos = self.table.row_pos; 370 | 371 | let (old_pos, old_draw_row) = self.table.last_draw_pos.get(); 372 | let current_row_begin = match old_pos.cmp(¤t_row_pos) { 373 | std::cmp::Ordering::Less => { 374 | let range = &self.table.rows[old_pos as usize..current_row_pos as usize]; 375 | old_draw_row + demand_height(self.rows_space_demand(range)) + separator_height 376 | } 377 | std::cmp::Ordering::Equal => old_draw_row, 378 | std::cmp::Ordering::Greater => { 379 | let range = &self.table.rows 380 | [current_row_pos as usize..(old_pos as usize).min(self.table.rows.len())]; 381 | old_draw_row - demand_height(self.rows_space_demand(range)) - separator_height 382 | } 383 | }; 384 | 385 | let min_diff = Height::new_unchecked(self.min_context as i32); 386 | 387 | let current_row_begin = current_row_begin 388 | .min(window.get_height().from_origin() - current_row_height - min_diff) 389 | .max(min_diff.from_origin()); 390 | 391 | let widgets_above = &self.table.rows[..current_row_pos as usize]; 392 | let widgets_below = &self.table.rows[(current_row_pos + 1) as usize..]; 393 | 394 | let max_above_height = demand_height(self.rows_space_demand(widgets_above)) 395 | + if widgets_above.is_empty() { 396 | Height::new_unchecked(0) 397 | } else { 398 | separator_height 399 | }; 400 | let max_below_height = demand_height(self.rows_space_demand(widgets_below)) 401 | + if widgets_below.is_empty() { 402 | Height::new_unchecked(0) 403 | } else { 404 | separator_height 405 | }; 406 | 407 | let min_current_pos_begin = 408 | window.get_height().from_origin() - max_below_height - current_row_height; 409 | let max_current_pos_begin = max_above_height.from_origin(); 410 | 411 | let current_row_begin = current_row_begin 412 | .max(min_current_pos_begin) 413 | .min(max_current_pos_begin); 414 | 415 | self.table 416 | .last_draw_pos 417 | .set((current_row_pos, current_row_begin)); 418 | 419 | let (window, mut below) = split_top(window, current_row_begin + current_row_height); 420 | let (mut above, window) = split_bottom(window, current_row_height.from_origin()); 421 | 422 | self.draw_row(current, current_row_pos, window, &column_widths, hints); 423 | 424 | // All rows below current 425 | for (row_pos, row) in widgets_below 426 | .iter() 427 | .enumerate() 428 | .map(|(i, row)| (i as u32 + current_row_pos + 1, row)) 429 | { 430 | if let &SeparatingStyle::Draw(ref c) = &self.row_sep_style { 431 | if let Some(w) = below { 432 | let (mut sep_window, rest) = split_top(w, RowIndex::from(1)); 433 | below = rest; 434 | 435 | sep_window.fill(c.clone()); 436 | } else { 437 | break; 438 | } 439 | } 440 | 441 | if let Some(w) = below { 442 | let (row_window, rest) = split_top(w, row_height(row).from_origin()); 443 | below = rest; 444 | self.draw_row(row, row_pos, row_window, &column_widths, hints); 445 | } else { 446 | break; 447 | } 448 | } 449 | 450 | // All rows above current 451 | for (row_pos, row) in widgets_above 452 | .iter() 453 | .enumerate() 454 | .rev() 455 | .map(|(i, row)| (i as u32, row)) 456 | { 457 | if let &SeparatingStyle::Draw(ref c) = &self.row_sep_style { 458 | if let Some(w) = above { 459 | let (rest, mut sep_window) = split_bottom(w, RowIndex::from(1)); 460 | above = rest; 461 | 462 | sep_window.fill(c.clone()); 463 | } else { 464 | break; 465 | } 466 | } 467 | 468 | if let Some(w) = above { 469 | let (rest, row_window) = split_bottom(w, row_height(row).from_origin()); 470 | above = rest; 471 | self.draw_row(row, row_pos, row_window, &column_widths, hints); 472 | } else { 473 | break; 474 | } 475 | } 476 | } 477 | } 478 | 479 | impl Navigatable for Table { 480 | fn move_up(&mut self) -> OperationResult { 481 | if self.row_pos > 0 { 482 | self.row_pos -= 1; 483 | Ok(()) 484 | } else { 485 | Err(()) 486 | } 487 | } 488 | fn move_down(&mut self) -> OperationResult { 489 | self.row_pos += 1; 490 | self.validate_row_pos() 491 | } 492 | fn move_left(&mut self) -> OperationResult { 493 | if self.col_pos != 0 { 494 | self.col_pos -= 1; 495 | Ok(()) 496 | } else { 497 | Err(()) 498 | } 499 | } 500 | fn move_right(&mut self) -> OperationResult { 501 | self.col_pos += 1; 502 | self.validate_col_pos() 503 | } 504 | } 505 | 506 | impl Scrollable for Table { 507 | fn scroll_backwards(&mut self) -> OperationResult { 508 | self.move_up() 509 | } 510 | fn scroll_forwards(&mut self) -> OperationResult { 511 | self.move_down() 512 | } 513 | fn scroll_to_beginning(&mut self) -> OperationResult { 514 | if self.row_pos != 0 { 515 | self.row_pos = 0; 516 | Ok(()) 517 | } else { 518 | Err(()) 519 | } 520 | } 521 | fn scroll_to_end(&mut self) -> OperationResult { 522 | let end = self.rows.len().saturating_sub(1) as u32; 523 | if self.row_pos != end { 524 | self.row_pos = end; 525 | Ok(()) 526 | } else { 527 | Err(()) 528 | } 529 | } 530 | } 531 | 532 | #[cfg(test)] 533 | mod test { 534 | use super::*; 535 | use base::test::FakeTerminal; 536 | use base::{GraphemeCluster, StyleModifier}; 537 | 538 | struct TestRow(String); 539 | impl TableRow for TestRow { 540 | type BehaviorContext = (); 541 | const COLUMNS: &'static [Column] = &[Column { 542 | access: |r| Box::new(r.0.as_str()), 543 | behavior: |_, _, _| None, 544 | }]; 545 | } 546 | 547 | fn test_table(num_rows: usize) -> Table { 548 | let mut table = Table::new(); 549 | { 550 | let mut rows = table.rows_mut(); 551 | for i in 0..num_rows { 552 | rows.push(TestRow(i.to_string())); 553 | } 554 | } 555 | table 556 | } 557 | 558 | fn test_table_str(lines: &[&str]) -> Table { 559 | let mut table = Table::new(); 560 | { 561 | let mut rows = table.rows_mut(); 562 | for l in lines { 563 | rows.push(TestRow(l.to_string())); 564 | } 565 | } 566 | table 567 | } 568 | 569 | fn aeq_table_draw( 570 | terminal_size: (u32, u32), 571 | solution: &str, 572 | table: &Table, 573 | f: impl Fn(TableWidget) -> TableWidget, 574 | ) { 575 | let mut term = FakeTerminal::with_size(terminal_size); 576 | f(table.as_widget()).draw(term.create_root_window(), RenderingHints::default()); 577 | assert_eq!( 578 | term, 579 | FakeTerminal::from_str(terminal_size, solution).expect("term from str"), 580 | "got <-> expected" 581 | ); 582 | } 583 | fn aeq_table_draw_focused_bold( 584 | terminal_size: (u32, u32), 585 | solution: &str, 586 | table: &Table, 587 | ) { 588 | aeq_table_draw(terminal_size, solution, table, |t: TableWidget| { 589 | t.focused(StyleModifier::new().bold(true)) 590 | }); 591 | } 592 | 593 | fn aeq_table_draw_focused_bold_sep_x( 594 | terminal_size: (u32, u32), 595 | solution: &str, 596 | table: &Table, 597 | ) { 598 | aeq_table_draw(terminal_size, solution, table, |t: TableWidget| { 599 | t.focused(StyleModifier::new().bold(true)) 600 | .row_separation(SeparatingStyle::Draw( 601 | GraphemeCluster::try_from('X').unwrap(), 602 | )) 603 | }); 604 | } 605 | 606 | #[test] 607 | fn smaller_than_terminal() { 608 | aeq_table_draw((1, 3), "0 1 2", &test_table(10), |t| t); 609 | } 610 | 611 | #[test] 612 | fn scroll_down_simple() { 613 | let mut table = test_table(6); 614 | let size = (1, 4); 615 | aeq_table_draw_focused_bold(size, "*0* 1 2 3", &table); 616 | table.move_down().unwrap(); 617 | aeq_table_draw_focused_bold(size, "0 *1* 2 3", &table); 618 | table.move_down().unwrap(); 619 | aeq_table_draw_focused_bold(size, "0 1 *2* 3", &table); 620 | table.move_down().unwrap(); 621 | aeq_table_draw_focused_bold(size, "1 2 *3* 4", &table); 622 | table.move_down().unwrap(); 623 | aeq_table_draw_focused_bold(size, "2 3 *4* 5", &table); 624 | table.move_down().unwrap(); 625 | aeq_table_draw_focused_bold(size, "2 3 4 *5*", &table); 626 | assert!(table.move_down().is_err()); 627 | } 628 | 629 | #[test] 630 | fn scroll_down_sep() { 631 | let mut table = test_table(4); 632 | let size = (1, 4); 633 | aeq_table_draw_focused_bold_sep_x(size, "*0* X 1 X", &table); 634 | table.move_down().unwrap(); 635 | aeq_table_draw_focused_bold_sep_x(size, "0 X *1* X", &table); 636 | table.move_down().unwrap(); 637 | aeq_table_draw_focused_bold_sep_x(size, "1 X *2* X", &table); 638 | table.move_down().unwrap(); 639 | aeq_table_draw_focused_bold_sep_x(size, "X 2 X *3*", &table); 640 | assert!(table.move_down().is_err()); 641 | } 642 | 643 | #[test] 644 | fn scroll_down_multiline() { 645 | let mut table = test_table_str(&["a\nb", "c", "d\ne\n", "f", "g\nh"]); 646 | let size = (1, 4); 647 | aeq_table_draw_focused_bold(size, "*ab* c d", &table); 648 | table.move_down().unwrap(); 649 | aeq_table_draw_focused_bold(size, "ab *c* d", &table); 650 | table.move_down().unwrap(); 651 | aeq_table_draw_focused_bold(size, "c *de* f", &table); 652 | table.move_down().unwrap(); 653 | aeq_table_draw_focused_bold(size, "de *f* g", &table); 654 | table.move_down().unwrap(); 655 | aeq_table_draw_focused_bold(size, "d f *gh*", &table); 656 | assert!(table.move_down().is_err()); 657 | } 658 | 659 | #[test] 660 | fn scroll_down_multiline_sep() { 661 | let mut table = test_table_str(&["a\nb", "c", "d\ne\nf", "f", "g\nh"]); 662 | let size = (1, 4); 663 | aeq_table_draw_focused_bold_sep_x(size, "*ab* X c", &table); 664 | table.move_down().unwrap(); 665 | aeq_table_draw_focused_bold_sep_x(size, "a X *c* X", &table); 666 | table.move_down().unwrap(); 667 | aeq_table_draw_focused_bold_sep_x(size, "X *def*", &table); 668 | table.move_down().unwrap(); 669 | aeq_table_draw_focused_bold_sep_x(size, "d X *f* X", &table); 670 | table.move_down().unwrap(); 671 | aeq_table_draw_focused_bold_sep_x(size, "f X *gh*", &table); 672 | assert!(table.move_down().is_err()); 673 | } 674 | 675 | #[test] 676 | fn scroll_up_simple() { 677 | let mut table = test_table(6); 678 | let size = (1, 4); 679 | table.scroll_to_end().unwrap(); 680 | aeq_table_draw_focused_bold(size, "2 3 4 *5*", &table); 681 | table.move_up().unwrap(); 682 | aeq_table_draw_focused_bold(size, "2 3 *4* 5", &table); 683 | table.move_up().unwrap(); 684 | aeq_table_draw_focused_bold(size, "2 *3* 4 5", &table); 685 | table.move_up().unwrap(); 686 | aeq_table_draw_focused_bold(size, "1 *2* 3 4", &table); 687 | table.move_up().unwrap(); 688 | aeq_table_draw_focused_bold(size, "0 *1* 2 3", &table); 689 | table.move_up().unwrap(); 690 | aeq_table_draw_focused_bold(size, "*0* 1 2 3", &table); 691 | assert!(table.move_up().is_err()); 692 | } 693 | 694 | #[test] 695 | fn scroll_up_sep() { 696 | let mut table = test_table(4); 697 | let size = (1, 4); 698 | table.scroll_to_end().unwrap(); 699 | aeq_table_draw_focused_bold_sep_x(size, "X 2 X *3*", &table); 700 | table.move_up().unwrap(); 701 | aeq_table_draw_focused_bold_sep_x(size, "X *2* X 3", &table); 702 | table.move_up().unwrap(); 703 | aeq_table_draw_focused_bold_sep_x(size, "X *1* X 2", &table); 704 | table.move_up().unwrap(); 705 | aeq_table_draw_focused_bold_sep_x(size, "*0* X 1 X", &table); 706 | assert!(table.move_up().is_err()); 707 | } 708 | 709 | #[test] 710 | fn sep_alternate_rows() { 711 | let table = test_table(4); 712 | aeq_table_draw((1, 4), "0 *1* 2 *3*", &table, |t| { 713 | t.row_separation(SeparatingStyle::AlternatingStyle( 714 | StyleModifier::new().bold(true), 715 | )) 716 | }); 717 | } 718 | 719 | #[test] 720 | fn sep_char() { 721 | let table = test_table(4); 722 | aeq_table_draw((1, 7), "0 X 1 X 2 X 3", &table, |t| { 723 | t.row_separation(SeparatingStyle::Draw( 724 | GraphemeCluster::try_from('X').unwrap(), 725 | )) 726 | }); 727 | } 728 | 729 | #[test] 730 | fn sep_none() { 731 | let table = test_table(4); 732 | aeq_table_draw((1, 4), "0 1 2 3", &table, |t| { 733 | t.row_separation(SeparatingStyle::None) 734 | }); 735 | } 736 | } 737 | --------------------------------------------------------------------------------