Replaced all list uses with the SelectableList widget and popup widget. Simplified more popups to use the widgets

This commit is contained in:
2024-02-13 16:16:31 -07:00
parent 649f4b5e3b
commit 4b734811f4
7 changed files with 221 additions and 141 deletions
+3 -74
View File
@@ -3,25 +3,23 @@ use std::iter;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text}; use ratatui::text::{Line, Text};
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use ratatui::widgets::Tabs; use ratatui::widgets::Tabs;
use ratatui::widgets::Wrap; use ratatui::widgets::Wrap;
use ratatui::widgets::{Clear, List, ListItem};
use ratatui::Frame; use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::models::stateful_list::StatefulList;
use crate::models::{HorizontallyScrollableText, Route, TabState}; use crate::models::{HorizontallyScrollableText, Route, TabState};
use crate::ui::radarr_ui::RadarrUi; use crate::ui::radarr_ui::RadarrUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{ use crate::ui::utils::{
background_block, borderless_block, centered_rect, layout_block_top_border, background_block, borderless_block, centered_rect, layout_paragraph_borderless, logo_block,
layout_paragraph_borderless, logo_block, title_block, title_block_centered, title_block, title_block_centered,
}; };
use crate::ui::widgets::button::Button; use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox; use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox; use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::loading_block::LoadingBlock;
mod radarr_ui; mod radarr_ui;
mod styles; mod styles;
@@ -238,14 +236,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
content_area content_area
} }
pub struct ListProps<'a, T> {
pub content: &'a mut StatefulList<T>,
pub title: &'static str,
pub is_loading: bool,
pub is_popup: bool,
pub help: Option<String>,
}
pub fn draw_prompt_box( pub fn draw_prompt_box(
f: &mut Frame<'_>, f: &mut Frame<'_>,
area: Rect, area: Rect,
@@ -351,67 +341,6 @@ pub fn draw_prompt_box_with_checkboxes(
f.render_widget(no_button, no_area); f.render_widget(no_button, no_area);
} }
pub fn draw_list_box<'a, T>(
f: &mut Frame<'_>,
area: Rect,
item_mapper: impl Fn(&T) -> ListItem<'a>,
list_props: ListProps<'a, T>,
) {
let ListProps {
content,
title,
is_loading,
is_popup,
help,
} = list_props;
let (content_area, block) = if is_popup {
f.render_widget(title_block(title), area);
(
draw_help_footer_and_get_content_area(f, area, help),
borderless_block(),
)
} else {
(area, title_block(title))
};
if !content.items.is_empty() {
let items: Vec<ListItem<'_>> = content.items.iter().map(item_mapper).collect();
let mut list = List::new(items).block(block);
if is_popup {
list = list.highlight_style(Style::new().highlight());
}
f.render_stateful_widget(list, content_area, &mut content.state);
} else {
f.render_widget(LoadingBlock::new(is_loading, block), content_area);
}
}
fn draw_help_footer_and_get_content_area(
f: &mut Frame<'_>,
area: Rect,
help: Option<String>,
) -> Rect {
if let Some(help_string) = help {
let [content_area, help_footer_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)])
.margin(1)
.areas(area);
let help_paragraph = Paragraph::new(Text::from(format!(" {help_string}").help()))
.block(layout_block_top_border())
.alignment(Alignment::Left);
f.render_widget(help_paragraph, help_footer_area);
content_area
} else {
area
}
}
pub fn draw_input_box_popup( pub fn draw_input_box_popup(
f: &mut Frame<'_>, f: &mut Frame<'_>,
area: Rect, area: Rect,
+20 -18
View File
@@ -2,6 +2,7 @@ use std::ops::Sub;
use chrono::Utc; use chrono::Utc;
use ratatui::layout::{Alignment, Layout}; use ratatui::layout::{Alignment, Layout};
use ratatui::style::Style;
use ratatui::text::{Span, Text}; use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, Paragraph, Row}; use ratatui::widgets::{Cell, Paragraph, Row};
use ratatui::{ use ratatui::{
@@ -17,11 +18,12 @@ use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style
use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi; use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi;
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border; use crate::ui::utils::layout_block_top_border;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::ListProps; use crate::ui::widgets::selectable_list::SelectableList;
use crate::{ use crate::{
models::Route, models::Route,
ui::{draw_list_box, utils::title_block, DrawUi}, ui::{utils::title_block, DrawUi},
}; };
mod system_details_ui; mod system_details_ui;
@@ -168,23 +170,23 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec
} }
fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_list_box( let block = title_block("Logs");
f,
area,
|log| {
let log_line = log.to_string();
let level = log_line.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level) if app.data.radarr_data.logs.items.is_empty() {
}, f.render_widget(LoadingBlock::new(app.is_loading, block), area);
ListProps { return;
content: &mut app.data.radarr_data.logs, }
title: "Logs",
is_loading: app.is_loading, let logs_box = SelectableList::new(&mut app.data.radarr_data.logs, |log| {
is_popup: false, let log_line = log.to_string();
help: None, let level = log_line.split('|').collect::<Vec<&str>>()[1].to_string();
},
); style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
})
.block(block)
.highlight_style(Style::new().default());
f.render_widget(logs_box, area);
} }
fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_help(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+51 -38
View File
@@ -18,9 +18,10 @@ use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, title_block}; use crate::ui::utils::{borderless_block, title_block};
use crate::ui::widgets::loading_block::LoadingBlock; use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::popup::Popup;
use crate::ui::widgets::selectable_list::SelectableList;
use crate::ui::{ use crate::ui::{
draw_help_footer_and_get_content_area, draw_large_popup_over, draw_list_box, draw_large_popup_over, draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi,
draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, ListProps,
}; };
#[cfg(test)] #[cfg(test)]
@@ -42,7 +43,8 @@ impl DrawUi for SystemDetailsUi {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
match active_radarr_block { match active_radarr_block {
ActiveRadarrBlock::SystemLogs => { ActiveRadarrBlock::SystemLogs => {
draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_logs_popup) draw_system_ui_layout(f, app, area);
draw_logs_popup(f, app);
} }
ActiveRadarrBlock::SystemTasks | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => { ActiveRadarrBlock::SystemTasks | ActiveRadarrBlock::SystemTaskStartConfirmPrompt => {
draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_tasks_popup) draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_tasks_popup)
@@ -51,7 +53,8 @@ impl DrawUi for SystemDetailsUi {
draw_medium_popup_over(f, app, area, draw_system_ui_layout, draw_queued_events) draw_medium_popup_over(f, app, area, draw_system_ui_layout, draw_queued_events)
} }
ActiveRadarrBlock::SystemUpdates => { ActiveRadarrBlock::SystemUpdates => {
draw_large_popup_over(f, app, area, draw_system_ui_layout, draw_updates_popup) draw_system_ui_layout(f, app, area);
draw_updates_popup(f, app);
} }
_ => (), _ => (),
} }
@@ -59,27 +62,35 @@ impl DrawUi for SystemDetailsUi {
} }
} }
fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
draw_list_box( let block = title_block("Log Details");
f, let help_footer = format!(
area, "<↑↓←→> scroll | {}",
|log| { build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)
let log_line = log.to_string();
let level = log.text.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
},
ListProps {
content: &mut app.data.radarr_data.log_details,
title: "Log Details",
is_loading: app.is_loading,
is_popup: true,
help: Some(format!(
"<↑↓←→> scroll | {}",
build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)
)),
},
); );
if app.data.radarr_data.log_details.items.is_empty() {
let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading, 75, 75)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.size());
return;
}
let logs_list = SelectableList::new(&mut app.data.radarr_data.log_details, |log| {
let log_line = log.to_string();
let level = log.text.split('|').collect::<Vec<&str>>()[1].to_string();
style_log_list_item(ListItem::new(Text::from(Span::raw(log_line))), level)
})
.block(borderless_block());
let popup = Popup::new(logs_list, 75, 75)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.size());
} }
fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
@@ -134,27 +145,29 @@ fn draw_start_task_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
); );
} }
fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
f.render_widget(title_block("Updates"), area); let help_footer = format!(
"<↑↓> scroll | {}",
let content_area = draw_help_footer_and_get_content_area( build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)
f,
area,
Some(format!(
"<↑↓> scroll | {}",
build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)
)),
); );
let updates = app.data.radarr_data.updates.get_text(); let updates = app.data.radarr_data.updates.get_text();
let block = borderless_block(); let block = title_block("Updates");
if !updates.is_empty() { if !updates.is_empty() {
let updates_paragraph = Paragraph::new(Text::from(updates)) let updates_paragraph = Paragraph::new(Text::from(updates))
.block(block) .block(borderless_block())
.scroll((app.data.radarr_data.updates.offset, 0)); .scroll((app.data.radarr_data.updates.offset, 0));
let popup = Popup::new(updates_paragraph, 75, 75)
.block(block)
.footer(&help_footer);
f.render_widget(updates_paragraph, content_area); f.render_widget(popup, f.size());
} else { } else {
f.render_widget(LoadingBlock::new(app.is_loading, block), content_area); let loading = LoadingBlock::new(app.is_loading, borderless_block());
let popup = Popup::new(loading, 75, 75)
.block(block)
.footer(&help_footer);
f.render_widget(popup, f.size());
} }
} }
+51 -8
View File
@@ -1,37 +1,80 @@
use crate::ui::utils::{background_block, centered_rect}; use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{background_block, centered_rect, layout_block_top_border};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::widgets::{Clear, Widget}; use ratatui::prelude::Text;
use ratatui::widgets::{Block, Clear, Paragraph, Widget};
#[cfg(test)] #[cfg(test)]
#[path = "popup_tests.rs"] #[path = "popup_tests.rs"]
mod popup_tests; mod popup_tests;
pub struct Popup<T: Widget> { pub struct Popup<'a, T: Widget> {
widget: T, widget: T,
percent_x: u16, percent_x: u16,
percent_y: u16, percent_y: u16,
block: Option<Block<'a>>,
footer: Option<&'a str>,
footer_alignment: Alignment,
} }
impl<T: Widget> Popup<T> { impl<'a, T: Widget> Popup<'a, T> {
pub fn new(widget: T, percent_x: u16, percent_y: u16) -> Self { pub fn new(widget: T, percent_x: u16, percent_y: u16) -> Self {
Self { Self {
widget, widget,
percent_x, percent_x,
percent_y, percent_y,
block: None,
footer: None,
footer_alignment: Alignment::Left,
} }
} }
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn footer(mut self, footer: &'a str) -> Self {
self.footer = Some(footer);
self
}
pub fn footer_alignment(mut self, alignment: Alignment) -> Self {
self.footer_alignment = alignment;
self
}
fn render_popup(self, area: Rect, buf: &mut Buffer) { fn render_popup(self, area: Rect, buf: &mut Buffer) {
let popup_area = centered_rect(self.percent_x, self.percent_y, area); let popup_area = centered_rect(self.percent_x, self.percent_y, area);
Clear.render(popup_area, buf); Clear.render(popup_area, buf);
background_block().render(popup_area, buf); background_block().render(popup_area, buf);
self.widget.render(popup_area, buf);
if let Some(block) = self.block {
block.render(popup_area, buf);
}
let content_area = if let Some(footer) = self.footer {
let [content_area, help_footer_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)])
.margin(1)
.areas(popup_area);
Paragraph::new(Text::from(format!(" {footer}").help()))
.block(layout_block_top_border())
.alignment(self.footer_alignment)
.render(help_footer_area, buf);
content_area
} else {
popup_area
};
self.widget.render(content_area, buf);
} }
} }
impl<T: Widget> Widget for Popup<T> { impl<'a, T: Widget> Widget for Popup<'a, T> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_popup(area, buf); self.render_popup(area, buf);
} }
+40
View File
@@ -2,6 +2,7 @@
mod tests { mod tests {
use crate::ui::widgets::popup::Popup; use crate::ui::widgets::popup::Popup;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::layout::Alignment;
use ratatui::widgets::Block; use ratatui::widgets::Block;
#[test] #[test]
@@ -11,5 +12,44 @@ mod tests {
assert_eq!(popup.widget, Block::new()); assert_eq!(popup.widget, Block::new());
assert_eq!(popup.percent_x, 50); assert_eq!(popup.percent_x, 50);
assert_eq!(popup.percent_y, 50); assert_eq!(popup.percent_y, 50);
assert_eq!(popup.block, None);
assert_eq!(popup.footer, None);
assert_eq!(popup.footer_alignment, Alignment::Left);
}
#[test]
fn test_popup_block() {
let popup = Popup::new(Block::new(), 50, 50).block(Block::new());
assert_eq!(popup.block, Some(Block::new()));
assert_eq!(popup.widget, Block::new());
assert_eq!(popup.percent_x, 50);
assert_eq!(popup.percent_y, 50);
assert_eq!(popup.footer, None);
assert_eq!(popup.footer_alignment, Alignment::Left);
}
#[test]
fn test_popup_footer() {
let popup = Popup::new(Block::new(), 50, 50).footer("footer");
assert_eq!(popup.footer, Some("footer"));
assert_eq!(popup.widget, Block::new());
assert_eq!(popup.percent_x, 50);
assert_eq!(popup.percent_y, 50);
assert_eq!(popup.block, None);
assert_eq!(popup.footer_alignment, Alignment::Left);
}
#[test]
fn test_popup_footer_alignment() {
let popup = Popup::new(Block::new(), 50, 50).footer_alignment(Alignment::Center);
assert_eq!(popup.footer_alignment, Alignment::Center);
assert_eq!(popup.widget, Block::new());
assert_eq!(popup.percent_x, 50);
assert_eq!(popup.percent_y, 50);
assert_eq!(popup.block, None);
assert_eq!(popup.footer, None);
} }
} }
+17 -3
View File
@@ -5,7 +5,7 @@ use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::prelude::Widget; use ratatui::prelude::Widget;
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::widgets::{List, ListItem, StatefulWidget}; use ratatui::widgets::{Block, List, ListItem, StatefulWidget};
#[cfg(test)] #[cfg(test)]
#[path = "selectable_list_tests.rs"] #[path = "selectable_list_tests.rs"]
@@ -17,6 +17,8 @@ where
{ {
content: &'a mut StatefulList<T>, content: &'a mut StatefulList<T>,
row_mapper: F, row_mapper: F,
highlight_style: Style,
block: Block<'a>,
} }
impl<'a, T, F> SelectableList<'a, T, F> impl<'a, T, F> SelectableList<'a, T, F>
@@ -27,15 +29,27 @@ where
Self { Self {
content, content,
row_mapper, row_mapper,
highlight_style: Style::new().highlight(),
block: layout_block(),
} }
} }
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = block;
self
}
fn render_list(self, area: Rect, buf: &mut Buffer) { fn render_list(self, area: Rect, buf: &mut Buffer) {
let items: Vec<ListItem<'_>> = self.content.items.iter().map(&self.row_mapper).collect(); let items: Vec<ListItem<'_>> = self.content.items.iter().map(&self.row_mapper).collect();
let selectable_list = List::new(items) let selectable_list = List::new(items)
.block(layout_block()) .block(self.block)
.highlight_style(Style::new().highlight()); .highlight_style(self.highlight_style);
StatefulWidget::render(selectable_list, area, buf, &mut self.content.state); StatefulWidget::render(selectable_list, area, buf, &mut self.content.state);
} }
+39
View File
@@ -1,8 +1,11 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::stateful_list::StatefulList; use crate::models::stateful_list::StatefulList;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{layout_block, title_block};
use crate::ui::widgets::selectable_list::SelectableList; use crate::ui::widgets::selectable_list::SelectableList;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::style::{Style, Stylize};
use ratatui::widgets::ListItem; use ratatui::widgets::ListItem;
#[test] #[test]
@@ -17,5 +20,41 @@ mod tests {
let row_mapper = selectable_list.row_mapper; let row_mapper = selectable_list.row_mapper;
assert_eq!(selectable_list.content.items, items); assert_eq!(selectable_list.content.items, items);
assert_eq!(row_mapper(&"test"), ListItem::new("test")); assert_eq!(row_mapper(&"test"), ListItem::new("test"));
assert_eq!(selectable_list.highlight_style, Style::new().highlight());
assert_eq!(selectable_list.block, layout_block());
}
#[test]
fn test_selectable_list_highlight_style() {
let items = vec!["test"];
let mut stateful_list = StatefulList::default();
stateful_list.set_items(items.clone());
let selectable_list =
SelectableList::new(&mut stateful_list, |item| ListItem::new(item.to_string()))
.highlight_style(Style::new().bold());
let row_mapper = selectable_list.row_mapper;
assert_eq!(selectable_list.highlight_style, Style::new().bold());
assert_eq!(selectable_list.content.items, items);
assert_eq!(row_mapper(&"test"), ListItem::new("test"));
assert_eq!(selectable_list.block, layout_block());
}
#[test]
fn test_selectable_list_block() {
let items = vec!["test"];
let mut stateful_list = StatefulList::default();
stateful_list.set_items(items.clone());
let selectable_list =
SelectableList::new(&mut stateful_list, |item| ListItem::new(item.to_string()))
.block(title_block("test"));
let row_mapper = selectable_list.row_mapper;
assert_eq!(selectable_list.block, title_block("test"));
assert_eq!(selectable_list.content.items, items);
assert_eq!(row_mapper(&"test"), ListItem::new("test"));
assert_eq!(selectable_list.highlight_style, Style::new().highlight());
} }
} }