From b5d8453d013f4b81934da88d649ea3685f113dc2 Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Tue, 9 Jan 2024 18:16:51 +0100 Subject: [PATCH] lib: Use themed style builders --- bin/commands/issue/suite.rs | 6 +- bin/commands/issue/suite/ui.rs | 45 +++---- bin/commands/patch/list/ui.rs | 71 ++++++----- bin/commands/patch/select.rs | 6 +- bin/commands/patch/suite.rs | 6 +- bin/commands/patch/suite/ui.rs | 18 +-- src/ui.rs | 33 +++--- src/ui/cob.rs | 85 +++++--------- src/ui/theme.rs | 207 +++++++++++++++++---------------- src/ui/widget/container.rs | 38 ++---- src/ui/widget/context.rs | 31 +++-- src/ui/widget/form.rs | 10 +- src/ui/widget/label.rs | 28 +---- src/ui/widget/list.rs | 14 +-- 14 files changed, 262 insertions(+), 336 deletions(-) diff --git a/bin/commands/issue/suite.rs b/bin/commands/issue/suite.rs index f7c51ac1..21de1c40 100644 --- a/bin/commands/issue/suite.rs +++ b/bin/commands/issue/suite.rs @@ -18,7 +18,7 @@ use radicle_tui as tui; use tui::cob; use tui::context::Context; use tui::ui::subscription; -use tui::ui::theme::{self, Theme}; +use tui::ui::theme::Theme; use tui::{Exit, PageStack, Tui}; use page::{IssuePage, ListPage}; @@ -110,7 +110,7 @@ impl App { Self { context, pages: PageStack::default(), - theme: theme::default_dark(), + theme: Theme::default(), quit: false, } } @@ -160,7 +160,7 @@ impl App { app: &mut Application, message: Message, ) -> Result> { - let theme = theme::default_dark(); + let theme = Theme::default(); match message { Message::Batch(messages) => { let mut results = vec![]; diff --git a/bin/commands/issue/suite/ui.rs b/bin/commands/issue/suite/ui.rs index 594bc5bd..37075811 100644 --- a/bin/commands/issue/suite/ui.rs +++ b/bin/commands/issue/suite/ui.rs @@ -6,6 +6,7 @@ use radicle::cob::thread::CommentId; use radicle::cob::issue::Issue; use radicle::cob::issue::IssueId; +use tui::ui::theme::style; use tuirealm::command::{Cmd, CmdResult}; use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State}; @@ -72,7 +73,7 @@ impl IssueBrowser { }; let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())) - .highlight(theme.colors.item_list_highlighted_bg); + .highlight(style::highlight().fg.unwrap()); Self { items, table } } @@ -123,7 +124,7 @@ impl LargeList { selected.map(|(id, issue)| IssueItem::from((context.profile(), repo, id, issue))); let list = Widget::new(List::new(&items, selected, theme.clone())) - .highlight(theme.colors.item_list_highlighted_bg); + .highlight(style::highlight().fg.unwrap()); let container = tui::ui::container(theme, list.to_boxed()); @@ -173,35 +174,35 @@ impl IssueHeader { let item = IssueItem::from((context.profile(), repo, id, issue.clone())); let title = Property::new( - tui::ui::label("Title").foreground(theme.colors.property_name_fg), - tui::ui::label(item.title()).foreground(theme.colors.browser_list_title), + tui::ui::label("Title").foreground(style::cyan().fg.unwrap()), + tui::ui::label(item.title()).foreground(style::reset().fg.unwrap()), ); - let author = match alias { - Some(_) => tui::ui::label(&cob::format_author(issue.author().id(), &alias, by_you)) - .foreground(theme.colors.browser_list_author), - None => tui::ui::label(&cob::format_author(issue.author().id(), &alias, by_you)) - .foreground(theme.colors.browser_list_author) - .dim(), + let author_style = match alias { + Some(_) => style::magenta(), + None => style::magenta_dim(), }; + + let author = tui::ui::label(&cob::format_author(issue.author().id(), &alias, by_you)) + .foreground(author_style.fg.unwrap()); let author = Property::new( - tui::ui::label("Author").foreground(theme.colors.property_name_fg), + tui::ui::label("Author").foreground(style::cyan().fg.unwrap()), author, ); let issue_id = Property::new( - tui::ui::label("Issue").foreground(theme.colors.property_name_fg), - tui::ui::label(&id.to_string()).foreground(theme.colors.browser_list_description), + tui::ui::label("Issue").foreground(style::cyan().fg.unwrap()), + tui::ui::label(&id.to_string()).foreground(style::gray().fg.unwrap()), ); let labels = Property::new( - tui::ui::label("Labels").foreground(theme.colors.property_name_fg), + tui::ui::label("Labels").foreground(style::cyan().fg.unwrap()), tui::ui::label(&cob::format_labels(item.labels())) - .foreground(theme.colors.browser_list_labels), + .foreground(style::lightblue().fg.unwrap()), ); let assignees = Property::new( - tui::ui::label("Assignees").foreground(theme.colors.property_name_fg), + tui::ui::label("Assignees").foreground(style::cyan().fg.unwrap()), tui::ui::label(&cob::format_assignees( &item .assignees() @@ -209,12 +210,12 @@ impl IssueHeader { .map(|item| (item.did(), item.alias(), item.is_you())) .collect::>(), )) - .foreground(theme.colors.browser_list_author), + .foreground(author_style.fg.unwrap()), ); let state = Property::new( - tui::ui::label("Status").foreground(theme.colors.property_name_fg), - tui::ui::label(&item.state().to_string()).foreground(theme.colors.browser_list_title), + tui::ui::label("Status").foreground(style::cyan().fg.unwrap()), + tui::ui::label(&item.state().to_string()).foreground(style::reset().fg.unwrap()), ); let table = tui::ui::property_table( @@ -303,9 +304,9 @@ impl CommentBody { Some((_, comment)) => comment.body().to_string(), None => String::new(), }; - let textarea = Widget::new(Textarea::new(theme.clone())) + let textarea = Widget::new(Textarea::default()) .content(AttrValue::String(content)) - .foreground(theme.colors.default_fg); + .foreground(style::reset().fg.unwrap()); let textarea = tui::ui::container(theme, textarea.to_boxed()); @@ -335,7 +336,7 @@ impl WidgetComponent for CommentBody { pub fn list_navigation(theme: &Theme) -> Widget { tui::ui::tabs( theme, - vec![tui::ui::reversable_label("Issues").foreground(theme.colors.tabs_highlighted_fg)], + vec![tui::ui::reversable_label("Issues").foreground(style::magenta().fg.unwrap())], ) } diff --git a/bin/commands/patch/list/ui.rs b/bin/commands/patch/list/ui.rs index 01ccf27a..35318a90 100644 --- a/bin/commands/patch/list/ui.rs +++ b/bin/commands/patch/list/ui.rs @@ -8,7 +8,7 @@ use radicle_tui as tui; use tui::context::Context; use tui::ui::cob::PatchItem; -use tui::ui::theme::Theme; +use tui::ui::theme::{style, Theme}; use tui::ui::widget::{Widget, WidgetComponent}; use tui::ui::widget::container::Tabs; @@ -23,14 +23,14 @@ pub struct PatchBrowser { impl PatchBrowser { pub fn new(context: &Context, theme: &Theme, selected: Option<(PatchId, Patch)>) -> Self { let header = [ - tui::ui::label(" ● "), - tui::ui::label("ID"), - tui::ui::label("Title"), - tui::ui::label("Author"), - tui::ui::label("Head"), - tui::ui::label("+"), - tui::ui::label("-"), - tui::ui::label("Updated"), + tui::ui::label(" ● ").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("ID").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("Title").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("Author").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("Head").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("+").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("-").foreground(style::reset_dim().fg.unwrap()), + tui::ui::label("Updated").foreground(style::reset_dim().fg.unwrap()), ]; let widths = [ @@ -65,7 +65,7 @@ impl PatchBrowser { }; let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())) - .highlight(theme.colors.item_list_highlighted_bg); + .highlight(style::highlight().fg.unwrap()); Self { items, table } } @@ -97,7 +97,7 @@ impl WidgetComponent for PatchBrowser { pub fn list_navigation(theme: &Theme) -> Widget { tui::ui::tabs( theme, - vec![tui::ui::reversable_label("Patches").foreground(theme.colors.tabs_highlighted_fg)], + vec![tui::ui::reversable_label("Patches").foreground(style::cyan().fg.unwrap())], ) } @@ -109,7 +109,7 @@ pub fn patches( Widget::new(PatchBrowser::new(context, theme, selected)) } -pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> Widget { +pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) -> Widget { use radicle::cob::patch::State; use tui::ui::{label, label_group}; @@ -132,49 +132,48 @@ pub fn browse_context(context: &Context, theme: &Theme, progress: Progress) -> W } let context = label(" Patches ") - .foreground(theme.colors.context_bg) - .background(theme.colors.context_badge_bg); + .foreground(style::magenta_reversed().fg.unwrap()) + .background(style::magenta_reversed().bg.unwrap()); let divider = label(" | ") - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let draft_n = label(&format!("{draft}")) - .foreground(tuirealm::props::Color::Gray) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let draft = label(" Draft") - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let open_n = label(&format!("{open}")) - .foreground(tuirealm::props::Color::Green) - .background(theme.colors.context_bg); + .foreground(style::green().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let open = label(" Open") - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let archived_n = label(&format!("{archived}")) - .foreground(tuirealm::props::Color::Yellow) - .background(theme.colors.context_bg); + .foreground(style::yellow().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let archived = label(" Archived") - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let merged_n = label(&format!("{merged}")) - .foreground(tuirealm::props::Color::Cyan) - .background(theme.colors.context_bg); + .foreground(style::cyan().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let merged = label(" Merged ") - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let progress = label(&format!(" {} ", progress.to_string())) - .foreground(theme.colors.context_bg) - .background(theme.colors.context_badge_bg); + .foreground(style::magenta_reversed().fg.unwrap()) + .background(style::magenta_reversed().bg.unwrap()); - let spacer = label("").background(theme.colors.context_bg); + let spacer = label("").background(style::default_reversed().bg.unwrap()); let context_bar = ContextBar::new( - theme.clone(), label_group(&[context]), label_group(&[spacer.clone()]), label_group(&[spacer]), diff --git a/bin/commands/patch/select.rs b/bin/commands/patch/select.rs index c67362d3..c01e2202 100644 --- a/bin/commands/patch/select.rs +++ b/bin/commands/patch/select.rs @@ -19,7 +19,7 @@ use radicle_tui as tui; use tui::context::Context; use tui::ui::subscription; -use tui::ui::theme::{self, Theme}; +use tui::ui::theme::Theme; use tui::{Exit, PageStack, Tui}; use page::ListView; @@ -74,7 +74,7 @@ impl App { Self { context, pages: PageStack::default(), - theme: theme::default_dark(), + theme: Theme::default(), quit: false, patch_id: None, } @@ -96,7 +96,7 @@ impl App { app: &mut Application, message: Message, ) -> Result> { - let theme = theme::default_dark(); + let theme = Theme::default(); match message { Message::Batch(messages) => { let mut results = vec![]; diff --git a/bin/commands/patch/suite.rs b/bin/commands/patch/suite.rs index a94eb5d6..2c6fa2e5 100644 --- a/bin/commands/patch/suite.rs +++ b/bin/commands/patch/suite.rs @@ -20,7 +20,7 @@ use radicle_tui as tui; use tui::cob; use tui::context::Context; use tui::ui::subscription; -use tui::ui::theme::{self, Theme}; +use tui::ui::theme::Theme; use tui::{Exit, PageStack, Tui}; use page::{ListView, PatchView}; @@ -94,7 +94,7 @@ impl App { Self { context, pages: PageStack::default(), - theme: theme::default_dark(), + theme: Theme::default(), quit: false, } } @@ -135,7 +135,7 @@ impl App { app: &mut Application, message: Message, ) -> Result> { - let theme = theme::default_dark(); + let theme = Theme::default(); match message { Message::Batch(messages) => { let mut results = vec![]; diff --git a/bin/commands/patch/suite/ui.rs b/bin/commands/patch/suite/ui.rs index fde02c56..a43a6533 100644 --- a/bin/commands/patch/suite/ui.rs +++ b/bin/commands/patch/suite/ui.rs @@ -11,7 +11,7 @@ use tui::context::Context; use tui::ui::cob; use tui::ui::cob::PatchItem; use tui::ui::layout; -use tui::ui::theme::Theme; +use tui::ui::theme::{style, Theme}; use tui::ui::widget::{Widget, WidgetComponent}; use tui::ui::widget::container::Tabs; @@ -69,7 +69,7 @@ impl PatchBrowser { }; let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone())) - .highlight(theme.colors.item_list_highlighted_bg); + .highlight(style::highlight().fg.unwrap()); Self { items, table } } @@ -163,7 +163,7 @@ impl WidgetComponent for Files { pub fn list_navigation(theme: &Theme) -> Widget { tui::ui::tabs( theme, - vec![tui::ui::reversable_label("Patches").foreground(theme.colors.tabs_highlighted_fg)], + vec![tui::ui::reversable_label("Patches").foreground(style::magenta().fg.unwrap())], ) } @@ -171,8 +171,8 @@ pub fn navigation(theme: &Theme) -> Widget { tui::ui::tabs( theme, vec![ - tui::ui::reversable_label("Activity").foreground(theme.colors.tabs_highlighted_fg), - tui::ui::reversable_label("Files").foreground(theme.colors.tabs_highlighted_fg), + tui::ui::reversable_label("Activity").foreground(style::magenta().fg.unwrap()), + tui::ui::reversable_label("Files").foreground(style::magenta().fg.unwrap()), ], ) } @@ -185,15 +185,15 @@ pub fn patches( Widget::new(PatchBrowser::new(context, theme, selected)) } -pub fn activity(theme: &Theme) -> Widget { - let not_implemented = tui::ui::label("not implemented").foreground(theme.colors.default_fg); +pub fn activity(_theme: &Theme) -> Widget { + let not_implemented = tui::ui::label("not implemented").foreground(style::reset().fg.unwrap()); let activity = Activity::new(not_implemented); Widget::new(activity) } -pub fn files(theme: &Theme) -> Widget { - let not_implemented = tui::ui::label("not implemented").foreground(theme.colors.default_fg); +pub fn files(_theme: &Theme) -> Widget { + let not_implemented = tui::ui::label("not implemented").foreground(style::reset().fg.unwrap()); let files = Files::new(not_implemented); Widget::new(files) diff --git a/src/ui.rs b/src/ui.rs index f3f9ce3d..9ec5e05e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -19,10 +19,10 @@ use widget::list::{ColumnWidth, Property, PropertyList, PropertyTable}; use widget::Widget; +use self::theme::{style, Theme}; use self::widget::label::LabelGroup; use super::context::Context; -use crate::ui::theme::Theme; pub fn global_listener() -> Widget { Widget::new(GlobalListener::default()) @@ -75,7 +75,7 @@ pub fn labeled_container( ) -> Widget { let header = container_header( theme, - label(&format!(" {title} ")).foreground(theme.colors.default_fg), + label(&format!(" {title} ")).foreground(style::reset().fg.unwrap()), ); let container = LabeledContainer::new(header, component, theme.clone()); @@ -83,9 +83,9 @@ pub fn labeled_container( } pub fn shortcut(theme: &Theme, short: &str, long: &str) -> Widget { - let short = label(short).foreground(theme.colors.shortcut_short_fg); + let short = label(short).foreground(style::gray().fg.unwrap()); + let long = label(long).foreground(style::gray_dim().fg.unwrap()); let divider = label(&theme.icons.whitespace.to_string()); - let long = label(long).foreground(theme.colors.shortcut_long_fg); // TODO: Remove when size constraints are implemented let short_w = short.query(Attribute::Width).unwrap().unwrap_size(); @@ -100,16 +100,16 @@ pub fn shortcut(theme: &Theme, short: &str, long: &str) -> Widget { pub fn shortcuts(theme: &Theme, shortcuts: Vec>) -> Widget { let divider = label(&format!(" {} ", theme.icons.shortcutbar_divider)) - .foreground(theme.colors.shortcutbar_divider_fg); + .foreground(style::gray_dim().fg.unwrap()); let shortcut_bar = Shortcuts::new(shortcuts, divider); Widget::new(shortcut_bar).height(1) } pub fn property(theme: &Theme, name: &str, value: &str) -> Widget { - let name = label(name).foreground(theme.colors.property_name_fg); + let name = label(name).foreground(style::cyan().fg.unwrap()); let divider = label(&format!(" {} ", theme.icons.property_divider)); - let value = label(value).foreground(theme.colors.default_fg); + let value = label(value).foreground(style::reset().fg.unwrap()); // TODO: Remove when size constraints are implemented let name_w = name.query(Attribute::Width).unwrap().unwrap_size(); @@ -140,9 +140,9 @@ pub fn tabs(_theme: &Theme, tabs: Vec>) -> Widget { Widget::new(tabs).height(2) } -pub fn app_info(context: &Context, theme: &Theme) -> Widget { - let project = label(context.project().name()).foreground(theme.colors.app_header_project_fg); - let rid = label(&format!(" ({})", context.id())).foreground(theme.colors.app_header_rid_fg); +pub fn app_info(context: &Context) -> Widget { + let project = label(context.project().name()).foreground(style::cyan().fg.unwrap()); + let rid = label(&format!(" ({})", context.id())).foreground(style::yellow().fg.unwrap()); let project_w = project .query(Attribute::Width) @@ -163,17 +163,16 @@ pub fn app_header( nav: Option>, ) -> Widget { let line = - label(&theme.icons.tab_overline.to_string()).foreground(theme.colors.tabs_highlighted_fg); + label(&theme.icons.tab_overline.to_string()).foreground(style::magenta().fg.unwrap()); let line = Widget::new(VerticalLine::new(line)); - let info = app_info(context, theme); + let info = app_info(context); let header = AppHeader::new(nav, info, line); Widget::new(header) } pub fn info(theme: &Theme, message: &str) -> Widget { - let textarea = - Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned())); + let textarea = Widget::new(Textarea::default()).content(AttrValue::String(message.to_owned())); let container = labeled_container(theme, "Info", textarea.to_boxed()); Widget::new(Popup::new(theme.clone(), container)) @@ -182,8 +181,7 @@ pub fn info(theme: &Theme, message: &str) -> Widget { } pub fn warning(theme: &Theme, message: &str) -> Widget { - let textarea = - Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned())); + let textarea = Widget::new(Textarea::default()).content(AttrValue::String(message.to_owned())); let container = labeled_container(theme, "Warning", textarea.to_boxed()); Widget::new(Popup::new(theme.clone(), container)) @@ -192,8 +190,7 @@ pub fn warning(theme: &Theme, message: &str) -> Widget { } pub fn error(theme: &Theme, message: &str) -> Widget { - let textarea = - Widget::new(Textarea::new(theme.clone())).content(AttrValue::String(message.to_owned())); + let textarea = Widget::new(Textarea::default()).content(AttrValue::String(message.to_owned())); let container = labeled_container(theme, "Error", textarea.to_boxed()); Widget::new(Popup::new(theme.clone(), container)) diff --git a/src/ui/cob.rs b/src/ui/cob.rs index e721b943..b5130bf0 100644 --- a/src/ui/cob.rs +++ b/src/ui/cob.rs @@ -2,7 +2,7 @@ pub mod format; use radicle_surf; -use tuirealm::props::{Color, Style, TextModifiers}; +use tuirealm::props::{Color, Style}; use tuirealm::tui::text::{Span, Spans}; use tuirealm::tui::widgets::Cell; @@ -20,6 +20,8 @@ use radicle::cob::{Label, Timestamp}; use crate::ui::theme::Theme; use crate::ui::widget::list::{ListItem, TableItem}; +use super::theme::style; + /// An author item that can be used in tables, list or trees. /// /// Breaks up dependencies to [`Profile`] and [`Repository`] that @@ -142,21 +144,16 @@ impl TryFrom<(&Profile, &Repository, PatchId, Patch)> for PatchItem { } impl TableItem<8> for PatchItem { - fn row(&self, theme: &Theme) -> [Cell; 8] { + fn row(&self, _theme: &Theme) -> [Cell; 8] { let (icon, color) = format_patch_state(&self.state); + let state = Cell::from(icon).style(Style::default().fg(color)); - - let id = Cell::from(format::cob(&self.id)) - .style(Style::default().fg(theme.colors.browser_list_id)); - - let title = Cell::from(self.title.clone()) - .style(Style::default().fg(theme.colors.browser_list_title)); + let id = Cell::from(format::cob(&self.id)).style(style::cyan()); + let title = Cell::from(self.title.clone()).style(style::reset()); let author_style = match &self.author.alias { - Some(_) => Style::default().fg(theme.colors.browser_list_author), - None => Style::default() - .fg(theme.colors.browser_list_author) - .add_modifier(TextModifiers::DIM), + Some(_) => style::magenta(), + None => style::magenta_dim(), }; let author = Cell::from(format_author( &self.author.did, @@ -165,17 +162,10 @@ impl TableItem<8> for PatchItem { )) .style(author_style); - let head = Cell::from(format::oid(self.head)) - .style(Style::default().fg(theme.colors.browser_patch_list_head)); - - let added = Cell::from(format!("+{}", self.added)) - .style(Style::default().fg(theme.colors.browser_patch_list_added)); - - let removed = Cell::from(format!("-{}", self.removed)) - .style(Style::default().fg(theme.colors.browser_patch_list_removed)); - - let updated = Cell::from(format::timestamp(&self.timestamp)) - .style(Style::default().fg(theme.colors.browser_list_timestamp)); + let head = Cell::from(format::oid(self.head)).style(style::lightblue()); + let added = Cell::from(format!("+{}", self.added)).style(style::green()); + let removed = Cell::from(format!("-{}", self.removed)).style(style::red()); + let updated = Cell::from(format::timestamp(&self.timestamp)).style(style::gray()); [state, id, title, author, head, added, removed, updated] } @@ -262,21 +252,16 @@ impl From<(&Profile, &Repository, IssueId, Issue)> for IssueItem { } impl TableItem<7> for IssueItem { - fn row(&self, theme: &Theme) -> [Cell; 7] { + fn row(&self, _theme: &Theme) -> [Cell; 7] { let (icon, color) = format_issue_state(&self.state); + let state = Cell::from(icon).style(Style::default().fg(color)); - - let id = Cell::from(format::cob(&self.id)) - .style(Style::default().fg(theme.colors.browser_list_id)); - - let title = Cell::from(self.title.clone()) - .style(Style::default().fg(theme.colors.browser_list_title)); + let id = Cell::from(format::cob(&self.id)).style(style::cyan()); + let title = Cell::from(self.title.clone()).style(style::reset()); let author_style = match &self.author.alias { - Some(_) => Style::default().fg(theme.colors.browser_list_author), - None => Style::default() - .fg(theme.colors.browser_list_author) - .add_modifier(TextModifiers::DIM), + Some(_) => style::magenta(), + None => style::magenta_dim(), }; let author = Cell::from(format_author( &self.author.did, @@ -285,19 +270,15 @@ impl TableItem<7> for IssueItem { )) .style(author_style); - let labels = Cell::from(format_labels(&self.labels)) - .style(Style::default().fg(theme.colors.browser_list_labels)); - + let labels = Cell::from(format_labels(&self.labels)).style(style::lightblue()); let assignees = self .assignees .iter() .map(|author| (author.did, author.alias.clone(), author.is_you)) .collect::>(); - let assignees = Cell::from(format_assignees(&assignees)) - .style(Style::default().fg(theme.colors.browser_list_author)); - let opened = Cell::from(format::timestamp(&self.timestamp)) - .style(Style::default().fg(theme.colors.browser_list_timestamp)); + let assignees = Cell::from(format_assignees(&assignees)).style(author_style); + let opened = Cell::from(format::timestamp(&self.timestamp)).style(style::gray()); [state, id, title, author, labels, assignees, opened] } @@ -307,18 +288,14 @@ impl ListItem for IssueItem { fn row(&self, theme: &Theme) -> tuirealm::tui::widgets::ListItem { let (state, state_color) = format_issue_state(&self.state); let author_style = match &self.author.alias { - Some(_) => Style::default().fg(theme.colors.browser_list_author), - None => Style::default() - .fg(theme.colors.browser_list_author) - .add_modifier(TextModifiers::DIM), + Some(_) => style::magenta(), + None => style::magenta_dim(), }; + let lines = vec![ Spans::from(vec![ Span::styled(state, Style::default().fg(state_color)), - Span::styled( - self.title.clone(), - Style::default().fg(theme.colors.browser_list_title), - ), + Span::styled(self.title.clone(), style::reset()), ]), Spans::from(vec![ Span::raw(String::from(" ")), @@ -326,14 +303,8 @@ impl ListItem for IssueItem { format_author(&self.author.did, &self.author.alias, self.author.is_you), author_style, ), - Span::styled( - format!(" {} ", theme.icons.property_divider), - Style::default().fg(theme.colors.property_divider_fg), - ), - Span::styled( - format::timestamp(&self.timestamp), - Style::default().fg(theme.colors.browser_list_timestamp), - ), + Span::styled(format!(" {} ", theme.icons.property_divider), style::gray()), + Span::styled(format::timestamp(&self.timestamp), style::gray()), ]), ]; tuirealm::tui::widgets::ListItem::new(lines) diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 71dcf1a9..4a08bab2 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -1,45 +1,4 @@ -use tuirealm::props::Color; - -const COLOR_DEFAULT_FG: Color = Color::Rgb(200, 200, 200); -const COLOR_DEFAULT_DARK_FG: Color = Color::Rgb(150, 150, 150); -const COLOR_DEFAULT_DARK: Color = Color::Rgb(100, 100, 100); -const COLOR_DEFAULT_DARKER: Color = Color::Rgb(70, 70, 70); -const COLOR_DEFAULT_DARKEST: Color = Color::Rgb(40, 40, 40); -const COLOR_DEFAULT_FAINT: Color = Color::Rgb(20, 20, 20); - -#[derive(Debug, Clone)] -pub struct Colors { - pub default_fg: Color, - pub tabs_highlighted_fg: Color, - pub app_header_project_fg: Color, - pub app_header_rid_fg: Color, - pub labeled_container_bg: Color, - pub item_list_highlighted_bg: Color, - pub property_name_fg: Color, - pub property_divider_fg: Color, - pub shortcut_short_fg: Color, - pub shortcut_long_fg: Color, - pub shortcutbar_divider_fg: Color, - pub browser_list_id: Color, - pub browser_list_title: Color, - pub browser_list_description: Color, - pub browser_list_author: Color, - pub browser_list_labels: Color, - pub browser_list_comments: Color, - pub browser_list_timestamp: Color, - pub browser_patch_list_head: Color, - pub browser_patch_list_added: Color, - pub browser_patch_list_removed: Color, - pub context_bg: Color, - pub context_light: Color, - pub context_dark: Color, - pub context_badge_bg: Color, - pub context_badge_edit_bg: Color, - pub context_color_fg: Color, - pub container_border_fg: Color, - pub container_border_focus_fg: Color, - pub input_placeholder_fg: Color, -} +use tuirealm::props::BorderType; #[derive(Debug, Clone)] pub struct Icons { @@ -55,70 +14,118 @@ pub struct Tables { pub spacing: u16, } -/// The Radicle TUI theme. Will be defined in a JSON config file in the -/// future. e.g.: -/// { -/// "name": "Default", -/// "colors": { -/// "foreground": "#ffffff", -/// "propertyForeground": "#ffffff", -/// "highlightedBackground": "#000000", -/// }, -/// "icons": { -/// "workspaces.divider": "|", -/// "shortcuts.divider: "∙", -/// } -/// } +/// The Radicle TUI theme. In the future, it might be defined in a JSON +/// config file. #[derive(Debug, Clone)] pub struct Theme { pub name: String, - pub colors: Colors, pub icons: Icons, pub tables: Tables, + pub border_type: BorderType, } -pub fn default_dark() -> Theme { - Theme { - name: String::from("Default"), - colors: Colors { - default_fg: COLOR_DEFAULT_FG, - tabs_highlighted_fg: Color::Magenta, - app_header_project_fg: Color::Cyan, - app_header_rid_fg: Color::Yellow, - labeled_container_bg: COLOR_DEFAULT_FAINT, - item_list_highlighted_bg: COLOR_DEFAULT_DARKER, - property_name_fg: Color::Cyan, - property_divider_fg: COLOR_DEFAULT_DARK, - shortcut_short_fg: COLOR_DEFAULT_DARK, - shortcut_long_fg: COLOR_DEFAULT_DARKER, - shortcutbar_divider_fg: COLOR_DEFAULT_DARKER, - browser_list_id: Color::Cyan, - browser_list_title: COLOR_DEFAULT_FG, - browser_list_description: COLOR_DEFAULT_DARK, - browser_list_author: Color::Magenta, - browser_list_labels: Color::LightBlue, - browser_list_comments: COLOR_DEFAULT_DARK_FG, - browser_list_timestamp: COLOR_DEFAULT_DARK, - browser_patch_list_head: Color::LightBlue, - browser_patch_list_added: Color::Green, - browser_patch_list_removed: Color::Red, - context_bg: COLOR_DEFAULT_DARKEST, - context_light: Color::Gray, - context_dark: COLOR_DEFAULT_DARK, - context_badge_bg: Color::Magenta, - context_badge_edit_bg: Color::Red, - context_color_fg: Color::Cyan, - container_border_fg: COLOR_DEFAULT_DARKEST, - container_border_focus_fg: COLOR_DEFAULT_DARK, - input_placeholder_fg: COLOR_DEFAULT_DARK, - }, - icons: Icons { - property_divider: '∙', - shortcutbar_divider: '∙', - tab_divider: '|', - tab_overline: '▔', - whitespace: ' ', - }, - tables: Tables { spacing: 2 }, +impl Default for Theme { + fn default() -> Theme { + Theme { + name: String::from("Default"), + icons: Icons { + property_divider: '∙', + shortcutbar_divider: '∙', + tab_divider: '|', + tab_overline: '▔', + whitespace: ' ', + }, + tables: Tables { spacing: 2 }, + border_type: BorderType::Rounded, + } + } +} + +pub mod style { + use tuirealm::props::{Color, Style, TextModifiers}; + + pub fn reset() -> Style { + Style::default().fg(Color::Reset) + } + + pub fn reset_dim() -> Style { + Style::default() + .fg(Color::Reset) + .add_modifier(TextModifiers::DIM) + } + + pub fn red() -> Style { + Style::default().fg(Color::Red) + } + + pub fn green() -> Style { + Style::default().fg(Color::Green) + } + + pub fn yellow() -> Style { + Style::default().fg(Color::Yellow) + } + + pub fn blue() -> Style { + Style::default().fg(Color::Blue) + } + + pub fn magenta() -> Style { + Style::default().fg(Color::Magenta) + } + + pub fn magenta_dim() -> Style { + Style::default() + .fg(Color::Magenta) + .add_modifier(TextModifiers::DIM) + } + + pub fn cyan() -> Style { + Style::default().fg(Color::Cyan) + } + + pub fn lightblue() -> Style { + Style::default().fg(Color::LightBlue) + } + + pub fn gray() -> Style { + Style::default().fg(Color::Gray) + } + + pub fn gray_dim() -> Style { + Style::default() + .fg(Color::Gray) + .add_modifier(TextModifiers::DIM) + } + + pub fn darkgray() -> Style { + Style::default().fg(Color::DarkGray) + } + + pub fn default_reversed() -> Style { + Style::default() + .fg(Color::Reset) + .bg(Color::DarkGray) + .add_modifier(TextModifiers::DIM) + } + + pub fn magenta_reversed() -> Style { + Style::default().fg(Color::DarkGray).bg(Color::Magenta) + } + + pub fn yellow_reversed() -> Style { + Style::default().fg(Color::DarkGray).bg(Color::Yellow) + } + + pub fn border(focus: bool) -> Style { + if focus { + gray_dim() + } else { + darkgray() + } + } + + pub fn highlight() -> Style { + darkgray() } } diff --git a/src/ui/widget/container.rs b/src/ui/widget/container.rs index d5183b25..c0d8190e 100644 --- a/src/ui/widget/container.rs +++ b/src/ui/widget/container.rs @@ -1,5 +1,5 @@ use tuirealm::command::{Cmd, CmdResult}; -use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Props, Style, TextModifiers}; +use tuirealm::props::{AttrValue, Attribute, BorderSides, Props, TextModifiers}; use tuirealm::tui::layout::{Constraint, Direction, Layout, Margin, Rect}; use tuirealm::tui::widgets::{Block, Cell, Clear, Row}; use tuirealm::{Frame, MockComponent, State, StateValue}; @@ -7,7 +7,7 @@ use tuirealm::{Frame, MockComponent, State, StateValue}; use crate::ui::ext::HeaderBlock; use crate::ui::layout; use crate::ui::state::TabState; -use crate::ui::theme::Theme; +use crate::ui::theme::{style, Theme}; use crate::ui::widget::{utils, Widget, WidgetComponent}; use super::label::Label; @@ -72,8 +72,6 @@ impl WidgetComponent for VerticalLine { } } -//////////////////////////////////////////////// - /// A tab header that displays all labels horizontally aligned and separated /// by a divider. Highlights the label defined by the current tab index. #[derive(Clone)] @@ -278,17 +276,11 @@ impl WidgetComponent for Header { .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let color = if focus { - self.theme.colors.container_border_focus_fg - } else { - self.theme.colors.container_border_fg - }; - if display { let block = HeaderBlock::default() .borders(BorderSides::all()) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded); + .border_style(style::border(focus)) + .border_type(self.theme.border_type); frame.render_widget(block, area); let layout = Layout::default() @@ -304,7 +296,7 @@ impl WidgetComponent for Header { .iter() .map(|label| { let cell: Cell = label.into(); - cell.style(Style::default().fg(self.theme.colors.default_fg)) + cell.style(style::reset()) }) .collect::>() .try_into() @@ -348,12 +340,6 @@ impl WidgetComponent for Container { .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let color = if focus { - self.theme.colors.container_border_focus_fg - } else { - self.theme.colors.container_border_fg - }; - if display { // Make some space on the left let layout = Layout::default() @@ -369,8 +355,8 @@ impl WidgetComponent for Container { let block = Block::default() .borders(BorderSides::ALL) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded); + .border_style(style::border(focus)) + .border_type(self.theme.border_type); frame.render_widget(block, area); } } @@ -409,12 +395,6 @@ impl WidgetComponent for LabeledContainer { .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let color = if focus { - self.theme.colors.container_border_focus_fg - } else { - self.theme.colors.container_border_fg - }; - let header_height = self .header .query(Attribute::Height) @@ -432,8 +412,8 @@ impl WidgetComponent for LabeledContainer { let block = Block::default() .borders(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded); + .border_style(style::border(focus)) + .border_type(self.theme.border_type); frame.render_widget(block.clone(), layout[1]); self.component diff --git a/src/ui/widget/context.rs b/src/ui/widget/context.rs index 84b7efe1..d907b4c1 100644 --- a/src/ui/widget/context.rs +++ b/src/ui/widget/context.rs @@ -6,7 +6,7 @@ use tuirealm::{Frame, MockComponent, State}; use super::label::{Label, LabelGroup}; use crate::ui::layout; -use crate::ui::theme::Theme; +use crate::ui::theme::{style, Theme}; use crate::ui::widget::{Widget, WidgetComponent}; pub enum Progress { @@ -128,14 +128,12 @@ pub struct ContextBar { col_2: Widget, col_3: Widget, col_4: Widget, - theme: Theme, } impl ContextBar { pub const PROP_EDIT_MODE: &'static str = "edit-mode"; pub fn new( - theme: Theme, col_0: Widget, col_1: Widget, col_2: Widget, @@ -143,7 +141,6 @@ impl ContextBar { col_4: Widget, ) -> Self { Self { - theme, col_0, col_1, col_2, @@ -173,7 +170,7 @@ impl WidgetComponent for ContextBar { if edit_mode { self.col_0.attr( Attribute::Background, - AttrValue::Color(self.theme.colors.context_badge_edit_bg), + AttrValue::Color(style::yellow_reversed().bg.unwrap()), ) } @@ -211,7 +208,7 @@ impl WidgetComponent for ContextBar { } pub fn bar( - theme: &Theme, + _theme: &Theme, label_0: &str, label_1: &str, label_2: &str, @@ -221,20 +218,20 @@ pub fn bar( use crate::ui::{label, label_group}; let label_0 = label(&format!(" {label_0} ")) - .foreground(theme.colors.context_bg) - .background(theme.colors.context_badge_bg); + .foreground(style::magenta_reversed().fg.unwrap()) + .background(style::magenta_reversed().bg.unwrap()); let label_1 = label(&format!(" {label_1} ")) - .foreground(theme.colors.context_color_fg) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let label_2 = label(&format!(" {label_2} ")) - .foreground(theme.colors.default_fg) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let label_3 = label(&format!(" {label_3} ")) - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let label_4 = label(&format!(" {label_4} ")) - .foreground(theme.colors.context_light) - .background(theme.colors.context_bg); + .foreground(style::default_reversed().fg.unwrap()) + .background(style::default_reversed().bg.unwrap()); let label_0 = label_group(&[label_0]); let label_1 = label_group(&[label_1]); @@ -242,7 +239,7 @@ pub fn bar( let label_3 = label_group(&[label_3]); let label_4 = label_group(&[label_4]); - let context_bar = ContextBar::new(theme.clone(), label_0, label_1, label_2, label_3, label_4); + let context_bar = ContextBar::new(label_0, label_1, label_2, label_3, label_4); Widget::new(context_bar).height(1) } diff --git a/src/ui/widget/form.rs b/src/ui/widget/form.rs index 3f64b77d..269d3a86 100644 --- a/src/ui/widget/form.rs +++ b/src/ui/widget/form.rs @@ -6,7 +6,7 @@ use tuirealm::tui::layout::{Constraint, Direction, Margin, Rect}; use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State, StateValue}; use crate::ui::state::FormState; -use crate::ui::theme::Theme; +use crate::ui::theme::{style, Theme}; use crate::ui::widget::{Widget, WidgetComponent}; use super::container::Container; @@ -24,12 +24,12 @@ impl TextField { .wrap(false) .single_line(true) .cursor_line_style(Style::reset()) - .style(Style::default().fg(theme.colors.default_fg)); + .style(style::reset()); let container = crate::ui::container(&theme, Box::new(input)); Self { input: container, - placeholder: crate::ui::label(title).foreground(theme.colors.input_placeholder_fg), + placeholder: crate::ui::label(title).foreground(style::gray_dim().fg.unwrap()), show_placeholder: true, } } @@ -98,12 +98,12 @@ impl TextArea { .wrap(true) .single_line(false) .cursor_line_style(Style::reset()) - .style(Style::default().fg(theme.colors.default_fg)); + .style(style::reset()); let container = crate::ui::container(&theme, Box::new(input)); Self { input: container, - placeholder: crate::ui::label(title).foreground(theme.colors.input_placeholder_fg), + placeholder: crate::ui::label(title).foreground(style::gray_dim().fg.unwrap()), show_placeholder: true, } } diff --git a/src/ui/widget/label.rs b/src/ui/widget/label.rs index e9fd4285..62480dbb 100644 --- a/src/ui/widget/label.rs +++ b/src/ui/widget/label.rs @@ -5,7 +5,7 @@ use tuirealm::tui::text::{Span, Spans, Text}; use tuirealm::{Frame, MockComponent, State, StateValue}; use crate::ui::layout; -use crate::ui::theme::Theme; +use crate::ui::theme::style; use crate::ui::widget::{Widget, WidgetComponent}; /// A label that can be styled using a foreground color and text modifiers. @@ -123,9 +123,8 @@ impl WidgetComponent for LabelGroup { } } +#[derive(Default)] pub struct Textarea { - /// The current theme. - theme: Theme, /// The scroll offset. offset: usize, /// The current line count. @@ -139,16 +138,6 @@ pub struct Textarea { impl Textarea { pub const PROP_DISPLAY_PROGRESS: &'static str = "display-progress"; - pub fn new(theme: Theme) -> Self { - Self { - theme, - offset: 0, - len: 0, - height: 0, - scroll_percent: 0, - } - } - fn scroll_percent(offset: usize, len: usize, height: usize) -> usize { if height >= len { 100 @@ -170,9 +159,6 @@ impl WidgetComponent for Textarea { let focus = properties .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let fg = properties - .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset)) - .unwrap_color(); let display_progress = properties .get_or( Attribute::Custom(Self::PROP_DISPLAY_PROGRESS), @@ -189,12 +175,6 @@ impl WidgetComponent for Textarea { .constraints([Constraint::Min(1), Constraint::Length(1)]) .split(area); - let highlight_color = if focus { - self.theme.colors.container_border_focus_fg - } else { - self.theme.colors.container_border_fg - }; - // TODO: replace with `ratatui`'s reflow module when that becomes // public: https://github.com/tui-rs-revival/ratatui/pull/9. // @@ -212,7 +192,7 @@ impl WidgetComponent for Textarea { let paragraph = Paragraph::new(body) .scroll((self.offset as u16, 0)) - .style(Style::default().fg(fg)); + .style(style::reset()); frame.render_widget(paragraph, layout[0]); self.scroll_percent = Self::scroll_percent(self.offset, self.len, self.height); @@ -220,7 +200,7 @@ impl WidgetComponent for Textarea { if display_progress { let progress = Spans::from(vec![Span::styled( format!("{} %", self.scroll_percent), - Style::default().fg(highlight_color), + style::border(focus), )]); let progress = Paragraph::new(progress).alignment(Alignment::Right); diff --git a/src/ui/widget/list.rs b/src/ui/widget/list.rs index db7b29cf..dd73d4f9 100644 --- a/src/ui/widget/list.rs +++ b/src/ui/widget/list.rs @@ -1,12 +1,12 @@ use tuirealm::command::{Cmd, CmdResult}; -use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Color, Props, Style}; +use tuirealm::props::{AttrValue, Attribute, BorderSides, Color, Props, Style}; use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect}; use tuirealm::tui::widgets::{Block, Cell, ListState, Row, TableState}; use tuirealm::{Frame, MockComponent, State, StateValue}; use crate::ui::layout; use crate::ui::state::ItemState; -use crate::ui::theme::Theme; +use crate::ui::theme::{style, Theme}; use crate::ui::widget::{utils, Widget, WidgetComponent}; use super::container::Header; @@ -234,12 +234,6 @@ where .get_or(Attribute::Focus, AttrValue::Flag(false)) .unwrap_flag(); - let color = if focus { - self.theme.colors.container_border_focus_fg - } else { - self.theme.colors.container_border_fg - }; - let layout = Layout::default() .direction(Direction::Vertical) .constraints(vec![Constraint::Length(3), Constraint::Min(1)]) @@ -256,8 +250,8 @@ where .block( Block::default() .borders(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT) - .border_style(Style::default().fg(color)) - .border_type(BorderType::Rounded), + .border_style(style::border(focus)) + .border_type(self.theme.border_type), ) .highlight_style(Style::default().bg(highlight)) .column_spacing(self.theme.tables.spacing)