diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1a15aa4..700c1b5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,7 +1,7 @@ use std::iter; use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; -use ratatui::style::{Modifier, Style, Stylize}; +use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Text}; use ratatui::widgets::Paragraph; use ratatui::widgets::Row; @@ -17,13 +17,16 @@ use crate::ui::radarr_ui::RadarrUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ background_block, borderless_block, centered_rect, layout_block, layout_block_top_border, - layout_button_paragraph, layout_button_paragraph_borderless, layout_paragraph_borderless, - logo_block, show_cursor, style_block_highlight, title_block, title_block_centered, + layout_paragraph_borderless, logo_block, title_block, title_block_centered, }; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; mod radarr_ui; mod styles; mod utils; +mod widgets; static HIGHLIGHT_SYMBOL: &str = "=> "; @@ -428,8 +431,11 @@ pub fn draw_prompt_box_with_content( Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - draw_button(f, yes_area, "Yes", yes_no_value); - draw_button(f, no_area, "No", !yes_no_value); + let yes_button = Button::new().title("Yes").selected(yes_no_value); + let no_button = Button::new().title("No").selected(!yes_no_value); + + f.render_widget(yes_button, yes_area); + f.render_widget(no_button, no_area); } pub fn draw_prompt_box_with_checkboxes( @@ -441,115 +447,41 @@ pub fn draw_prompt_box_with_checkboxes( highlight_yes_no: bool, yes_no_value: bool, ) { - f.render_widget(title_block_centered(title), area); let mut constraints = vec![ Constraint::Length(4), Constraint::Fill(0), Constraint::Length(3), ]; - constraints.splice( 1..1, iter::repeat(Constraint::Length(3)).take(checkboxes.len()), ); - let chunks = Layout::vertical(constraints).margin(1).split(area); - let prompt_paragraph = layout_paragraph_borderless(prompt); - f.render_widget(prompt_paragraph, chunks[0]); for i in 0..checkboxes.len() { - let (label, is_checked, is_selected) = checkboxes[i]; - draw_checkbox_with_label(f, chunks[i + 1], label, is_checked, is_selected); + let (label, is_checked, is_highlighted) = checkboxes[i]; + let checkbox = Checkbox::new(label) + .checked(is_checked) + .highlighted(is_highlighted); + f.render_widget(checkbox, chunks[i + 1]); } let [yes_area, no_area] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(chunks[checkboxes.len() + 2]); - draw_button(f, yes_area, "Yes", highlight_yes_no && yes_no_value); - draw_button(f, no_area, "No", highlight_yes_no && !yes_no_value); -} + let yes_button = Button::new() + .title("Yes") + .selected(yes_no_value && highlight_yes_no); + let no_button = Button::new() + .title("No") + .selected(!yes_no_value && highlight_yes_no); -pub fn draw_checkbox(f: &mut Frame<'_>, area: Rect, is_checked: bool, is_selected: bool) { - let check = if is_checked { "✔" } else { "" }; - let label_paragraph = Paragraph::new(Text::from(check)) - .block(layout_block()) - .alignment(Alignment::Center) - .style(style_block_highlight(is_selected).add_modifier(Modifier::BOLD)); - let checkbox_area = Rect { width: 5, ..area }; - - f.render_widget(label_paragraph, checkbox_area); -} - -pub fn draw_checkbox_with_label( - f: &mut Frame<'_>, - area: Rect, - label: &str, - is_checked: bool, - is_selected: bool, -) { - let [label_area, checkbox_area] = - Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); - - let label_paragraph = Paragraph::new(Text::from(format!("\n{label}: "))) - .block(borderless_block()) - .alignment(Alignment::Right) - .primary(); - - f.render_widget(label_paragraph, label_area); - - draw_checkbox(f, checkbox_area, is_checked, is_selected); -} - -pub fn draw_button(f: &mut Frame<'_>, area: Rect, label: &str, is_selected: bool) { - let label_paragraph = layout_button_paragraph(is_selected, label, Alignment::Center); - - f.render_widget(label_paragraph, area); -} - -pub fn draw_button_with_icon( - f: &mut Frame<'_>, - area: Rect, - label: &str, - icon: &str, - is_selected: bool, -) { - let label_paragraph = layout_button_paragraph_borderless(is_selected, label, Alignment::Left); - let icon_paragraph = layout_button_paragraph_borderless(is_selected, icon, Alignment::Right); - - let [label_area, icon_area] = - Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) - .flex(Flex::SpaceBetween) - .margin(1) - .areas(area); - - f.render_widget( - layout_block().style(style_block_highlight(is_selected)), - area, - ); - f.render_widget(label_paragraph, label_area); - f.render_widget(icon_paragraph, icon_area); -} - -pub fn draw_drop_down_menu_button( - f: &mut Frame<'_>, - area: Rect, - description: &str, - selection: &str, - is_selected: bool, -) { - let [label_area, button_area] = - Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); - - let description_paragraph = Paragraph::new(Text::from(format!("\n{description}: "))) - .block(borderless_block()) - .alignment(Alignment::Right) - .primary(); - - f.render_widget(description_paragraph, label_area); - - draw_button_with_icon(f, button_area, selection, "▼ ", is_selected); + f.render_widget(title_block_centered(title), area); + f.render_widget(prompt_paragraph, chunks[0]); + f.render_widget(yes_button, yes_area); + f.render_widget(no_button, no_area); } pub fn draw_selectable_list<'a, T>( @@ -625,95 +557,6 @@ fn draw_help_footer_and_get_content_area( } } -pub struct TextBoxProps<'a> { - pub text_box_area: Rect, - pub block_title: Option<&'a str>, - pub block_content: &'a str, - pub offset: usize, - pub should_show_cursor: bool, - pub is_selected: bool, - pub cursor_after_string: bool, -} - -pub fn draw_text_box(f: &mut Frame<'_>, text_box_props: TextBoxProps<'_>) { - let TextBoxProps { - text_box_area, - block_title, - block_content, - offset, - should_show_cursor, - is_selected, - cursor_after_string, - } = text_box_props; - let (block, style) = if let Some(title) = block_title { - (title_block_centered(title), Style::new().default()) - } else { - ( - layout_block(), - if should_show_cursor { - Style::new().default() - } else { - style_block_highlight(is_selected) - }, - ) - }; - let paragraph = Paragraph::new(Text::from(block_content)) - .style(style) - .block(block); - f.render_widget(paragraph, text_box_area); - - if should_show_cursor { - show_cursor(f, text_box_area, offset, block_content, cursor_after_string); - } -} - -pub struct LabeledTextBoxProps<'a> { - pub area: Rect, - pub label: &'a str, - pub text: &'a str, - pub offset: usize, - pub is_selected: bool, - pub should_show_cursor: bool, - pub cursor_after_string: bool, -} - -pub fn draw_text_box_with_label( - f: &mut Frame<'_>, - labeled_text_box_props: LabeledTextBoxProps<'_>, -) { - let LabeledTextBoxProps { - area, - label, - text, - offset, - is_selected, - should_show_cursor, - cursor_after_string, - } = labeled_text_box_props; - let [label_area, text_box_area] = - Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); - - let label_paragraph = Paragraph::new(Text::from(format!("\n{label}: "))) - .block(borderless_block()) - .alignment(Alignment::Right) - .primary(); - - f.render_widget(label_paragraph, label_area); - - draw_text_box( - f, - TextBoxProps { - text_box_area, - block_title: None, - block_content: text, - offset, - should_show_cursor, - is_selected, - cursor_after_string, - }, - ); -} - pub fn draw_input_box_popup( f: &mut Frame<'_>, area: Rect, @@ -724,18 +567,12 @@ pub fn draw_input_box_popup( .margin(1) .areas(area); - draw_text_box( - f, - TextBoxProps { - text_box_area, - block_title: Some(box_title), - block_content: &box_content.text, - offset: *box_content.offset.borrow(), - should_show_cursor: true, - is_selected: false, - cursor_after_string: true, - }, - ); + let input_box = InputBox::new(&box_content.text) + .offset(*box_content.offset.borrow()) + .block(title_block_centered(box_title)); + + input_box.show_cursor(f, text_box_area); + f.render_widget(input_box, text_box_area); let help = Paragraph::new(" cancel") .help() diff --git a/src/ui/radarr_ui/collections/edit_collection_ui.rs b/src/ui/radarr_ui/collections/edit_collection_ui.rs index 42b3b47..8c888ed 100644 --- a/src/ui/radarr_ui/collections/edit_collection_ui.rs +++ b/src/ui/radarr_ui/collections/edit_collection_ui.rs @@ -8,13 +8,17 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, COLLECTION_DETAILS_BLOCKS, EDIT_COLLECTION_BLOCKS, }; use crate::models::Route; +use crate::render_selectable_input_box; use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsUi; use crate::ui::radarr_ui::collections::draw_collections; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; use crate::ui::{ - draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, - draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, - draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, + draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, + draw_popup, draw_selectable_list, DrawUi, }; #[cfg(test)] @@ -126,12 +130,9 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> search_on_add, path, } = app.data.radarr_data.edit_collection_modal.as_ref().unwrap(); - let selected_minimum_availability = minimum_availability_list.current_selection(); let selected_quality_profile = quality_profile_list.current_selection(); - f.render_widget(title_block_centered(&title), area); - let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, root_folder_area, search_on_add_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), @@ -145,63 +146,52 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_> ]) .margin(1) .areas(area); - - let prompt_paragraph = layout_paragraph_borderless(&collection_overview); - f.render_widget(prompt_paragraph, paragraph_area); - let [save_area, cancel_area] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - draw_checkbox_with_label( - f, - monitored_area, - "Monitored", - monitored.unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditCollectionToggleMonitored, - ); - - draw_drop_down_menu_button( - f, - min_availability_area, - "Minimum Availability", - selected_minimum_availability.to_display_str(), - selected_block == &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability, - ); - draw_drop_down_menu_button( - f, - quality_profile_area, - "Quality Profile", - selected_quality_profile, - selected_block == &ActiveRadarrBlock::EditCollectionSelectQualityProfile, - ); + let prompt_paragraph = layout_paragraph_borderless(&collection_overview); + let monitored_checkbox = Checkbox::new("Monitored") + .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleMonitored) + .checked(monitored.unwrap_or_default()); + let min_availability_drop_down_button = Button::new() + .title(selected_minimum_availability.to_display_str()) + .label("Minimum Availability") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::EditCollectionSelectMinimumAvailability); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::EditCollectionSelectQualityProfile); if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: root_folder_area, - label: "Root Folder", - text: &path.text, - offset: *path.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::EditCollectionRootFolderPathInput, - cursor_after_string: true, - }, - ); + let root_folder_input_box = InputBox::new(&path.text) + .offset(*path.offset.borrow()) + .label("Root Folder") + .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionRootFolderPathInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditCollectionRootFolderPathInput); + render_selectable_input_box!(root_folder_input_box, f, root_folder_area); } - draw_checkbox_with_label( - f, - search_on_add_area, - "Search on Add", - search_on_add.unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditCollectionToggleSearchOnAdd, - ); + let search_on_add_checkbox = Checkbox::new("Search on Add") + .highlighted(selected_block == &ActiveRadarrBlock::EditCollectionToggleSearchOnAdd) + .checked(search_on_add.unwrap_or_default()); + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); - draw_button(f, save_area, "Save", yes_no_value && highlight_yes_no); - draw_button(f, cancel_area, "Cancel", !yes_no_value && highlight_yes_no); + f.render_widget(title_block_centered(&title), area); + f.render_widget(prompt_paragraph, paragraph_area); + f.render_widget(monitored_checkbox, monitored_area); + f.render_widget(min_availability_drop_down_button, min_availability_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(search_on_add_checkbox, search_on_add_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); } fn draw_edit_collection_select_minimum_availability_popup( diff --git a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs index 3e4b023..2268b28 100644 --- a/src/ui/radarr_ui/indexers/edit_indexer_ui.rs +++ b/src/ui/radarr_ui/indexers/edit_indexer_ui.rs @@ -1,12 +1,14 @@ use crate::app::App; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS}; use crate::models::Route; +use crate::render_selectable_input_box; use crate::ui::radarr_ui::indexers::draw_indexers; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; -use crate::ui::{ - draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, - DrawUi, LabeledTextBoxProps, -}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::{draw_popup_over, loading, DrawUi}; use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::Frame; @@ -48,19 +50,16 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if edit_indexer_modal_option.is_some() { let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap(); - f.render_widget(block, area); let [settings_area, buttons_area] = Layout::vertical([Constraint::Fill(0), Constraint::Length(3)]) .margin(1) .areas(area); - let [left_side_area, right_side_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) .areas(settings_area); - - let [name, rss, auto_search, interactive_search, _] = Layout::vertical([ + let [name_area, rss_area, auto_search_area, interactive_search_area, _] = Layout::vertical([ Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), @@ -78,115 +77,87 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .areas(right_side_area); if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: name, - label: "Name", - text: &edit_indexer_modal.name.text, - offset: *edit_indexer_modal.name.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerNameInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerNameInput, - cursor_after_string: true, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: url_area, - label: "URL", - text: &edit_indexer_modal.url.text, - offset: *edit_indexer_modal.url.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerUrlInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerUrlInput, - cursor_after_string: true, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: api_key_area, - label: "API Key", - text: &edit_indexer_modal.api_key.text, - offset: *edit_indexer_modal.api_key.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerApiKeyInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerApiKeyInput, - cursor_after_string: true, - }, - ); + let name_input_box = InputBox::new(&edit_indexer_modal.name.text) + .offset(*edit_indexer_modal.name.offset.borrow()) + .label("Name") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerNameInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerNameInput); + let url_input_box = InputBox::new(&edit_indexer_modal.url.text) + .offset(*edit_indexer_modal.url.offset.borrow()) + .label("URL") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerUrlInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerUrlInput); + let api_key_input_box = InputBox::new(&edit_indexer_modal.api_key.text) + .offset(*edit_indexer_modal.api_key.offset.borrow()) + .label("API Key") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerApiKeyInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerApiKeyInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(*edit_indexer_modal.tags.offset.borrow()) + .label("Tags") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput); + + render_selectable_input_box!(name_input_box, f, name_area); + render_selectable_input_box!(url_input_box, f, url_area); + render_selectable_input_box!(api_key_input_box, f, api_key_area); + if protocol == "torrent" { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: seed_ratio_area, - label: "Seed Ratio", - text: &edit_indexer_modal.seed_ratio.text, - offset: *edit_indexer_modal.seed_ratio.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerSeedRatioInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerSeedRatioInput, - cursor_after_string: true, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: tags_area, - label: "Tags", - text: &edit_indexer_modal.tags.text, - offset: *edit_indexer_modal.tags.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerTagsInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput, - cursor_after_string: true, - }, - ); + let seed_ratio_input_box = InputBox::new(&edit_indexer_modal.seed_ratio.text) + .offset(*edit_indexer_modal.seed_ratio.offset.borrow()) + .label("Seed Ratio") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerSeedRatioInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerSeedRatioInput); + let tags_input_box = InputBox::new(&edit_indexer_modal.tags.text) + .offset(*edit_indexer_modal.tags.offset.borrow()) + .label("Tags") + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerTagsInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput); + + render_selectable_input_box!(seed_ratio_input_box, f, seed_ratio_area); + render_selectable_input_box!(tags_input_box, f, tags_area); } else { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: seed_ratio_area, - label: "Tags", - text: &edit_indexer_modal.tags.text, - offset: *edit_indexer_modal.tags.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditIndexerTagsInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditIndexerTagsInput, - cursor_after_string: true, - }, - ); + render_selectable_input_box!(tags_input_box, f, seed_ratio_area); } - draw_checkbox_with_label( - f, - rss, - "Enable RSS", - edit_indexer_modal.enable_rss.unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableRss, - ); - draw_checkbox_with_label( - f, - auto_search, - "Enable Automatic Search", - edit_indexer_modal - .enable_automatic_search - .unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch, - ); - draw_checkbox_with_label( - f, - interactive_search, - "Enable Interactive Search", - edit_indexer_modal - .enable_interactive_search - .unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, - ); + let rss_checkbox = Checkbox::new("Enable RSS") + .checked(edit_indexer_modal.enable_rss.unwrap_or_default()) + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableRss); + let auto_search_checkbox = Checkbox::new("Enable Automatic Search") + .checked( + edit_indexer_modal + .enable_automatic_search + .unwrap_or_default(), + ) + .highlighted(selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableAutomaticSearch); + let interactive_search_checkbox = Checkbox::new("Enable Interactive Search") + .checked( + edit_indexer_modal + .enable_interactive_search + .unwrap_or_default(), + ) + .highlighted( + selected_block == &ActiveRadarrBlock::EditIndexerToggleEnableInteractiveSearch, + ); let [save_area, cancel_area] = Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) .flex(Flex::Center) .areas(buttons_area); - draw_button(f, save_area, "Save", yes_no_value && highlight_yes_no); - draw_button(f, cancel_area, "Cancel", !yes_no_value && highlight_yes_no); + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(block, area); + f.render_widget(rss_checkbox, rss_area); + f.render_widget(auto_search_checkbox, auto_search_area); + f.render_widget(interactive_search_checkbox, interactive_search_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); } } else { loading(f, block, area, app.is_loading); diff --git a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs index 21009a4..384782c 100644 --- a/src/ui/radarr_ui/indexers/indexer_settings_ui.rs +++ b/src/ui/radarr_ui/indexers/indexer_settings_ui.rs @@ -6,12 +6,14 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS, }; use crate::models::Route; +use crate::render_selectable_input_box; use crate::ui::radarr_ui::indexers::draw_indexers; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::title_block_centered; -use crate::ui::{ - draw_button, draw_checkbox_with_label, draw_popup_over, draw_text_box_with_label, loading, - DrawUi, LabeledTextBoxProps, -}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; +use crate::ui::{draw_popup_over, loading, DrawUi}; #[cfg(test)] #[path = "indexer_settings_ui_tests.rs"] @@ -50,18 +52,15 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: if indexer_settings_option.is_some() { let indexer_settings = indexer_settings_option.as_ref().unwrap(); - f.render_widget(block, area); let [settings_area, buttons_area] = Layout::vertical([Constraint::Fill(0), Constraint::Length(3)]) .margin(1) .areas(area); - let [left_side_area, right_side_area] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .margin(1) .areas(settings_area); - let [min_age_area, retention_area, max_size_area, prefer_flags_area, _] = Layout::vertical([ Constraint::Length(3), Constraint::Length(3), @@ -81,110 +80,80 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: .areas(right_side_area); if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: min_age_area, - label: "Minimum Age (minutes) ▴▾", - text: &indexer_settings.minimum_age.to_string(), - offset: 0, - is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput, - cursor_after_string: false, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: retention_area, - label: "Retention (days) ▴▾", - text: &indexer_settings.retention.to_string(), - offset: 0, - is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsRetentionInput, - cursor_after_string: false, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: max_size_area, - label: "Maximum Size (MB) ▴▾", - text: &indexer_settings.maximum_size.to_string(), - offset: 0, - is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput, - cursor_after_string: false, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: availability_delay_area, - label: "Availability Delay (days) ▴▾", - text: &indexer_settings.availability_delay.to_string(), - offset: 0, - is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput, - cursor_after_string: false, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: rss_sync_interval_area, - label: "RSS Sync Interval (minutes) ▴▾", - text: &indexer_settings.rss_sync_interval.to_string(), - offset: 0, - is_selected: selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput, - cursor_after_string: false, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: whitelisted_sub_tags_area, - label: "Whitelisted Subtitle Tags", - text: &indexer_settings.whitelisted_hardcoded_subs.text, - offset: *indexer_settings.whitelisted_hardcoded_subs.offset.borrow(), - is_selected: selected_block - == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - should_show_cursor: active_radarr_block - == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, - cursor_after_string: true, - }, - ); + let min_age = indexer_settings.minimum_age.to_string(); + let retention = indexer_settings.retention.to_string(); + let max_size = indexer_settings.maximum_size.to_string(); + let availability_delay = indexer_settings.availability_delay.to_string(); + let rss_sync_interval = indexer_settings.rss_sync_interval.to_string(); + + let min_age_text_box = InputBox::new(&min_age) + .cursor_after_string(false) + .label("Minimum Age (minutes) ▴▾") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsMinimumAgeInput) + .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsMinimumAgeInput); + let retention_input_box = InputBox::new(&retention) + .cursor_after_string(false) + .label("Retention (days) ▴▾") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsRetentionInput) + .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsRetentionInput); + let max_size_input_box = InputBox::new(&max_size) + .cursor_after_string(false) + .label("Maximum Size (MB) ▴▾") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsMaximumSizeInput) + .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsMaximumSizeInput); + let availability_delay_input_box = InputBox::new(&availability_delay) + .cursor_after_string(false) + .label("Availability Delay (days) ▴▾") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput) + .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsAvailabilityDelayInput); + let rss_sync_interval_input_box = InputBox::new(&rss_sync_interval) + .cursor_after_string(false) + .label("RSS Sync Interval (minutes) ▴▾") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput) + .selected(active_radarr_block == ActiveRadarrBlock::IndexerSettingsRssSyncIntervalInput); + let whitelisted_subs_input_box = + InputBox::new(&indexer_settings.whitelisted_hardcoded_subs.text) + .offset(*indexer_settings.whitelisted_hardcoded_subs.offset.borrow()) + .label("Whitelisted Subtitle Tags") + .highlighted( + selected_block == &ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + ) + .selected( + active_radarr_block == ActiveRadarrBlock::IndexerSettingsWhitelistedSubtitleTagsInput, + ); + + render_selectable_input_box!(min_age_text_box, f, min_age_area); + render_selectable_input_box!(retention_input_box, f, retention_area); + render_selectable_input_box!(max_size_input_box, f, max_size_area); + render_selectable_input_box!(availability_delay_input_box, f, availability_delay_area); + render_selectable_input_box!(rss_sync_interval_input_box, f, rss_sync_interval_area); + render_selectable_input_box!(whitelisted_subs_input_box, f, whitelisted_sub_tags_area); } - draw_checkbox_with_label( - f, - prefer_flags_area, - "Prefer Indexer Flags", - indexer_settings.prefer_indexer_flags, - selected_block == &ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags, - ); - - draw_checkbox_with_label( - f, - allow_hardcoded_subs_area, - "Allow Hardcoded Subs", - indexer_settings.allow_hardcoded_subs, - selected_block == &ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs, - ); + let prefer_indexer_flags_checkbox = Checkbox::new("Prefer Indexer Flags") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsTogglePreferIndexerFlags) + .checked(indexer_settings.prefer_indexer_flags); + let allow_hardcoded_subs_checkbox = Checkbox::new("Allow Hardcoded Subs") + .highlighted(selected_block == &ActiveRadarrBlock::IndexerSettingsToggleAllowHardcodedSubs) + .checked(indexer_settings.allow_hardcoded_subs); let [save_area, cancel_area] = Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(25)]) .flex(Flex::Center) .areas(buttons_area); - draw_button(f, save_area, "Save", yes_no_value && highlight_yes_no); - draw_button(f, cancel_area, "Cancel", !yes_no_value && highlight_yes_no); + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(block, area); + f.render_widget(prefer_indexer_flags_checkbox, prefer_flags_area); + f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); } else { loading(f, block, area, app.is_loading); } diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index 8c8886d..c716bec 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -16,14 +16,14 @@ use crate::ui::utils::{ borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless, title_block_centered, }; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::input_box::InputBox; use crate::ui::{ - draw_button, draw_drop_down_menu_button, draw_drop_down_popup, draw_error_popup, - draw_error_popup_over, draw_large_popup_over, draw_medium_popup_over, draw_selectable_list, - draw_table, draw_text_box, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, TableProps, - TextBoxProps, + draw_drop_down_popup, draw_error_popup, draw_error_popup_over, draw_large_popup_over, + draw_medium_popup_over, draw_selectable_list, draw_table, DrawUi, TableProps, }; use crate::utils::convert_runtime; -use crate::App; +use crate::{render_selectable_input_box, App}; #[cfg(test)] #[path = "add_movie_ui_tests.rs"] @@ -120,24 +120,17 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::AddMovieSearchInput => { - draw_text_box( - f, - TextBoxProps { - text_box_area: search_box_area, - block_title: Some("Add Movie"), - block_content, - offset, - should_show_cursor: true, - is_selected: false, - cursor_after_string: true, - }, - ); - f.render_widget(layout_block(), results_area); - + let search_box = InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Movie")); let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help()); let help_paragraph = Paragraph::new(help_text) .block(borderless_block()) .alignment(Alignment::Center); + + search_box.show_cursor(f, search_box_area); + f.render_widget(layout_block(), results_area); + f.render_widget(search_box, search_box_area); f.render_widget(help_paragraph, help_area); } ActiveRadarrBlock::AddMovieEmptySearchResults => { @@ -252,17 +245,11 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } } - draw_text_box( - f, - TextBoxProps { - text_box_area: search_box_area, - block_title: Some("Add Movie"), - block_content, - offset, - should_show_cursor: false, - is_selected: false, - cursor_after_string: true, - }, + f.render_widget( + InputBox::new(block_content) + .offset(offset) + .block(title_block_centered("Add Movie")), + search_box_area, ); } @@ -395,54 +382,50 @@ fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - draw_drop_down_menu_button( - f, - root_folder_area, - "Root Folder", - &selected_root_folder.path, - selected_block == &ActiveRadarrBlock::AddMovieSelectRootFolder, - ); + let root_folder_drop_down_button = Button::new() + .title(&selected_root_folder.path) + .label("Root Folder") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectRootFolder); + let monitor_drop_down_button = Button::new() + .title(selected_monitor.to_display_str()) + .label("Monitor") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectMonitor); + let min_availability_drop_down_button = Button::new() + .title(selected_minimum_availability.to_display_str()) + .label("Minimum Availability") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectMinimumAvailability); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::AddMovieSelectQualityProfile); - draw_drop_down_menu_button( - f, - monitor_area, - "Monitor", - selected_monitor.to_display_str(), - selected_block == &ActiveRadarrBlock::AddMovieSelectMonitor, - ); - - draw_drop_down_menu_button( - f, - min_availability_area, - "Minimum Availability", - selected_minimum_availability.to_display_str(), - selected_block == &ActiveRadarrBlock::AddMovieSelectMinimumAvailability, - ); - draw_drop_down_menu_button( - f, - quality_profile_area, - "Quality Profile", - selected_quality_profile, - selected_block == &ActiveRadarrBlock::AddMovieSelectQualityProfile, - ); + f.render_widget(root_folder_drop_down_button, root_folder_area); + f.render_widget(monitor_drop_down_button, monitor_area); + f.render_widget(min_availability_drop_down_button, min_availability_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: tags_area, - label: "Tags", - text: &tags.text, - offset: *tags.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::AddMovieTagsInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput, - cursor_after_string: true, - }, - ); + let tags_input_box = InputBox::new(&tags.text) + .offset(*tags.offset.borrow()) + .label("Tags") + .highlighted(selected_block == &ActiveRadarrBlock::AddMovieTagsInput) + .selected(active_radarr_block == ActiveRadarrBlock::AddMovieTagsInput); + render_selectable_input_box!(tags_input_box, f, tags_area); } - draw_button(f, add_area, "Add", yes_no_value && highlight_yes_no); - draw_button(f, cancel_area, "Cancel", !yes_no_value && highlight_yes_no); + let add_button = Button::new() + .title("Add") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(add_button, add_area); + f.render_widget(cancel_button, cancel_area); } fn draw_add_movie_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { diff --git a/src/ui/radarr_ui/library/edit_movie_ui.rs b/src/ui/radarr_ui/library/edit_movie_ui.rs index 4e36106..7cfc458 100644 --- a/src/ui/radarr_ui/library/edit_movie_ui.rs +++ b/src/ui/radarr_ui/library/edit_movie_ui.rs @@ -9,14 +9,18 @@ use crate::models::servarr_data::radarr::radarr_data::{ ActiveRadarrBlock, EDIT_MOVIE_BLOCKS, MOVIE_DETAILS_BLOCKS, }; use crate::models::Route; +use crate::render_selectable_input_box; use crate::ui::radarr_ui::library::draw_library; use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi; +use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{layout_paragraph_borderless, title_block_centered}; +use crate::ui::widgets::button::Button; +use crate::ui::widgets::checkbox::Checkbox; +use crate::ui::widgets::input_box::InputBox; use crate::ui::{ - draw_button, draw_checkbox_with_label, draw_drop_down_menu_button, draw_drop_down_popup, - draw_large_popup_over_background_fn_with_ui, draw_medium_popup_over, draw_popup, - draw_selectable_list, draw_text_box_with_label, DrawUi, LabeledTextBoxProps, + draw_drop_down_popup, draw_large_popup_over_background_fn_with_ui, + draw_medium_popup_over, draw_popup, draw_selectable_list, DrawUi, }; #[cfg(test)] @@ -123,12 +127,9 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are path, tags, } = app.data.radarr_data.edit_movie_modal.as_ref().unwrap(); - let selected_minimum_availability = minimum_availability_list.current_selection(); let selected_quality_profile = quality_profile_list.current_selection(); - f.render_widget(title_block_centered(&title), area); - let [paragraph_area, monitored_area, min_availability_area, quality_profile_area, path_area, tags_area, _, buttons_area] = Layout::vertical([ Constraint::Length(6), @@ -142,66 +143,61 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are ]) .margin(1) .areas(area); - - let prompt_paragraph = layout_paragraph_borderless(&movie_overview); - f.render_widget(prompt_paragraph, paragraph_area); - let [save_area, cancel_area] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) .areas(buttons_area); - draw_checkbox_with_label( - f, - monitored_area, - "Monitored", - monitored.unwrap_or_default(), - selected_block == &ActiveRadarrBlock::EditMovieToggleMonitored, - ); - - draw_drop_down_menu_button( - f, - min_availability_area, - "Minimum Availability", - selected_minimum_availability.to_display_str(), - selected_block == &ActiveRadarrBlock::EditMovieSelectMinimumAvailability, - ); - draw_drop_down_menu_button( - f, - quality_profile_area, - "Quality Profile", - selected_quality_profile, - selected_block == &ActiveRadarrBlock::EditMovieSelectQualityProfile, - ); + let prompt_paragraph = layout_paragraph_borderless(&movie_overview); + let monitored_checkbox = Checkbox::new("Monitored") + .checked(monitored.unwrap_or_default()) + .highlighted(selected_block == &ActiveRadarrBlock::EditMovieToggleMonitored); + let min_availability_drop_down_button = Button::new() + .title(selected_minimum_availability.to_display_str()) + .label("Minimum Availability") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::EditMovieSelectMinimumAvailability); + let quality_profile_drop_down_button = Button::new() + .title(selected_quality_profile) + .label("Quality Profile") + .icon("▼") + .selected(selected_block == &ActiveRadarrBlock::EditMovieSelectQualityProfile); if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() { - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: path_area, - label: "Path", - text: &path.text, - offset: *path.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditMoviePathInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMoviePathInput, - cursor_after_string: true, - }, - ); - draw_text_box_with_label( - f, - LabeledTextBoxProps { - area: tags_area, - label: "Tags", - text: &tags.text, - offset: *tags.offset.borrow(), - is_selected: selected_block == &ActiveRadarrBlock::EditMovieTagsInput, - should_show_cursor: active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput, - cursor_after_string: true, - }, - ); + let path_input_box = InputBox::new(&path.text) + .offset(*path.offset.borrow()) + .label("Path") + .highlighted(selected_block == &ActiveRadarrBlock::EditMoviePathInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditMoviePathInput); + let tags_input_box = InputBox::new(&tags.text) + .offset(*tags.offset.borrow()) + .label("Tags") + .highlighted(selected_block == &ActiveRadarrBlock::EditMovieTagsInput) + .selected(active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput); + + match active_radarr_block { + ActiveRadarrBlock::EditMoviePathInput => path_input_box.show_cursor(f, path_area), + ActiveRadarrBlock::EditMovieTagsInput => tags_input_box.show_cursor(f, tags_area), + _ => (), + } + + render_selectable_input_box!(path_input_box, f, path_area); + render_selectable_input_box!(tags_input_box, f, tags_area); } - draw_button(f, save_area, "Save", yes_no_value && highlight_yes_no); - draw_button(f, cancel_area, "Cancel", !yes_no_value && highlight_yes_no); + let save_button = Button::new() + .title("Save") + .selected(yes_no_value && highlight_yes_no); + let cancel_button = Button::new() + .title("Cancel") + .selected(!yes_no_value && highlight_yes_no); + + f.render_widget(title_block_centered(&title), area); + f.render_widget(prompt_paragraph, paragraph_area); + f.render_widget(monitored_checkbox, monitored_area); + f.render_widget(min_availability_drop_down_button, min_availability_area); + f.render_widget(quality_profile_drop_down_button, quality_profile_area); + f.render_widget(save_button, save_area); + f.render_widget(cancel_button, cancel_area); } fn draw_edit_movie_select_minimum_availability_popup( diff --git a/src/ui/utils.rs b/src/ui/utils.rs index f68c750..a209894 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -1,9 +1,9 @@ use crate::ui::styles::ManagarrStyle; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Style, Stylize}; +use ratatui::symbols; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, BorderType, Borders, LineGauge, Paragraph, Wrap}; -use ratatui::{symbols, Frame}; pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55); @@ -37,28 +37,6 @@ pub fn layout_block_bottom_border<'a>() -> Block<'a> { Block::new().borders(Borders::BOTTOM) } -pub fn layout_button_paragraph( - is_selected: bool, - label: &str, - alignment: Alignment, -) -> Paragraph<'_> { - Paragraph::new(Text::from(label)) - .block(layout_block()) - .alignment(alignment) - .style(style_block_highlight(is_selected)) -} - -pub fn layout_button_paragraph_borderless( - is_selected: bool, - label: &str, - alignment: Alignment, -) -> Paragraph<'_> { - Paragraph::new(Text::from(label)) - .block(borderless_block()) - .alignment(alignment) - .style(style_block_highlight(is_selected)) -} - pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> { Paragraph::new(Text::from(string)) .block(borderless_block()) @@ -135,20 +113,6 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> { .label(Line::from(format!("{title}: {:.0}%", ratio * 100.0))) } -pub fn show_cursor( - f: &mut Frame<'_>, - area: Rect, - offset: usize, - string: &str, - cursor_after_string: bool, -) { - if cursor_after_string { - f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1); - } else { - f.set_cursor(area.x + 1u16, area.y + 1); - } -} - pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize { (area.width as f64 * (percentage as f64 / 100.0)) as usize } diff --git a/src/ui/widgets/button.rs b/src/ui/widgets/button.rs new file mode 100644 index 0000000..f029706 --- /dev/null +++ b/src/ui/widgets/button.rs @@ -0,0 +1,116 @@ +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{layout_block, style_block_highlight}; +use ratatui::buffer::Buffer; +use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; +use ratatui::prelude::{Style, Styled, Text, Widget}; +use ratatui::widgets::Paragraph; + +#[derive(Default)] +pub struct Button<'a> { + title: &'a str, + label: Option<&'a str>, + icon: Option<&'a str>, + style: Style, + is_selected: bool, +} + +impl<'a> Button<'a> { + pub fn title(mut self, title: &'a str) -> Button<'a> { + self.title = title; + self + } + + pub fn label(mut self, label: &'a str) -> Button<'a> { + self.label = Some(label); + self + } + + pub fn icon(mut self, icon: &'a str) -> Button<'a> { + self.icon = Some(icon); + self + } + + pub fn style>(mut self, style: S) -> Button<'a> { + self.style = style.into(); + self + } + + pub fn selected(mut self, is_selected: bool) -> Button<'a> { + self.is_selected = is_selected; + self + } + + fn render_button_with_icon(&self, area: Rect, buf: &mut Buffer) { + let [title_area, icon_area] = Layout::horizontal([ + Constraint::Length(self.title.len() as u16), + Constraint::Percentage(25), + ]) + .flex(Flex::SpaceBetween) + .margin(1) + .areas(area); + let style = style_block_highlight(self.is_selected); + + if let Some(icon) = self.icon { + layout_block().style(style).render(area, buf); + Paragraph::new(Text::from(self.title)) + .alignment(Alignment::Left) + .style(style) + .render(title_area, buf); + Paragraph::new(Text::from(format!("{icon} "))) + .alignment(Alignment::Right) + .style(style) + .render(icon_area, buf); + } + } + + fn render_labeled_button(&self, area: Rect, buf: &mut Buffer) { + let [label_area, button_area] = + Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); + let label_paragraph = Paragraph::new(Text::from(format!("\n{}: ", self.label.unwrap()))) + .alignment(Alignment::Right) + .primary(); + + if self.icon.is_some() { + self.render_button_with_icon(button_area, buf); + label_paragraph.render(label_area, buf); + } else { + self.render_button(button_area, buf); + label_paragraph.render(label_area, buf); + } + } + + fn render_button(&self, area: Rect, buf: &mut Buffer) { + Paragraph::new(Text::from(self.title)) + .block(layout_block()) + .alignment(Alignment::Center) + .style(style_block_highlight(self.is_selected)) + .render(area, buf); + } +} + +impl<'a> Widget for Button<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + if self.label.is_some() { + self.render_labeled_button(area, buf); + } else if self.icon.is_some() { + self.render_button_with_icon(area, buf); + } else { + self.render_button(area, buf); + } + } +} + +impl<'a> Styled for Button<'a> { + type Item = Button<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style>(self, style: S) -> Self::Item { + self.style(style) + } +} diff --git a/src/ui/widgets/checkbox.rs b/src/ui/widgets/checkbox.rs new file mode 100644 index 0000000..7747b7c --- /dev/null +++ b/src/ui/widgets/checkbox.rs @@ -0,0 +1,61 @@ +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, layout_block, style_block_highlight}; +use ratatui::buffer::Buffer; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::prelude::Text; +use ratatui::style::Stylize; +use ratatui::widgets::{Paragraph, Widget}; + +pub struct Checkbox<'a> { + label: &'a str, + is_checked: bool, + is_highlighted: bool, +} + +impl<'a> Checkbox<'a> { + pub fn new(label: &'a str) -> Checkbox<'a> { + Checkbox { + label, + is_checked: false, + is_highlighted: false, + } + } + + pub fn checked(mut self, is_checked: bool) -> Checkbox<'a> { + self.is_checked = is_checked; + self + } + + pub fn highlighted(mut self, is_selected: bool) -> Checkbox<'a> { + self.is_highlighted = is_selected; + self + } + + fn render_checkbox(&self, area: Rect, buf: &mut Buffer) { + let check = if self.is_checked { "✔" } else { "" }; + let [label_area, checkbox_area] = + Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); + let checkbox_box_area = Rect { + width: 5, + ..checkbox_area + }; + + Paragraph::new(Text::from(format!("\n{}: ", self.label))) + .block(borderless_block()) + .alignment(Alignment::Right) + .primary() + .render(label_area, buf); + + Paragraph::new(Text::from(check)) + .block(layout_block()) + .alignment(Alignment::Center) + .style(style_block_highlight(self.is_highlighted).bold()) + .render(checkbox_box_area, buf); + } +} + +impl<'a> Widget for Checkbox<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + self.render_checkbox(area, buf); + } +} diff --git a/src/ui/widgets/input_box.rs b/src/ui/widgets/input_box.rs new file mode 100644 index 0000000..8ce702e --- /dev/null +++ b/src/ui/widgets/input_box.rs @@ -0,0 +1,150 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::prelude::Text; +use ratatui::style::{Style, Styled, Stylize}; +use ratatui::widgets::{Block, Paragraph, Widget}; +use ratatui::Frame; + +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::{borderless_block, layout_block}; + +pub struct InputBox<'a> { + content: &'a str, + offset: usize, + style: Style, + block: Block<'a>, + label: Option<&'a str>, + cursor_after_string: bool, + is_highlighted: Option, + is_selected: Option, +} + +impl<'a> InputBox<'a> { + pub fn new(content: &'a str) -> InputBox<'_> { + InputBox { + content, + offset: 0, + style: Style::new().default(), + block: layout_block(), + label: None, + cursor_after_string: true, + is_highlighted: None, + is_selected: None, + } + } + + pub fn style>(mut self, style: S) -> InputBox<'a> { + self.style = style.into(); + self + } + + pub fn block(mut self, block: Block<'a>) -> InputBox<'a> { + self.block = block; + self + } + + pub fn label(mut self, label: &'a str) -> InputBox<'a> { + self.label = Some(label); + self + } + + pub fn offset(mut self, offset: usize) -> InputBox<'a> { + self.offset = offset; + self + } + + pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> { + self.cursor_after_string = cursor_after_string; + self + } + + pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> { + self.is_highlighted = Some(is_highlighted); + self + } + + pub fn selected(mut self, is_selected: bool) -> InputBox<'a> { + self.is_selected = Some(is_selected); + self + } + + pub fn is_selected(&self) -> bool { + self.is_selected.unwrap_or_default() + } + + pub fn show_cursor(&self, f: &mut Frame<'_>, area: Rect) { + let area = if self.label.is_some() { + Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).split(area)[1] + } else { + area + }; + + if self.cursor_after_string { + f.set_cursor( + area.x + (self.content.len() - self.offset) as u16 + 1, + area.y + 1, + ); + } else { + f.set_cursor(area.x + 1u16, area.y + 1); + } + } + + fn render_input_box(&self, area: Rect, buf: &mut Buffer) { + let style = + if matches!(self.is_highlighted, Some(true)) && matches!(self.is_selected, Some(false)) { + Style::new().system_function().bold() + } else { + self.style + }; + + let input_box_paragraph = Paragraph::new(Text::from(self.content)) + .style(style) + .block(self.block.clone()); + + if let Some(label) = self.label { + let [label_area, text_box_area] = + Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); + + Paragraph::new(Text::from(format!("\n{label}: "))) + .block(borderless_block()) + .alignment(Alignment::Right) + .primary() + .render(label_area, buf); + input_box_paragraph.render(text_box_area, buf); + } else { + input_box_paragraph.render(area, buf); + } + } +} + +impl<'a> Widget for InputBox<'a> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + self.render_input_box(area, buf); + } +} + +impl<'a> Styled for InputBox<'a> { + type Item = InputBox<'a>; + + fn style(&self) -> Style { + self.style + } + + fn set_style>(self, style: S) -> Self::Item { + self.style(style) + } +} + +#[macro_export] +macro_rules! render_selectable_input_box { + ($input_box:ident, $frame:ident, $area:ident) => { + if $input_box.is_selected() { + $input_box.show_cursor($frame, $area); + } + + $frame.render_widget($input_box, $area); + }; +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs new file mode 100644 index 0000000..9fa08be --- /dev/null +++ b/src/ui/widgets/mod.rs @@ -0,0 +1,3 @@ +pub(super) mod button; +pub(super) mod input_box; +pub(super) mod checkbox;