Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui/ux): Show navigation help when h is pressed #42

Merged
merged 5 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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