From b8da79cfcff0767821cd8832224dbf78b2857776 Mon Sep 17 00:00:00 2001 From: mertwole <33563701+mertwole@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:23:44 +0700 Subject: [PATCH] feat(ui): Dynamic theme selection (#39) --- app/src/app.rs | 8 +++- app/src/screen/asset/mod.rs | 6 +-- app/src/screen/asset/view.rs | 60 ++++++++++++++++--------- app/src/screen/common.rs | 26 ++++++++++- app/src/screen/deposit/mod.rs | 6 +-- app/src/screen/deposit/view.rs | 52 ++++++++++++++++----- app/src/screen/device_selection/mod.rs | 6 +-- app/src/screen/device_selection/view.rs | 27 +++++++---- app/src/screen/mod.rs | 14 +++--- app/src/screen/portfolio/mod.rs | 6 +-- app/src/screen/portfolio/view.rs | 51 ++++++++++++++------- app/src/screen/resources.rs | 21 +++++++++ 12 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 app/src/screen/resources.rs diff --git a/app/src/app.rs b/app/src/app.rs index e2ca95b..c983adb 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -23,7 +23,7 @@ use crate::{ cache::Cache as LedgerApiCache, mock::LedgerApiMock, Device, DeviceInfo, LedgerApiT, }, }, - screen::{OutgoingMessage, Screen, ScreenName}, + screen::{resources::Resources, OutgoingMessage, Screen, ScreenName}, }; pub struct App { @@ -140,8 +140,12 @@ impl App { mut screen: Screen, terminal: &mut Terminal, ) -> (StateRegistry, ApiRegistry, OutgoingMessage) { + let resources = Resources::default(); + loop { - terminal.draw(|frame| screen.render(frame)).unwrap(); + terminal + .draw(|frame| screen.render(frame, &resources)) + .unwrap(); let event = event::poll(Duration::ZERO) .unwrap() diff --git a/app/src/screen/asset/mod.rs b/app/src/screen/asset/mod.rs index a745aca..6eb0674 100644 --- a/app/src/screen/asset/mod.rs +++ b/app/src/screen/asset/mod.rs @@ -3,7 +3,7 @@ use ratatui::{crossterm::event::Event, Frame}; use rust_decimal::Decimal; use strum::EnumIter; -use super::{OutgoingMessage, ScreenT}; +use super::{resources::Resources, OutgoingMessage, ScreenT}; use crate::{ api::{ blockchain_monitoring::{BlockchainMonitoringApiT, TransactionInfo, TransactionUid}, @@ -104,8 +104,8 @@ impl ScreenT) { - view::render(self, frame); + fn render(&self, frame: &mut Frame<'_>, resources: &Resources) { + view::render(self, frame, resources); } fn tick(&mut self, event: Option) -> Option { diff --git a/app/src/screen/asset/view.rs b/app/src/screen/asset/view.rs index fd95825..a24d068 100644 --- a/app/src/screen/asset/view.rs +++ b/app/src/screen/asset/view.rs @@ -20,7 +20,10 @@ use crate::{ common_types::{Account, Network}, ledger::LedgerApiT, }, - screen::common::{format_address, network_symbol, render_centered_text}, + screen::{ + common::{format_address, network_symbol, render_centered_text, BackgroundWidget}, + resources::Resources, + }, }; use super::{Model, TimePeriod}; @@ -31,15 +34,23 @@ const TX_UID_MAX_LEN: usize = 16; pub(super) fn render( model: &Model, frame: &mut Frame<'_>, + resources: &Resources, ) { let area = frame.size(); + frame.render_widget(BackgroundWidget::new(resources.background_color), area); + let [price_chart_area, txs_list_area] = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Fill(1); 2]) .areas(area); - let price_chart_block = Block::new().title("Price").borders(Borders::all()); + let price_chart_block = Block::new() + .title("Price") + .borders(Borders::all()) + .fg(resources.main_color) + .bg(resources.background_color); + let inner_price_chart_area = price_chart_block.inner(price_chart_area); frame.render_widget(price_chart_block, price_chart_area); @@ -49,15 +60,23 @@ pub(super) fn render { @@ -90,8 +110,9 @@ fn render_price_chart( selected_time_period: TimePeriod, frame: &mut Frame<'_>, area: Rect, + resources: &Resources, ) { - let legend = render_chart_legend(selected_time_period); + let legend = render_chart_legend(selected_time_period, resources); let max_price = *prices.iter().max().expect("Empty `prices` vector provided"); @@ -105,23 +126,19 @@ fn render_price_chart( .marker(symbols::Marker::Bar) .name(legend) .graph_type(GraphType::Line) - .style(Style::default().magenta()) .data(&price_data)]; - let x_axis = Axis::default() - .style(Style::default().white()) - .bounds([0.0, (price_data.len() - 1) as f64]); + let x_axis = Axis::default().bounds([0.0, (price_data.len() - 1) as f64]); - let y_axis = Axis::default() - .style(Style::default().white()) - .bounds([0.0, max_price.try_into().unwrap()]); + let y_axis = Axis::default().bounds([0.0, max_price.try_into().unwrap()]); let chart = Chart::new(datasets) .x_axis(x_axis) .y_axis(y_axis) .legend_position(Some(ratatui::widgets::LegendPosition::BottomRight)) // Always show a legend(see `hidden_legend_constraints` docs). - .hidden_legend_constraints((Constraint::Min(0), Constraint::Min(0))); + .hidden_legend_constraints((Constraint::Min(0), Constraint::Min(0))) + .bg(resources.background_color); frame.render_widget(chart, area); } @@ -130,13 +147,15 @@ fn render_price_chart_placeholder( selected_time_period: TimePeriod, frame: &mut Frame<'_>, area: Rect, + resources: &Resources, ) { - let legend = render_chart_legend(selected_time_period); + let legend = render_chart_legend(selected_time_period, resources); let chart = Chart::new(vec![Dataset::default().name(legend)]) .legend_position(Some(ratatui::widgets::LegendPosition::BottomRight)) // Always show a legend(see `hidden_legend_constraints` docs). - .hidden_legend_constraints((Constraint::Min(0), Constraint::Min(0))); + .hidden_legend_constraints((Constraint::Min(0), Constraint::Min(0))) + .bg(resources.background_color); frame.render_widget(chart, area); @@ -144,7 +163,7 @@ fn render_price_chart_placeholder( render_centered_text(frame, area, text); } -fn render_chart_legend(selected_time_period: TimePeriod) -> Line<'static> { +fn render_chart_legend(selected_time_period: TimePeriod, resources: &Resources) -> Line<'static> { let legend = TimePeriod::iter().map(|period| { let label = match period { TimePeriod::Day => " d[ay]", @@ -155,9 +174,9 @@ fn render_chart_legend(selected_time_period: TimePeriod) -> Line<'static> { }; if period == selected_time_period { - Span::raw(label).red() + Span::raw(label).fg(resources.accent_color) } else { - Span::raw(label).green() + Span::raw(label).fg(resources.main_color) } }); @@ -169,6 +188,7 @@ fn render_tx_list( tx_list: &[(TransactionUid, TransactionInfo)], frame: &mut Frame<'_>, area: Rect, + resources: &Resources, ) { let (selected_account_network, selected_account) = selected_account; @@ -193,7 +213,7 @@ fn render_tx_list( vec![ Span::raw(from), Span::raw(" -> "), - Span::raw(to).green(), + Span::raw(to).fg(resources.accent_color), Span::raw(format!(" for {}{}", amount, network_icon)), ] } @@ -202,7 +222,7 @@ fn render_tx_list( let to = format_address(&to.get_info().pk, ADDRESSES_MAX_LEN); vec![ - Span::raw(from).green(), + Span::raw(from).fg(resources.accent_color), Span::raw(" -> "), Span::raw(to), Span::raw(format!(" for {}{}", amount, network_icon)), diff --git a/app/src/screen/common.rs b/app/src/screen/common.rs index 7631d28..d7e4ee0 100644 --- a/app/src/screen/common.rs +++ b/app/src/screen/common.rs @@ -1,6 +1,8 @@ use ratatui::{ layout::{Constraint, Flex, Layout, Rect}, - text::Text, + style::{Color, Stylize}, + text::{Line, Text}, + widgets::Widget, Frame, }; @@ -48,6 +50,28 @@ pub fn format_address(address: &str, max_symbols: usize) -> String { ) } +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 ratatui::prelude::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); + } + } +} + #[cfg(test)] mod tests { use itertools::Itertools; diff --git a/app/src/screen/deposit/mod.rs b/app/src/screen/deposit/mod.rs index ad2f52c..113dc15 100644 --- a/app/src/screen/deposit/mod.rs +++ b/app/src/screen/deposit/mod.rs @@ -2,7 +2,7 @@ use std::time::Instant; use ratatui::{crossterm::event::Event, Frame}; -use super::{OutgoingMessage, ScreenT}; +use super::{resources::Resources, OutgoingMessage, ScreenT}; use crate::{ api::{ blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT, @@ -33,8 +33,8 @@ impl ScreenT) { - view::render(self, frame); + fn render(&self, frame: &mut Frame<'_>, resources: &Resources) { + view::render(self, frame, resources); } fn tick(&mut self, event: Option) -> Option { diff --git a/app/src/screen/deposit/view.rs b/app/src/screen/deposit/view.rs index cb8d768..4a571c5 100644 --- a/app/src/screen/deposit/view.rs +++ b/app/src/screen/deposit/view.rs @@ -4,14 +4,18 @@ use qrcode::{Color as QrCodeColor, QrCode}; use ratatui::{ layout::{Alignment, Constraint, Flex, Layout, Rect}, prelude::Buffer, - style::Stylize, + style::{Color, Stylize}, text::Text, widgets::{Block, BorderType, Borders, Padding, Widget}, Frame, }; -use crate::api::{ - blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT, ledger::LedgerApiT, +use crate::{ + api::{ + blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT, + ledger::LedgerApiT, + }, + screen::{common::BackgroundWidget, resources::Resources}, }; use super::Model; @@ -21,7 +25,12 @@ const DISPLAY_COPIED_TEXT_FOR: Duration = Duration::from_secs(2); pub(super) fn render( model: &Model, frame: &mut Frame<'_>, + resources: &Resources, ) { + let area = frame.size(); + + frame.render_widget(BackgroundWidget::new(resources.background_color), area); + let pubkey = model .state .selected_account @@ -31,9 +40,9 @@ pub(super) fn render Self { + fn size(mut self, size: QrCodeSize) -> Self { self.size = size; self } + fn dark_color(mut self, color: Color) -> Self { + self.dark_color = color; + self + } + + fn light_color(mut self, color: Color) -> Self { + self.light_color = color; + self + } + fn render_big(&self, code: QrCode) -> String { let width = code.width(); let colors = code.into_colors(); diff --git a/app/src/screen/device_selection/mod.rs b/app/src/screen/device_selection/mod.rs index 603a3ef..b31633c 100644 --- a/app/src/screen/device_selection/mod.rs +++ b/app/src/screen/device_selection/mod.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use futures::executor::block_on; use ratatui::{crossterm::event::Event, Frame}; -use super::{OutgoingMessage, ScreenT}; +use super::{resources::Resources, OutgoingMessage, ScreenT}; use crate::{ api::{ blockchain_monitoring::BlockchainMonitoringApiT, @@ -71,8 +71,8 @@ impl ScreenT) { - view::render(self, frame); + fn render(&self, frame: &mut Frame<'_>, resources: &Resources) { + view::render(self, frame, resources); } fn tick(&mut self, event: Option) -> Option { diff --git a/app/src/screen/device_selection/view.rs b/app/src/screen/device_selection/view.rs index 72fbc0e..e0574a6 100644 --- a/app/src/screen/device_selection/view.rs +++ b/app/src/screen/device_selection/view.rs @@ -1,26 +1,33 @@ use ratatui::{ layout::{Alignment, Margin}, - style::{Color, Stylize}, + style::Stylize, text::Text, widgets::{Block, BorderType, Borders, List, Padding}, Frame, }; use super::Model; -use crate::api::{ - blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT, ledger::LedgerApiT, +use crate::{ + api::{ + blockchain_monitoring::BlockchainMonitoringApiT, coin_price::CoinPriceApiT, + ledger::LedgerApiT, + }, + screen::{common::BackgroundWidget, resources::Resources}, }; pub(super) fn render( model: &Model, frame: &mut Frame<'_>, + resources: &Resources, ) { let area = frame.size(); + frame.render_widget(BackgroundWidget::new(resources.background_color), area); + let list_block = Block::new() .border_type(BorderType::Double) .borders(Borders::all()) - .border_style(Color::Green) + .border_style(resources.main_color) .padding(Padding::uniform(1)) .title("Select a device") .title_alignment(Alignment::Center); @@ -32,11 +39,15 @@ pub(super) fn render { Asset(asset::Model), @@ -44,12 +46,12 @@ impl Screen) { + pub fn render(&self, frame: &mut Frame<'_>, resources: &Resources) { match self { - Self::Asset(screen) => screen.render(frame), - Self::Deposit(screen) => screen.render(frame), - Self::DeviceSelection(screen) => screen.render(frame), - Self::Portfolio(screen) => screen.render(frame), + Self::Asset(screen) => screen.render(frame, resources), + Self::Deposit(screen) => screen.render(frame, resources), + Self::DeviceSelection(screen) => screen.render(frame, resources), + Self::Portfolio(screen) => screen.render(frame, resources), } } @@ -75,7 +77,7 @@ impl Screen { fn construct(state: StateRegistry, api_registry: ApiRegistry) -> Self; - fn render(&self, frame: &mut Frame<'_>); + fn render(&self, frame: &mut Frame<'_>, resources: &Resources); fn tick(&mut self, event: Option) -> Option; fn deconstruct(self) -> (StateRegistry, ApiRegistry); diff --git a/app/src/screen/portfolio/mod.rs b/app/src/screen/portfolio/mod.rs index 3fd9670..e2b9bb9 100644 --- a/app/src/screen/portfolio/mod.rs +++ b/app/src/screen/portfolio/mod.rs @@ -4,7 +4,7 @@ use futures::executor::block_on; use ratatui::{crossterm::event::Event, Frame}; use rust_decimal::Decimal; -use super::{OutgoingMessage, ScreenT}; +use super::{resources::Resources, OutgoingMessage, ScreenT}; use crate::{ api::{ blockchain_monitoring::BlockchainMonitoringApiT, @@ -108,8 +108,8 @@ impl ScreenT) { - view::render(self, frame); + fn render(&self, frame: &mut Frame<'_>, resources: &Resources) { + view::render(self, frame, resources); } fn tick(&mut self, event: Option) -> Option { diff --git a/app/src/screen/portfolio/view.rs b/app/src/screen/portfolio/view.rs index c80fb59..29199e2 100644 --- a/app/src/screen/portfolio/view.rs +++ b/app/src/screen/portfolio/view.rs @@ -1,6 +1,7 @@ use ratatui::{ - layout::{Alignment, Constraint}, - style::{Color, Style, Stylize}, + buffer::Buffer, + layout::{Alignment, Constraint, Rect}, + style::{Style, Stylize}, text::Text, widgets::{ block::Title, Block, BorderType, Borders, HighlightSpacing, Padding, Row, StatefulWidget, @@ -19,18 +20,27 @@ use crate::{ common_types::{Account, Network}, ledger::LedgerApiT, }, - screen::common::network_symbol, + screen::{ + common::{network_symbol, BackgroundWidget}, + resources::Resources, + }, }; pub(super) fn render( model: &Model, frame: &mut Frame<'_>, + resources: &Resources, ) { + frame.render_widget( + BackgroundWidget::new(resources.background_color), + frame.size(), + ); + if let Some(accounts) = model.state.device_accounts.as_ref() { - render_account_table(model, frame, accounts); + render_account_table(model, frame, accounts, resources); } else { // TODO: Process case when device is connected but accounts haven't been loaded yet. - render_account_table_placeholder(frame); + render_account_table_placeholder(frame, resources); } } @@ -38,6 +48,7 @@ fn render_account_table, frame: &mut Frame<'_>, accounts: &[(Network, Vec)], + resources: &Resources, ) { let area = frame.size(); @@ -70,6 +81,7 @@ fn render_account_table { network: Network, accounts_and_balances: Vec<(Account, Option)>, @@ -91,9 +103,11 @@ struct NetworkAccountsTable { is_self_selected: bool, price: Option, + + resources: &'a Resources, } -impl PreRender for NetworkAccountsTable { +impl<'a> PreRender for NetworkAccountsTable<'a> { fn pre_render(&mut self, context: &tui_widget_list::PreRenderContext) -> u16 { self.is_self_selected = context.is_selected; @@ -101,8 +115,8 @@ impl PreRender for NetworkAccountsTable { } } -impl Widget for NetworkAccountsTable { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) +impl<'a> Widget for NetworkAccountsTable<'a> { + fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { @@ -119,7 +133,7 @@ impl Widget for NetworkAccountsTable { let block = Block::new() .border_type(BorderType::Rounded) .borders(Borders::all()) - .border_style(Color::Yellow) + .border_style(self.resources.main_color) .title(Title::from(self.network.get_info().name).alignment(Alignment::Left)) .title(Title::from(price_label).alignment(Alignment::Right)); @@ -147,13 +161,17 @@ impl Widget for NetworkAccountsTable { let balance = Text::from(balance).alignment(Alignment::Center); let price = Text::from(price).alignment(Alignment::Right); - Row::new(vec![pk, balance, price]) + Row::new(vec![pk, balance, price]).fg(self.resources.main_color) }); let table = Table::new(rows, [Constraint::Ratio(1, 3); 3]) .column_spacing(1) .block(block) - .highlight_style(Style::new().reversed()) + .highlight_style( + Style::new() + .bg(self.resources.accent_color) + .fg(self.resources.background_color), + ) .highlight_spacing(HighlightSpacing::Always) .highlight_symbol(">>"); @@ -162,21 +180,22 @@ impl Widget for NetworkAccountsTable { } } -fn render_account_table_placeholder(frame: &mut Frame<'_>) { +fn render_account_table_placeholder(frame: &mut Frame<'_>, resources: &Resources) { let area = frame.size(); let block = Block::new() .border_type(BorderType::Double) .borders(Borders::all()) - .border_style(Color::Yellow) + .border_style(resources.main_color) .padding(Padding::uniform(1)) .title("Portfolio") .title_alignment(Alignment::Center); let text_area = block.inner(area); - let text = - Text::raw("Device is not selected. Please select device(`d`)").alignment(Alignment::Center); + let text = Text::raw("Device is not selected. Please select device(`d`)") + .alignment(Alignment::Center) + .fg(resources.main_color); frame.render_widget(block, area); frame.render_widget(text, text_area); diff --git a/app/src/screen/resources.rs b/app/src/screen/resources.rs new file mode 100644 index 0000000..d3c5143 --- /dev/null +++ b/app/src/screen/resources.rs @@ -0,0 +1,21 @@ +use ratatui::style::Color; + +pub struct Resources { + pub main_color: Color, + pub accent_color: Color, + pub background_color: Color, + pub qr_code_dark_color: Color, + pub qr_code_light_color: Color, +} + +impl Default for Resources { + fn default() -> Self { + Self { + main_color: Color::Black, + accent_color: Color::Red, + background_color: Color::White, + qr_code_dark_color: Color::Black, + qr_code_light_color: Color::White, + } + } +}