Skip to content

Commit

Permalink
feat(ui/ux): Show navigation help when h is pressed (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole committed Sep 6, 2024
1 parent a51af74 commit 35141da
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 102 deletions.
8 changes: 8 additions & 0 deletions app/src/screen/asset/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ pub enum InputEvent {
#[description = "Quit application"]
Quit,

#[key = 'h']
#[description = "Open/close navigation help"]
NavigationHelp,

#[key = 'b']
#[description = "Return one screen back"]
Back,
Expand Down Expand Up @@ -60,6 +64,10 @@ pub(super) fn process_input<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonito

match event {
InputEvent::Quit => Some(OutgoingMessage::Exit),
InputEvent::NavigationHelp => {
model.show_navigation_help ^= true;
None
}
InputEvent::Back => Some(OutgoingMessage::Back),
InputEvent::OpenDepositScreen => Some(OutgoingMessage::SwitchScreen(ScreenName::Deposit)),
InputEvent::SelectTimeInterval(event) => {
Expand Down
2 changes: 2 additions & 0 deletions app/src/screen/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct Model<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT> {
coin_price_history: Option<Vec<PriceHistoryPoint>>,
transactions: Option<Vec<(TransactionUid, TransactionInfo)>>,
selected_time_period: TimePeriod,
show_navigation_help: bool,

state: StateRegistry,
apis: ApiRegistry<L, C, M>,
Expand Down Expand Up @@ -98,6 +99,7 @@ impl<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApiT> ScreenT<L, C,
coin_price_history: Default::default(),
transactions: Default::default(),
selected_time_period: DEFAULT_SELECTED_TIME_PERIOD,
show_navigation_help: false,

state,
apis: api_registry,
Expand Down
10 changes: 8 additions & 2 deletions app/src/screen/asset/view.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use input_mapping_common::InputMappingT;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Style, Stylize},
symbols,
symbols::{self},
text::{Line, Span, Text},
widgets::{
Axis, Block, Borders, Chart, Dataset, GraphType, HighlightSpacing, Padding, Row, Table,
Expand All @@ -21,7 +22,7 @@ use crate::{
ledger::LedgerApiT,
},
screen::{
common::{format_address, network_symbol, render_centered_text, BackgroundWidget},
common::{self, format_address, network_symbol, render_centered_text, BackgroundWidget},
resources::Resources,
},
};
Expand Down Expand Up @@ -103,6 +104,11 @@ pub(super) fn render<L: LedgerApiT, C: CoinPriceApiT, M: BlockchainMonitoringApi
render_tx_list_placeholder(frame, inner_txs_list_area);
}
}

if model.show_navigation_help {
let mapping = super::controller::InputEvent::get_mapping();
common::render_navigation_help(mapping, frame, resources);
}
}

fn render_price_chart(
Expand Down
91 changes: 0 additions & 91 deletions app/src/screen/common.rs

This file was deleted.

29 changes: 29 additions & 0 deletions app/src/screen/common/background_widget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Stylize},
text::Line,
widgets::Widget,
};

pub struct BackgroundWidget {
color: Color,
}

impl BackgroundWidget {
pub fn new(color: Color) -> Self {
Self { color }
}
}

impl Widget for BackgroundWidget {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let line = Line::raw(" ".repeat(area.width as usize)).bg(self.color);
for y in area.y..area.y + area.height {
buf.set_line(area.x, y, &line, area.width);
}
}
}
127 changes: 127 additions & 0 deletions app/src/screen/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use input_mapping_common::InputMapping;
use ratatui::{
layout::{Alignment, Constraint, Flex, Layout, Margin, Rect},
style::Stylize,
text::Text,
widgets::{Block, BorderType, Borders, Padding},
Frame,
};

use crate::api::common_types::Network;

mod background_widget;
pub use background_widget::*;

mod navigation_help_widget;
pub use navigation_help_widget::*;

use super::resources::Resources;

pub fn network_symbol(network: Network) -> String {
match network {
Network::Bitcoin => "₿",
Network::Ethereum => "⟠",
}
.to_string()
}

pub fn render_centered_text(frame: &mut Frame, area: Rect, text: Text) {
let [area] = Layout::horizontal([Constraint::Length(text.width() as u16)])
.flex(Flex::Center)
.areas(area);
let [area] = Layout::vertical([Constraint::Length(text.height() as u16)])
.flex(Flex::Center)
.areas(area);

frame.render_widget(text, area);
}

pub fn format_address(address: &str, max_symbols: usize) -> String {
if max_symbols <= 3 {
return "".to_string();
}

if max_symbols <= 8 {
return "...".to_string();
}

let part_size = (max_symbols - 3) / 2;
let part_size = part_size.min(8);

if address.len() <= part_size * 2 {
return address.to_string();
}

format!(
"{}...{}",
&address[..part_size],
&address[(address.len() - part_size)..]
)
}

pub fn render_navigation_help(
input_mapping: InputMapping,
frame: &mut Frame<'_>,
resources: &Resources,
) {
let area = frame.size();

let bindings = input_mapping
.mapping
.into_iter()
.map(|map| (map.key, map.description))
.collect();

let widget = NavigationHelpWidget::new(bindings);

let block_area = area.inner(Margin::new(8, 4));

let width = widget.min_width().max(block_area.width as usize / 2);
let height = widget.height();

let block = Block::new()
.border_type(BorderType::Double)
.borders(Borders::all())
.border_style(resources.main_color)
.padding(Padding::proportional(1))
.title("Help")
.title_alignment(Alignment::Center)
.reset()
.bg(resources.background_color)
.fg(resources.main_color);

let block_inner = block.inner(block_area);

let [widget_area] = Layout::horizontal([Constraint::Length(width as u16)])
.flex(Flex::Center)
.areas(block_inner);
let [widget_area] = Layout::vertical([Constraint::Length(height as u16)])
.flex(Flex::Center)
.areas(widget_area);

frame.render_widget(
BackgroundWidget::new(resources.background_color),
block_area,
);
frame.render_widget(block, block_area);

frame.render_widget(widget, widget_area);
}

#[cfg(test)]
mod tests {
use itertools::Itertools;

use super::format_address;

#[test]
fn test_format_address() {
let address_lengths = [0, 2, 8, 10, 100].into_iter();
let max_lengths = [0, 3, 5, 6, 8, 10, 100].into_iter();

for (addr_len, max_len) in address_lengths.cartesian_product(max_lengths) {
let address = "0".repeat(addr_len);
assert!(format_address(&address, max_len).len() <= max_len);
}
}
}
73 changes: 73 additions & 0 deletions app/src/screen/common/navigation_help_widget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use ratatui::{
buffer::Buffer,
crossterm::event::KeyCode,
layout::{Alignment, Rect},
text::Text,
widgets::Widget,
};

pub struct NavigationHelpWidget {
key_bindings: Vec<(KeyCode, String)>,
}

impl NavigationHelpWidget {
pub fn new(key_bindings: Vec<(KeyCode, String)>) -> Self {
Self { key_bindings }
}

pub fn height(&self) -> usize {
self.key_bindings.len()
}

pub fn min_width(&self) -> usize {
self.key_bindings
.iter()
.map(|(key, description)| min_line_length(description, &key_name(key)))
.max()
.unwrap_or_default()
}
}

impl Widget for NavigationHelpWidget {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let width = area.width as usize;

let text: String = self
.key_bindings
.iter()
.map(|(key, description)| {
let key_name = key_name(key);
let line_len = min_line_length(description, &key_name);

let padding = width - line_len.min(width);
let padding_str: String = vec![".".to_string(); padding + 1].into_iter().collect();

format!("[{}]{}{}", key_name, padding_str, description)
})
.intersperse("\n".to_string())
.collect();

let text = Text::raw(text).alignment(Alignment::Center);
text.render(area, buf);
}
}

fn key_name(key: &KeyCode) -> String {
match key {
KeyCode::Char(ch) => ch.to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Enter => "⏎".to_string(),
_ => unimplemented!(),
}
}

fn min_line_length(description: &str, key_name: &str) -> usize {
const BRACKETS_LEN: usize = 2;
const MINIMAL_SPACING: usize = 1;

Text::raw(description).width() + Text::raw(key_name).width() + BRACKETS_LEN + MINIMAL_SPACING
}
Loading

0 comments on commit 35141da

Please sign in to comment.