From 672b940bc509d7097f90c1db976bf830451117f3 Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 6 Nov 2021 22:09:58 +0100 Subject: [PATCH] Fix rendering; added missing props in constructor --- Cargo.toml | 1 + examples/demo.rs | 536 ++++++++++++++++++++++++-------------- examples/utils/context.rs | 102 -------- examples/utils/input.rs | 84 ------ examples/utils/keymap.rs | 48 ---- examples/utils/mod.rs | 30 --- src/lib.rs | 30 ++- src/widget.rs | 88 ++++--- 8 files changed, 411 insertions(+), 508 deletions(-) delete mode 100644 examples/utils/context.rs delete mode 100644 examples/utils/input.rs delete mode 100644 examples/utils/keymap.rs delete mode 100644 examples/utils/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 8cdb8e8..29a8887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ repository = "https://github.com/veeso/tui-realm-treeview" orange-trees = "0.1.0" #tuirealm = { version = "^1.0.0", default-features = false, features = [ "derive" ]} tuirealm = { git = "https://github.com/veeso/tui-realm", branch = "new-api", default-features = false, features = [ "derive" ] } +unicode-width = "0.1.8" [dev-dependencies] crossterm = "0.20" diff --git a/examples/demo.rs b/examples/demo.rs index ae17c9a..7f20045 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -21,73 +21,75 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -mod utils; -use utils::context::Context; -use utils::keymap::*; - use std::path::{Path, PathBuf}; -use std::thread::sleep; use std::time::Duration; -use tui_realm_stdlib::{input, label}; -use tuirealm::props::{ - borders::{BorderType, Borders}, - Alignment, +use tui_realm_stdlib::{Input, Phantom}; +use tuirealm::{ + application::PollStrategy, + command::{Cmd, CmdResult, Direction, Position}, + event::{Event, Key, KeyEvent, KeyModifiers}, + props::{Alignment, AttrValue, Attribute, BorderType, Borders, Color, InputType, Style}, + terminal::TerminalBridge, + Application, Component, EventListenerCfg, MockComponent, NoUserEvent, State, StateValue, Sub, + SubClause, SubEventClause, Update, View, }; -use tuirealm::{Msg, Payload, PropsBuilder, Update, Value, View}; // tui -use tuirealm::tui::layout::{Constraint, Direction, Layout}; -use tuirealm::tui::style::Color; +use tuirealm::tui::layout::{Constraint, Direction as LayoutDirection, Layout}; // treeview -use tui_realm_treeview::{Node, Tree, TreeView, TreeViewPropsBuilder}; - -const COMPONENT_INPUT: &str = "INPUT"; -const COMPONENT_LABEL: &str = "LABEL"; -const COMPONENT_TREEVIEW: &str = "TREEVIEW"; +use tui_realm_treeview::{Node, Tree, TreeView, TREE_CMD_CLOSE, TREE_CMD_OPEN}; const MAX_DEPTH: usize = 3; +// -- message +#[derive(Debug, PartialEq)] +pub enum Msg { + AppClose, + ExtendDir(String), + FsTreeBlur, + GoToBlur, + GoTo(PathBuf), + GoToUpperDir, + None, +} + +// Let's define the component ids for our application +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum Id { + FsTree, + GlobalListener, + GoTo, +} + struct Model { path: PathBuf, tree: Tree, quit: bool, // Becomes true when the user presses redraw: bool, // Tells whether to refresh the UI; performance optimization - view: View, + terminal: TerminalBridge, } impl Model { - fn new(view: View, p: &Path) -> Self { + fn new(p: &Path) -> Self { Model { quit: false, redraw: true, - view, tree: Tree::new(Self::dir_tree(p, MAX_DEPTH)), path: p.to_path_buf(), + terminal: TerminalBridge::new().expect("Could not initialize terminal"), } } - fn quit(&mut self) { - self.quit = true; - } - - fn redraw(&mut self) { - self.redraw = true; - } - - fn reset(&mut self) { - self.redraw = false; - } - pub fn scan_dir(&mut self, p: &Path) { self.path = p.to_path_buf(); self.tree = Tree::new(Self::dir_tree(p, MAX_DEPTH)); } - pub fn upper_dir(&self) -> Option<&Path> { - self.path.parent() + pub fn upper_dir(&self) -> Option { + self.path.parent().map(|x| x.to_path_buf()) } - pub fn extend_dir(&mut self, id: &str, p: &Path, depth: usize) { - if let Some(node) = self.tree.query_mut(id) { + pub fn extend_dir(&mut self, id: &String, p: &Path, depth: usize) { + if let Some(node) = self.tree.root_mut().query_mut(id) { if depth > 0 && p.is_dir() { // Clear node node.clear(); @@ -115,186 +117,320 @@ impl Model { } node } + + fn view(&mut self, app: &mut Application) { + let _ = self.terminal.raw_mut().draw(|f| { + // Prepare chunks + let chunks = Layout::default() + .direction(LayoutDirection::Vertical) + .margin(1) + .constraints([Constraint::Min(5), Constraint::Length(3)].as_ref()) + .split(f.size()); + app.view(&Id::FsTree, f, chunks[0]); + app.view(&Id::GoTo, f, chunks[1]); + }); + } + + fn reload_tree(&mut self, view: &mut View) { + let current_node = match view.state(&Id::FsTree).ok().unwrap() { + State::One(StateValue::String(id)) => Some(id), + _ => None, + }; + // Remount tree + assert!(view.umount(&Id::FsTree).is_ok()); + assert!(view + .mount( + Id::FsTree, + Box::new(FsTree::new(self.tree.clone(), current_node)) + ) + .is_ok()); + assert!(view.active(&Id::FsTree).is_ok()); + } } fn main() { - // let's create a context: the context contains the backend of crossterm and the input handler - let mut ctx: Context = Context::new(); - // Enter alternate screen - ctx.enter_alternate_screen(); - // Clear screen - ctx.clear_screen(); // Make model - let mut model: Model = Model::new( - View::init(), - std::env::current_dir().ok().unwrap().as_path(), - ); - // Mount the component you need; we'll use a Label and an Input - model.view.mount( - COMPONENT_LABEL, - Box::new(label::Label::new( - label::LabelPropsBuilder::default() - .with_foreground(Color::Cyan) - .with_text(String::from( - "Selected node will appear here after a submit", - )) - .build(), - )), - ); - // Mount input - model.view.mount( - COMPONENT_INPUT, - Box::new(input::Input::new( - input::InputPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue) - .with_label("Go to...", Alignment::Left) - .with_foreground(Color::LightBlue) - .build(), - )), - ); - let title: String = model.path.to_string_lossy().to_string(); - // Moount tree - model.view.mount( - COMPONENT_TREEVIEW, - Box::new(TreeView::new( - TreeViewPropsBuilder::default() - .with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow) - .with_foreground(Color::LightYellow) - .with_background(Color::Black) - .with_title(title, Alignment::Left) - .with_tree(model.tree.root()) - .with_highlighted_str("🚀") - .keep_state(true) - .with_max_page_steps(8) - .build(), - )), + let mut model: Model = Model::new(std::env::current_dir().ok().unwrap().as_path()); + let _ = model.terminal.enable_raw_mode(); + let _ = model.terminal.enter_alternate_screen(); + // Setup app + let mut app: Application = Application::init( + EventListenerCfg::default().default_input_listener(Duration::from_millis(10)), ); + assert!(app + .mount( + Id::FsTree, + Box::new(FsTree::new(model.tree.clone(), None)), + vec![] + ) + .is_ok()); + assert!(app + .mount(Id::GoTo, Box::new(GoTo::default()), vec![]) + .is_ok()); + // Mount global listener which will listen for + assert!(app + .mount( + Id::GlobalListener, + Box::new(GlobalListener::default()), + vec![Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }), + SubClause::Always + )] + ) + .is_ok()); // We need to give focus to input then - model.view.active(COMPONENT_TREEVIEW); + assert!(app.active(&Id::FsTree).is_ok()); // let's loop until quit is true while !model.quit { - // Listen for input events - if let Ok(Some(ev)) = ctx.input_hnd.read_event() { - // Pass event to view - let msg = model.view.on(ev); - model.redraw(); - // Call the elm friend update - model.update(msg); + // Tick + if let Ok(sz) = app.tick(&mut model, PollStrategy::Once) { + if sz > 0 { + // NOTE: redraw if at least one msg has been processed + model.redraw = true; + } } - // If redraw, draw interface + // Redraw if model.redraw { - // Call the elm friend vie1 function - view(&mut ctx, &model.view); - model.reset(); + model.view(&mut app); + model.redraw = false; } - sleep(Duration::from_millis(10)); } - // Let's drop the context finally - drop(ctx); + // Terminate terminal + let _ = model.terminal.leave_alternate_screen(); + let _ = model.terminal.disable_raw_mode(); + let _ = model.terminal.clear_screen(); } -fn view(ctx: &mut Context, view: &View) { - let _ = ctx.terminal.draw(|f| { - // Prepare chunks - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Length(1), - Constraint::Min(5), - Constraint::Length(3), - ] - .as_ref(), - ) - .split(f.size()); - view.render(COMPONENT_LABEL, f, chunks[0]); - view.render(COMPONENT_TREEVIEW, f, chunks[1]); - view.render(COMPONENT_INPUT, f, chunks[2]); - }); -} +// -- update -impl Update for Model { - fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> { - let ref_msg: Option<(&str, &Msg)> = msg.as_ref().map(|(s, msg)| (s.as_str(), msg)); - match ref_msg { - None => None, // Exit after None - Some(msg) => match msg { - (COMPONENT_TREEVIEW, Msg::OnChange(Payload::One(Value::Str(node_id)))) => { - // Update span - let props = label::LabelPropsBuilder::from( - self.view.get_props(COMPONENT_LABEL).unwrap(), - ) - .with_text(format!("Selected: '{}'", node_id)) - .build(); - // Report submit - let msg = self.view.update(COMPONENT_LABEL, props); - self.update(msg) - } - (COMPONENT_TREEVIEW, Msg::OnSubmit(Payload::One(Value::Str(node_id)))) => { - // Update tree - self.extend_dir( - node_id.as_str(), - PathBuf::from(node_id.as_str()).as_path(), - MAX_DEPTH, - ); - // Update - let props = TreeViewPropsBuilder::from( - self.view.get_props(COMPONENT_TREEVIEW).unwrap(), - ) - .with_tree(self.tree.root()) - .with_title(self.path.to_string_lossy(), Alignment::Left) - .build(); - let msg = self.view.update(COMPONENT_TREEVIEW, props); - self.update(msg) - } - (COMPONENT_TREEVIEW, key) if key == &MSG_KEY_BACKSPACE => { - // Update tree - match self.upper_dir() { - None => None, - Some(p) => { - let p: PathBuf = p.to_path_buf(); - self.scan_dir(p.as_path()); - // Update - let props = TreeViewPropsBuilder::from( - self.view.get_props(COMPONENT_TREEVIEW).unwrap(), - ) - .with_tree(self.tree.root()) - .with_title(self.path.to_string_lossy(), Alignment::Left) - .build(); - let msg = self.view.update(COMPONENT_TREEVIEW, props); - self.update(msg) - } - } - } - (COMPONENT_INPUT, Msg::OnSubmit(Payload::One(Value::Str(input)))) => { - let p: PathBuf = PathBuf::from(input.as_str()); - self.scan_dir(p.as_path()); - // Update - let props = TreeViewPropsBuilder::from( - self.view.get_props(COMPONENT_TREEVIEW).unwrap(), - ) - .with_tree(self.tree.root()) - .with_title(self.path.to_string_lossy(), Alignment::Left) - .build(); - let msg = self.view.update(COMPONENT_TREEVIEW, props); - self.update(msg) - } - (COMPONENT_INPUT, key) if key == &MSG_KEY_TAB => { - self.view.active(COMPONENT_TREEVIEW); - None - } - (COMPONENT_TREEVIEW, key) if key == &MSG_KEY_TAB => { - self.view.active(COMPONENT_INPUT); - None - } - (_, key) if key == &MSG_KEY_ESC => { - // Quit on esc - self.quit(); - None +impl Update for Model { + fn update(&mut self, view: &mut View, msg: Option) -> Option { + match msg.unwrap_or(Msg::None) { + Msg::AppClose => { + self.quit = true; + None + } + Msg::ExtendDir(path) => { + self.extend_dir(&path, PathBuf::from(path.as_str()).as_path(), MAX_DEPTH); + self.reload_tree(view); + None + } + Msg::GoTo(path) => { + // Go to and reload tree + self.scan_dir(path.as_path()); + self.reload_tree(view); + None + } + Msg::GoToUpperDir => { + if let Some(parent) = self.upper_dir() { + self.scan_dir(parent.as_path()); + self.reload_tree(view); } - _ => None, - }, + None + } + Msg::FsTreeBlur => { + assert!(view.active(&Id::GoTo).is_ok()); + None + } + Msg::GoToBlur => { + assert!(view.active(&Id::FsTree).is_ok()); + None + } + Msg::None => None, + } + } +} + +// -- components + +#[derive(MockComponent)] +pub struct FsTree { + component: TreeView, +} + +impl FsTree { + pub fn new(tree: Tree, initial_node: Option) -> Self { + // Preserve initial node if exists + let initial_node = match initial_node { + Some(id) if tree.root().query(&id).is_some() => id, + _ => tree.root().id().to_string(), + }; + FsTree { + component: TreeView::default() + .foreground(Color::Reset) + .borders( + Borders::default() + .color(Color::LightYellow) + .modifiers(BorderType::Rounded), + ) + .inactive(Style::default().fg(Color::Gray)) + .indent_size(3) + .scroll_step(6) + .title(tree.root().id(), Alignment::Left) + .highlighted_color(Color::LightYellow) + .highlight_symbol("🦄") + .with_tree(tree) + .initial_node(initial_node), + } + } +} + +impl Component for FsTree { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Custom(TREE_CMD_CLOSE)), + Event::Keyboard(KeyEvent { + code: Key::Right, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Custom(TREE_CMD_OPEN)), + Event::Keyboard(KeyEvent { + code: Key::PageDown, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Scroll(Direction::Down)), + Event::Keyboard(KeyEvent { + code: Key::PageUp, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Scroll(Direction::Up)), + Event::Keyboard(KeyEvent { + code: Key::Down, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Move(Direction::Down)), + Event::Keyboard(KeyEvent { + code: Key::Up, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Move(Direction::Up)), + Event::Keyboard(KeyEvent { + code: Key::Home, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::GoTo(Position::Begin)), + Event::Keyboard(KeyEvent { + code: Key::End, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::GoTo(Position::End)), + Event::Keyboard(KeyEvent { + code: Key::Enter, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Submit), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + modifiers: KeyModifiers::NONE, + }) => return Some(Msg::GoToUpperDir), + Event::Keyboard(KeyEvent { + code: Key::Tab, + modifiers: KeyModifiers::NONE, + }) => return Some(Msg::FsTreeBlur), + _ => return None, + }; + match result { + CmdResult::Submit(State::One(StateValue::String(node))) => Some(Msg::ExtendDir(node)), + _ => Some(Msg::None), + } + } +} + +// -- global listener + +#[derive(Default, MockComponent)] +pub struct GlobalListener { + component: Phantom, +} + +impl Component for GlobalListener { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }) => Some(Msg::AppClose), + _ => None, + } + } +} + +// -- goto input + +#[derive(MockComponent)] +pub struct GoTo { + component: Input, +} + +impl Default for GoTo { + fn default() -> Self { + Self { + component: Input::default() + .foreground(Color::LightBlue) + .borders( + Borders::default() + .color(Color::LightBlue) + .modifiers(BorderType::Rounded), + ) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz", + Style::default().fg(Color::Rgb(120, 120, 120)), + ) + .title("Go to...", Alignment::Left), + } + } +} + +impl Component for GoTo { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Enter, + modifiers: KeyModifiers::NONE, + }) => { + let res = self.perform(Cmd::Submit); + // Clear value + self.attr(Attribute::Value, AttrValue::String(String::new())); + res + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Type(ch)), + Event::Keyboard(KeyEvent { + code: Key::Left, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Move(Direction::Left)), + Event::Keyboard(KeyEvent { + code: Key::Right, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Move(Direction::Right)), + Event::Keyboard(KeyEvent { + code: Key::Home, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::GoTo(Position::Begin)), + Event::Keyboard(KeyEvent { + code: Key::End, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::GoTo(Position::End)), + Event::Keyboard(KeyEvent { + code: Key::Delete, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Cancel), + Event::Keyboard(KeyEvent { + code: Key::Backspace, + modifiers: KeyModifiers::NONE, + }) => self.perform(Cmd::Delete), + Event::Keyboard(KeyEvent { + code: Key::Tab, + modifiers: KeyModifiers::NONE, + }) => return Some(Msg::GoToBlur), + _ => return None, + }; + match result { + CmdResult::Submit(State::One(StateValue::String(path))) => { + Some(Msg::GoTo(PathBuf::from(path.as_str()))) + } + _ => Some(Msg::None), } } } diff --git a/examples/utils/context.rs b/examples/utils/context.rs deleted file mode 100644 index 4c07606..0000000 --- a/examples/utils/context.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! ## Context -//! -//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context - -/** - * MIT License - * - * tui-realm - Copyright (C) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -// Dependencies -extern crate crossterm; -extern crate tuirealm; -use super::input::InputHandler; - -// Includes -use crossterm::event::DisableMouseCapture; -use crossterm::execute; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -}; -use std::io::{stdout, Stdout}; -use tuirealm::tui::backend::CrosstermBackend; -use tuirealm::tui::Terminal; - -/// ## Context -/// -/// Context holds data structures used by the ui -pub struct Context { - pub(crate) input_hnd: InputHandler, - pub(crate) terminal: Terminal>, -} - -impl Context { - /// ### new - /// - /// Instantiates a new Context - pub fn new() -> Context { - let _ = enable_raw_mode(); - // Create terminal - let mut stdout = stdout(); - assert!(execute!(stdout, EnterAlternateScreen).is_ok()); - Context { - input_hnd: InputHandler::new(), - terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(), - } - } - - /// ### enter_alternate_screen - /// - /// Enter alternate screen (gui window) - pub fn enter_alternate_screen(&mut self) { - let _ = execute!( - self.terminal.backend_mut(), - EnterAlternateScreen, - DisableMouseCapture - ); - } - - /// ### leave_alternate_screen - /// - /// Go back to normal screen (gui window) - pub fn leave_alternate_screen(&mut self) { - let _ = execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - ); - } - - /// ### clear_screen - /// - /// Clear terminal screen - pub fn clear_screen(&mut self) { - let _ = self.terminal.clear(); - } -} - -impl Drop for Context { - fn drop(&mut self) { - // Re-enable terminal stuff - self.leave_alternate_screen(); - let _ = disable_raw_mode(); - } -} diff --git a/examples/utils/input.rs b/examples/utils/input.rs deleted file mode 100644 index b763d50..0000000 --- a/examples/utils/input.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! ## Input -//! -//! `input` is the module which provides all the functionalities related to input events in the user interface - -/** - * MIT License - * - * tui-realm - Copyright (C) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -extern crate crossterm; - -use crossterm::event::{poll, read, Event}; -use std::time::Duration; - -/// ## InputHandler -/// -/// InputHandler is the struct which provides an helper to poll for input events -pub(crate) struct InputHandler; - -impl InputHandler { - /// ### InputHandler - /// - /// - pub(crate) fn new() -> InputHandler { - InputHandler {} - } - - /// ### fetch_events - /// - /// Check if new events have been received from handler - #[allow(dead_code)] - pub(crate) fn fetch_events(&self) -> Result, ()> { - let mut inbox: Vec = Vec::new(); - loop { - match self.read_event() { - Ok(ev_opt) => match ev_opt { - Some(ev) => inbox.push(ev), - None => break, - }, - Err(_) => return Err(()), - } - } - Ok(inbox) - } - - /// ### read_event - /// - /// Read event from input listener - pub(crate) fn read_event(&self) -> Result, ()> { - if let Ok(available) = poll(Duration::from_millis(10)) { - match available { - true => { - // Read event - if let Ok(ev) = read() { - Ok(Some(ev)) - } else { - Err(()) - } - } - false => Ok(None), - } - } else { - Err(()) - } - } -} diff --git a/examples/utils/keymap.rs b/examples/utils/keymap.rs deleted file mode 100644 index 580f186..0000000 --- a/examples/utils/keymap.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! ## Keymap -//! -//! Keymap contains pub constants which can be used in the `update` function to match messages - -/** - * MIT License - * - * tui-realm - Copyright (C) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -extern crate crossterm; -extern crate tuirealm; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use tuirealm::Msg; - -// -- keys - -pub const MSG_KEY_ESC: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Esc, - modifiers: KeyModifiers::NONE, -}); - -pub const MSG_KEY_TAB: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, -}); - -pub const MSG_KEY_BACKSPACE: Msg = Msg::OnKey(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE, -}); diff --git a/examples/utils/mod.rs b/examples/utils/mod.rs deleted file mode 100644 index 9ee8d5c..0000000 --- a/examples/utils/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! ## Utils -//! -//! `Utils` provides structures useful to implement gui with tui-rs - -/** - * MIT License - * - * tui-realm - Copyright (C) 2021 Christian Visintin - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -pub mod context; -mod input; -pub mod keymap; diff --git a/src/lib.rs b/src/lib.rs index 83aa9ff..b5249ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,6 +173,25 @@ impl TreeView { self } + /// ### highlight_symbol + /// + /// Set symbol to prepend to highlighted node + pub fn highlight_symbol>(mut self, symbol: S) -> Self { + self.attr( + Attribute::HighlightedStr, + AttrValue::String(symbol.as_ref().to_string()), + ); + self + } + + /// ### highlighted_color + /// + /// Set color to apply to highlighted item + pub fn highlighted_color(mut self, color: Color) -> Self { + self.attr(Attribute::HighlightedColor, AttrValue::Color(color)); + self + } + /// ### initial_node /// /// Set initial node for tree state. @@ -340,10 +359,11 @@ impl MockComponent for TreeView { .props .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground)) .unwrap_color(); - let (hg_fg, hg_bg): (Color, Color) = match focus { - true => (background, hg_color), - false => (hg_color, background), - }; + let hg_style = match focus { + true => Style::default().bg(hg_color).fg(Color::Black), + false => Style::default().fg(hg_color), + } + .add_modifier(modifiers); let hg_str = self .props .get(Attribute::HighlightedStr) @@ -352,7 +372,7 @@ impl MockComponent for TreeView { // Make widget let mut tree = TreeWidget::new(self.tree()) .block(div) - .highlight_style(Style::default().fg(hg_fg).bg(hg_bg).add_modifier(modifiers)) + .highlight_style(hg_style) .indent_size(indent_size.into()) .style( Style::default() diff --git a/src/widget.rs b/src/widget.rs index 63984b0..b605128 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -33,6 +33,7 @@ use tuirealm::tui::{ style::Style, widgets::{Block, StatefulWidget, Widget}, }; +use unicode_width::UnicodeWidthStr; /// ## TreeWidget /// @@ -143,8 +144,8 @@ impl<'a> StatefulWidget for TreeWidget<'a> { } // Recurse render let mut render = Render { - depth: 0, - skip_rows: Self::calc_rows_to_skip(self.tree.root(), state, area.height), + depth: 1, + skip_rows: self.calc_rows_to_skip(state, area.height), }; self.iter_nodes(self.tree.root(), area, buf, state, &mut render); } @@ -154,13 +155,13 @@ impl<'a> TreeWidget<'a> { fn iter_nodes( &self, node: &Node, - area: Rect, + mut area: Rect, buf: &mut Buffer, state: &TreeState, render: &mut Render, - ) { + ) -> Rect { // Render self - let mut area = self.render_node(node, area, buf, state, render); + area = self.render_node(node, area, buf, state, render); // Render children if node is open if state.is_open(node) { // Increment depth @@ -169,11 +170,12 @@ impl<'a> TreeWidget<'a> { if area.height == 0 { break; } - area = self.render_node(child, area, buf, state, render); + area = self.iter_nodes(child, area, buf, state, render); } // Decrement depth render.depth -= 1; } + area } fn render_node( @@ -200,31 +202,34 @@ impl<'a> TreeWidget<'a> { width: area.width, height: 1, }; - // Apply style - match state.is_selected(node) { - false => buf.set_style(node_area, self.style), - true => buf.set_style(node_area, self.highlight_style), + // Get style to use + let style = match state.is_selected(node) { + false => self.style, + true => self.highlight_style, }; + // Apply style + buf.set_style(node_area, style); // Calc depth for node (is selected?) - let depth = match state.is_selected(node) { - true => render.depth - 1, - false => render.depth, - } * self.indent_size; + let indent_size = render.depth * self.indent_size; + let indent_size = match state.is_selected(node) { + true if highlight_symbol.is_some() => { + indent_size.saturating_sub(highlight_symbol.as_deref().unwrap().width() + 1) + } + _ => indent_size, + }; let width: usize = area.width as usize; - // Write depth - let (start_x, start_y) = - buf.set_stringn(area.x, area.y, " ".repeat(depth), width - depth, self.style); + // Write indentation + let (start_x, start_y) = buf.set_stringn( + area.x, + area.y, + " ".repeat(indent_size), + width - indent_size, + style, + ); // Write highlight symbol let (start_x, start_y) = highlight_symbol - .map(|x| { - buf.set_stringn( - start_x, - start_y, - x, - width - start_x as usize, - self.highlight_style, - ) - }) + .map(|x| buf.set_stringn(start_x, start_y, x, width - start_x as usize, style)) + .map(|(x, y)| buf.set_stringn(x, y, " ", width - start_x as usize, style)) .unwrap_or((start_x, start_y)); // Write node name let (start_x, start_y) = buf.set_stringn( @@ -232,25 +237,25 @@ impl<'a> TreeWidget<'a> { start_y, node.value(), width - start_x as usize, - self.style, + style, ); // Write arrow based on node let write_after = if state.is_open(node) { // Is open - "\u{25bc}" // Arrow down + " \u{25bc}" // Arrow down } else if node.is_leaf() { // Is leaf (has no children) - " " + " " } else { // Has children, but is closed - "\u{25b6}" // Arrow to right + " \u{25b6}" // Arrow to right }; let _ = buf.set_stringn( start_x, start_y, write_after, width - start_x as usize, - self.style, + style, ); // Return new area Rect { @@ -264,7 +269,7 @@ impl<'a> TreeWidget<'a> { /// ### calc_rows_to__skip /// /// Calculate rows to skip before starting rendering the current tree - fn calc_rows_to_skip(node: &Node, state: &TreeState, height: u16) -> usize { + fn calc_rows_to_skip(&self, state: &TreeState, height: u16) -> usize { // if no node is selected, return 0 let selected = match state.selected() { Some(s) => s, @@ -306,9 +311,9 @@ impl<'a> TreeWidget<'a> { } // Return the result of recursive call; // if the result is less than area height, then return 0; otherwise subtract the height to result - match calc_rows_to_skip_r(node, state, height, selected, 0).0 { + match calc_rows_to_skip_r(self.tree.root(), state, height, selected, 0).0 { x if x < (height as usize) => 0, - x => x - (height as usize), + x => x - (height as usize) + 1, } } } @@ -359,9 +364,12 @@ mod test { // Select aA2 let aa2 = tree.root().query(&String::from("aA2")).unwrap(); state.select(tree.root(), aa2); - // Get rows to skip - assert_eq!(TreeWidget::calc_rows_to_skip(tree.root(), &state, 8), 0); // Before end - assert_eq!(TreeWidget::calc_rows_to_skip(tree.root(), &state, 6), 0); // At end + // Get rows to skip (no block) + let widget = TreeWidget::new(&tree); + // Before end + assert_eq!(widget.calc_rows_to_skip(&state, 8), 0); + // At end + assert_eq!(widget.calc_rows_to_skip(&state, 6), 0); } #[test] @@ -373,7 +381,9 @@ mod test { // Select bB2 let bb2 = tree.root().query(&String::from("bB2")).unwrap(); state.select(tree.root(), bb2); - // Get rows to skip - assert_eq!(TreeWidget::calc_rows_to_skip(tree.root(), &state, 8), 12); // 20th element - height (12) + // Get rows to skip (no block) + let widget = TreeWidget::new(&tree); + // 20th element - height (12) + 1 + assert_eq!(widget.calc_rows_to_skip(&state, 8), 13); } }