From 59b55ee192d9742b72e053f615680f765669194e Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Sat, 8 May 2021 08:54:20 +0530 Subject: [PATCH] Add more UI styling options With this commit, the following can be done: - Define layout constraints based on screen size and relative panel size. - Define borders. - Define panel style. - Define panel title and title style. --- src/config.rs | 145 ++++++++++++++++++--------- src/config.yml | 51 +++++++--- src/ui.rs | 260 +++++++++++++++++++++++++++++++++---------------- 3 files changed, 311 insertions(+), 145 deletions(-) diff --git a/src/config.rs b/src/config.rs index dd25f49..f36ded7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::collections::HashMap; use tui::layout::Constraint as TuiConstraint; +use tui::layout::Rect; +use tui::widgets::Borders as TuiBorders; #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -254,24 +256,50 @@ pub enum Constraint { Percentage(u16), Ratio(u32, u32), Length(u16), + LengthLessThanScreenHeight(u16), + LengthLessThanScreenWidth(u16), + LengthLessThanLayoutHeight(u16), + LengthLessThanLayoutWidth(u16), Max(u16), + MaxLessThanScreenHeight(u16), + MaxLessThanScreenWidth(u16), + MaxLessThanLayoutHeight(u16), + MaxthLessThanLayoutWidth(u16), Min(u16), + MinLessThanScreenHeight(u16), + MinLessThanScreenWidth(u16), + MinLessThanLayoutHeight(u16), + MinLessThanLayoutWidth(u16), } -impl Default for Constraint { - fn default() -> Self { - Self::Min(1) - } -} - -impl Into for Constraint { - fn into(self) -> TuiConstraint { +impl Constraint { + pub fn to_tui(self, screen_size: Rect, layout_size: Rect) -> TuiConstraint { match self { - Self::Length(n) => TuiConstraint::Length(n), Self::Percentage(n) => TuiConstraint::Percentage(n), Self::Ratio(x, y) => TuiConstraint::Ratio(x, y), + Self::Length(n) => TuiConstraint::Length(n), + Self::LengthLessThanScreenHeight(n) => { + TuiConstraint::Length(screen_size.height.max(n) - n) + } + Self::LengthLessThanScreenWidth(n) => { + TuiConstraint::Length(screen_size.width.max(n) - n) + } + Self::LengthLessThanLayoutHeight(n) => { + TuiConstraint::Length(layout_size.height.max(n) - n) + } + Self::LengthLessThanLayoutWidth(n) => { + TuiConstraint::Length(layout_size.width.max(n) - n) + } Self::Max(n) => TuiConstraint::Max(n), + Self::MaxLessThanScreenHeight(n) => TuiConstraint::Max(screen_size.height.max(n) - n), + Self::MaxLessThanScreenWidth(n) => TuiConstraint::Max(screen_size.width.max(n) - n), + Self::MaxLessThanLayoutHeight(n) => TuiConstraint::Max(layout_size.height.max(n) - n), + Self::MaxthLessThanLayoutWidth(n) => TuiConstraint::Max(layout_size.width.max(n) - n), Self::Min(n) => TuiConstraint::Min(n), + Self::MinLessThanScreenHeight(n) => TuiConstraint::Min(screen_size.height.max(n) - n), + Self::MinLessThanScreenWidth(n) => TuiConstraint::Min(screen_size.width.max(n) - n), + Self::MinLessThanLayoutHeight(n) => TuiConstraint::Min(layout_size.height.max(n) - n), + Self::MinLessThanLayoutWidth(n) => TuiConstraint::Min(layout_size.width.max(n) - n), } } } @@ -999,6 +1027,26 @@ impl ModesConfig { } } +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Border { + Top, + Right, + Bottom, + Left, +} + +impl Border { + pub fn bits(self) -> u32 { + match self { + Self::Top => TuiBorders::TOP.bits(), + Self::Right => TuiBorders::RIGHT.bits(), + Self::Bottom => TuiBorders::BOTTOM.bits(), + Self::Left => TuiBorders::LEFT.bits(), + } + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LayoutOptions { @@ -1009,7 +1057,7 @@ pub struct LayoutOptions { #[serde(default)] vertical_margin: Option, #[serde(default)] - constraints: Vec, + constraints: Option>, } impl LayoutOptions { @@ -1017,12 +1065,12 @@ impl LayoutOptions { self.margin = other.margin.or(self.margin); self.horizontal_margin = other.horizontal_margin.or(self.horizontal_margin); self.vertical_margin = other.vertical_margin.or(self.vertical_margin); - self.constraints = other.constraints; + self.constraints = other.constraints.or(self.constraints); self } /// Get a reference to the layout options's constraints. - pub fn constraints(&self) -> &Vec { + pub fn constraints(&self) -> &Option> { &self.constraints } @@ -1042,15 +1090,45 @@ impl LayoutOptions { } } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BlockConfig { + #[serde(default)] + title: UiElement, + + #[serde(default)] + borders: Option>, + + #[serde(default)] + style: Style, +} + +impl BlockConfig { + /// Get a reference to the block config's borders. + pub fn borders(&self) -> &Option> { + &self.borders + } + + /// Get a reference to the block config's title. + pub fn title(&self) -> &UiElement { + &self.title + } + + /// Get a reference to the block config's style. + pub fn style(&self) -> Style { + self.style + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub enum Layout { - Nothing, - Table, - InputAndLogs, - Selection, - HelpMenu, - SortAndFilter, + Nothing(BlockConfig), + Table(BlockConfig), + InputAndLogs(BlockConfig), + Selection(BlockConfig), + HelpMenu(BlockConfig), + SortAndFilter(BlockConfig), Horizontal { config: LayoutOptions, splits: Vec, @@ -1063,41 +1141,14 @@ pub enum Layout { impl Default for Layout { fn default() -> Self { - Self::Nothing + Self::Nothing(Default::default()) } } impl Layout { pub fn extend(self, other: Self) -> Self { match (self, other) { - ( - Self::Horizontal { - config: sc, - splits: _, - }, - Self::Horizontal { - config: oc, - splits: os, - }, - ) => Self::Horizontal { - config: sc.extend(oc), - splits: os, - }, - - ( - Self::Vertical { - config: sc, - splits: _, - }, - Self::Vertical { - config: oc, - splits: os, - }, - ) => Self::Vertical { - config: sc.extend(oc), - splits: os, - }, - (s, Self::Nothing) => s, + (s, Self::Nothing(_)) => s, (_, other) => other, } } diff --git a/src/config.yml b/src/config.yml index ff94511..5c58c3e 100644 --- a/src/config.yml +++ b/src/config.yml @@ -19,17 +19,25 @@ layouts: - Min: 1 - Length: 3 splits: - - SortAndFilter - - Table - - InputAndLogs + - SortAndFilter: + borders: [Top, Right, Bottom, Left] + - Table: + title: + style: + fg: Red + borders: [Top, Right, Bottom, Left] + - InputAndLogs: + borders: [Top, Right, Bottom, Left] - Vertical: config: constraints: - Percentage: 50 - Percentage: 50 splits: - - Selection - - HelpMenu + - Selection: + borders: [Top, Right, Bottom, Left] + - HelpMenu: + borders: [Top, Right, Bottom, Left] no_help: Horizontal: config: @@ -44,10 +52,14 @@ layouts: - Min: 1 - Length: 3 splits: - - SortAndFilter - - Table - - InputAndLogs - - Selection + - SortAndFilter: + borders: [Top, Right, Bottom, Left] + - Table: + borders: [Top, Right, Bottom, Left] + - InputAndLogs: + borders: [Top, Right, Bottom, Left] + - Selection: + borders: [Top, Right, Bottom, Left] no_selection: Horizontal: @@ -63,10 +75,14 @@ layouts: - Min: 1 - Length: 3 splits: - - SortAndFilter - - Table - - InputAndLogs - - HelpMenu + - SortAndFilter: + borders: [Top, Right, Bottom, Left] + - Table: + borders: [Top, Right, Bottom, Left] + - InputAndLogs: + borders: [Top, Right, Bottom, Left] + - HelpMenu: + borders: [Top, Right, Bottom, Left] no_help_no_selection: Vertical: @@ -76,9 +92,12 @@ layouts: - Min: 1 - Length: 3 splits: - - SortAndFilter - - Table - - InputAndLogs + - SortAndFilter: + borders: [Top, Right, Bottom, Left] + - Table: + borders: [Top, Right, Bottom, Left] + - InputAndLogs: + borders: [Top, Right, Bottom, Left] general: show_hidden: false diff --git a/src/ui.rs b/src/ui.rs index c95574e..fe399c7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,7 @@ use crate::app; use crate::app::HelpMenuLine; use crate::app::{Node, ResolvedNode}; +use crate::config::BlockConfig; use crate::config::Layout; use handlebars::Handlebars; use lazy_static::lazy_static; @@ -13,7 +14,7 @@ use tui::layout::Rect; use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout}; use tui::style::{Color, Modifier, Style as TuiStyle}; use tui::text::{Span, Spans}; -use tui::widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table}; +use tui::widgets::{Block, Borders as TuiBorders, Cell, List, ListItem, Paragraph, Row, Table}; use tui::Frame; lazy_static! { @@ -168,10 +169,36 @@ impl NodeUiMetadata { } } -fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Handlebars) { - let config = app.config().to_owned(); - let header_height = config.general().table().header().height().unwrap_or(1); - let height: usize = (rect.height.max(header_height + 2) - (header_height + 2)).into(); +fn block<'a>(config: BlockConfig, default_title: String) -> Block<'a> { + Block::default() + .borders(TuiBorders::from_bits_truncate( + config + .borders() + .clone() + .unwrap_or_default() + .iter() + .map(|b| b.bits()) + .reduce(|a, b| (a ^ b)) + .unwrap_or_else(|| TuiBorders::NONE.bits()), + )) + .title(Span::styled( + config.title().format().clone().unwrap_or(default_title), + config.title().style().into(), + )) + .style(config.style().into()) +} + +fn draw_table( + config: BlockConfig, + f: &mut Frame, + screen_size: Rect, + layout_size: Rect, + app: &app::App, + hb: &Handlebars, +) { + let app_config = app.config().to_owned(); + let header_height = app_config.general().table().header().height().unwrap_or(1); + let height: usize = (layout_size.height.max(header_height + 2) - (header_height + 2)).into(); let rows = app .directory_buffer() @@ -190,7 +217,7 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han let is_first = index == 0; let is_last = index == dir.total().max(1) - 1; - let tree = config + let tree = app_config .general() .table() .tree() @@ -206,19 +233,24 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han }) .unwrap_or_default(); - let node_type = config + let node_type = app_config .node_types() .special() .get(node.relative_path()) - .or_else(|| config.node_types().extension().get(node.extension())) - .or_else(|| config.node_types().mime_essence().get(node.mime_essence())) + .or_else(|| app_config.node_types().extension().get(node.extension())) + .or_else(|| { + app_config + .node_types() + .mime_essence() + .get(node.mime_essence()) + }) .unwrap_or_else(|| { if node.is_symlink() { - &config.node_types().symlink() + &app_config.node_types().symlink() } else if node.is_dir() { - &config.node_types().directory() + &app_config.node_types().directory() } else { - &config.node_types().file() + &app_config.node_types().file() } }); @@ -230,7 +262,7 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han }; let (mut prefix, mut suffix, mut style) = { - let ui = config.general().default_ui().clone(); + let ui = app_config.general().default_ui().clone(); ( ui.prefix().clone(), ui.suffix().clone(), @@ -239,14 +271,14 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han }; if is_selected { - let ui = config.general().selection_ui().clone(); + let ui = app_config.general().selection_ui().clone(); prefix = ui.prefix().clone().or(prefix); suffix = ui.suffix().clone().or(suffix); style = style.extend(ui.style()); }; if is_focused { - let ui = config.general().focus_ui().clone(); + let ui = app_config.general().focus_ui().clone(); prefix = ui.prefix().clone().or(prefix); suffix = ui.suffix().clone().or(suffix); style = style.extend(ui.style()); @@ -281,30 +313,41 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han }) .unwrap_or_default(); - let table_constraints: Vec = config + let table_constraints: Vec = app_config .general() .table() .col_widths() .clone() .unwrap_or_default() .into_iter() - .map(|c| c.into()) + .map(|c| c.to_tui(screen_size, layout_size)) .collect(); let table = Table::new(rows) .widths(&table_constraints) - .style(config.general().table().style().into()) - .highlight_style(config.general().focus_ui().style().into()) - .column_spacing(config.general().table().col_spacing().unwrap_or_default()) - .block(Block::default().borders(Borders::ALL).title(format!( - " {} ({}) ", - app.pwd(), - app.directory_buffer().map(|d| d.total()).unwrap_or_default() - ))); + .style(app_config.general().table().style().into()) + .highlight_style(app_config.general().focus_ui().style().into()) + .column_spacing( + app_config + .general() + .table() + .col_spacing() + .unwrap_or_default(), + ) + .block(block( + config, + format!( + " {} ({}) ", + app.pwd(), + app.directory_buffer() + .map(|d| d.total()) + .unwrap_or_default() + ), + )); let table = table.clone().header( Row::new( - config + app_config .general() .table() .header() @@ -316,18 +359,25 @@ fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Han .collect::>(), ) .height(header_height) - .style(config.general().table().header().style().into()), + .style(app_config.general().table().header().style().into()), ); - f.render_widget(table, rect); + f.render_widget(table, layout_size); } -fn draw_selection(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { +fn draw_selection( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + app: &app::App, + _: &Handlebars, +) { let selection: Vec = app .selection() .iter() .rev() - .take((rect.height.max(2) - 2).into()) + .take((layout_size.height.max(2) - 2).into()) .rev() .map(|n| n.absolute_path().clone()) .map(ListItem::new) @@ -336,16 +386,20 @@ fn draw_selection(f: &mut Frame, rect: Rect, app: &app::App, _: & let selection_count = selection.len(); // Selected items - let selection_list = List::new(selection).block( - Block::default() - .borders(Borders::ALL) - .title(format!(" Selection ({}) ", selection_count)), - ); + let selection_list = + List::new(selection).block(block(config, format!(" Selection ({}) ", selection_count))); - f.render_widget(selection_list, rect); + f.render_widget(selection_list, layout_size); } -fn draw_help_menu(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { +fn draw_help_menu( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + app: &app::App, + _: &Handlebars, +) { let help_menu_rows = app .mode() .help_menu() @@ -374,20 +428,26 @@ fn draw_help_menu(f: &mut Frame, rect: Rect, app: &app::App, _: & }; let help_menu = Table::new(help_menu_rows) - .block(Block::default().borders(Borders::ALL).title(format!( - " Help [{}{}] ", - &app.mode().name(), - read_only_indicator - ))) + .block(block( + config, + format!(" Help [{}{}] ", &app.mode().name(), read_only_indicator), + )) .widths(&[ TuiConstraint::Percentage(20), TuiConstraint::Percentage(20), TuiConstraint::Percentage(60), ]); - f.render_widget(help_menu, rect); + f.render_widget(help_menu, layout_size); } -fn draw_input_buffer(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { +fn draw_input_buffer( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + app: &app::App, + _: &Handlebars, +) { let input_buf = Paragraph::new(Spans::from(vec![ Span::styled( app.config() @@ -409,11 +469,18 @@ fn draw_input_buffer(f: &mut Frame, rect: Rect, app: &app::App, _ app.config().general().cursor().style().into(), ), ])) - .block(Block::default().borders(Borders::ALL).title(" Input ")); - f.render_widget(input_buf, rect); + .block(block(config, " Input ".into())); + f.render_widget(input_buf, layout_size); } -fn draw_sort_n_filter(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { +fn draw_sort_n_filter( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + app: &app::App, + _: &Handlebars, +) { let ui = app.config().general().sort_and_filter_ui().clone(); let filter_by = app.explorer_config().filters(); let sort_by = app.explorer_config().sorters(); @@ -474,15 +541,23 @@ fn draw_sort_n_filter(f: &mut Frame, rect: Rect, app: &app::App, .collect::>(); spans.pop(); - let p = Paragraph::new(Spans::from(spans)).block(Block::default().borders(Borders::ALL).title( + let p = Paragraph::new(Spans::from(spans)).block(block( + config, format!(" Sort & filter ({}) ", filter_by.len() + sort_by.len()), )); - f.render_widget(p, rect); + f.render_widget(p, layout_size); } -fn draw_logs(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { - let config = app.config().general().logs().clone(); +fn draw_logs( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + app: &app::App, + _: &Handlebars, +) { + let logs_config = app.config().general().logs().clone(); let logs = app .logs() .iter() @@ -495,55 +570,70 @@ fn draw_logs(f: &mut Frame, rect: Rect, app: &app::App, _: &Handl app::LogLevel::Info => ListItem::new(format!( "{} | {} | {}", &time, - &config.info().format().to_owned().unwrap_or_default(), + &logs_config.info().format().to_owned().unwrap_or_default(), l.message() )) - .style(config.info().style().into()), + .style(logs_config.info().style().into()), app::LogLevel::Success => ListItem::new(format!( "{} | {} | {}", &time, - &config.success().format().to_owned().unwrap_or_default(), + &logs_config + .success() + .format() + .to_owned() + .unwrap_or_default(), l.message() )) - .style(config.success().style().into()), + .style(logs_config.success().style().into()), app::LogLevel::Error => ListItem::new(format!( "{} | {} | {}", &time, - &config.error().format().to_owned().unwrap_or_default(), + &logs_config.error().format().to_owned().unwrap_or_default(), l.message() )) - .style(config.error().style().into()), + .style(logs_config.error().style().into()), } }) .collect::>(); - let logs_list = List::new(logs).block( - Block::default() - .borders(Borders::ALL) - .title(format!(" Logs ({}) ", app.logs().len())), - ); + let logs_list = List::new(logs).block(block(config, format!(" Logs ({}) ", app.logs().len()))); - f.render_widget(logs_list, rect); + f.render_widget(logs_list, layout_size); +} + +pub fn draw_nothing( + config: BlockConfig, + f: &mut Frame, + _screen_size: Rect, + layout_size: Rect, + _app: &app::App, + _hb: &Handlebars, +) { + let nothing = Paragraph::new("").block(block(config, "".into())); + f.render_widget(nothing, layout_size); } pub fn draw_layout( layout: Layout, f: &mut Frame, - rect: Rect, + screen_size: Rect, + layout_size: Rect, app: &app::App, hb: &Handlebars, ) { match layout { - Layout::Nothing => {} - Layout::Table => draw_table(f, rect, app, hb), - Layout::SortAndFilter => draw_sort_n_filter(f, rect, app, hb), - Layout::HelpMenu => draw_help_menu(f, rect, app, hb), - Layout::Selection => draw_selection(f, rect, app, hb), - Layout::InputAndLogs => { + Layout::Nothing(config) => draw_nothing(config, f, screen_size, layout_size, app, hb), + Layout::Table(config) => draw_table(config, f, screen_size, layout_size, app, hb), + Layout::SortAndFilter(config) => { + draw_sort_n_filter(config, f, screen_size, layout_size, app, hb) + } + Layout::HelpMenu(config) => draw_help_menu(config, f, screen_size, layout_size, app, hb), + Layout::Selection(config) => draw_selection(config, f, screen_size, layout_size, app, hb), + Layout::InputAndLogs(config) => { if app.input_buffer().is_some() { - draw_input_buffer(f, rect, app, hb); + draw_input_buffer(config, f, screen_size, layout_size, app, hb); } else { - draw_logs(f, rect, app, hb); + draw_logs(config, f, screen_size, layout_size, app, hb); }; } Layout::Horizontal { config, splits } => { @@ -552,8 +642,10 @@ pub fn draw_layout( .constraints( config .constraints() + .clone() + .unwrap_or_default() .iter() - .map(|c| (*c).into()) + .map(|c| c.to_tui(screen_size, layout_size)) .collect::>(), ) .horizontal_margin( @@ -568,11 +660,12 @@ pub fn draw_layout( .or_else(|| config.margin()) .unwrap_or_default(), ) - .split(rect); + .split(layout_size); + splits .into_iter() - .enumerate() - .for_each(|(i, s)| draw_layout(s, f, chunks[i], app, hb)); + .zip(chunks.into_iter()) + .for_each(|(split, chunk)| draw_layout(split, f, screen_size, chunk, app, hb)); } Layout::Vertical { config, splits } => { @@ -581,8 +674,10 @@ pub fn draw_layout( .constraints( config .constraints() + .clone() + .unwrap_or_default() .iter() - .map(|c| (*c).into()) + .map(|c| c.to_tui(screen_size, layout_size)) .collect::>(), ) .horizontal_margin( @@ -597,20 +692,21 @@ pub fn draw_layout( .or_else(|| config.margin()) .unwrap_or_default(), ) - .split(rect); + .split(layout_size); + splits .into_iter() - .enumerate() - .for_each(|(i, s)| draw_layout(s, f, chunks[i], app, hb)); + .zip(chunks.into_iter()) + .for_each(|(split, chunk)| draw_layout(split, f, screen_size, chunk, app, hb)); } } } pub fn draw(f: &mut Frame, app: &app::App, hb: &Handlebars) { - let rect = f.size(); + let screen_size = f.size(); let layout = app.layout().clone(); - draw_layout(layout, f, rect, app, hb); + draw_layout(layout, f, screen_size, screen_size, app, hb); } #[cfg(test)]