Extract widgets
All checks were successful
Cargo CI / Build and Test (pull_request) Successful in 1m57s
Cargo CI / Lint (pull_request) Successful in 1m5s

This commit is contained in:
Wojciech Kozlowski 2024-08-29 16:53:49 +02:00
parent 4a22c29eb8
commit 284556f756
2 changed files with 156 additions and 143 deletions

View File

@ -6,6 +6,7 @@ mod minibuffer;
mod overlay; mod overlay;
mod reload; mod reload;
mod style; mod style;
mod widgets;
use browse::{AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState}; use browse::{AlbumArea, AlbumState, ArtistArea, ArtistState, FrameArea, TrackArea, TrackState};
use display::UiDisplay; use display::UiDisplay;
@ -16,162 +17,36 @@ use musichoard::collection::{album::Album, track::Track, Collection};
use overlay::{OverlayBuilder, OverlaySize}; use overlay::{OverlayBuilder, OverlaySize};
use ratatui::{ use ratatui::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
widgets::{Block, BorderType, Borders, Clear, List, Paragraph, Wrap}, widgets::{Paragraph, Wrap},
Frame, Frame,
}; };
use widgets::UiWidget;
use crate::tui::{ use crate::tui::{
app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState}, app::{AppPublicState, AppState, Category, IAppAccess, Selection, WidgetState},
lib::interface::musicbrainz::Match, lib::interface::musicbrainz::Match,
ui::{reload::ReloadMenu, style::UiStyle}, ui::reload::ReloadMenu,
}; };
pub trait IUi { pub trait IUi {
fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame); fn render<APP: IAppAccess>(app: &mut APP, frame: &mut Frame);
} }
struct Column<'a> {
paragraph: Paragraph<'a>,
area: Rect,
}
pub struct Ui; pub struct Ui;
impl Ui { impl Ui {
fn block<'a>(active: bool, error: bool) -> Block<'a> {
Block::default().style(UiStyle::block_style(active, error))
}
fn block_with_borders<'a>(title: &str, active: bool, error: bool) -> Block<'a> {
Self::block(active, error)
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(format!(" {title} "))
}
fn render_list_widget(
title: &str,
list: List,
state: &mut WidgetState,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_stateful_widget(
list.highlight_style(UiStyle::highlight_style(active))
.highlight_symbol(">> ")
.style(UiStyle::style(active, false))
.block(Self::block_with_borders(title, active, false)),
area,
&mut state.list,
);
state.height = area.height.saturating_sub(2) as usize;
}
fn render_overlay_list_widget(
title: &str,
list: List,
state: &mut WidgetState,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_widget(Clear, area);
Self::render_list_widget(title, list, state, active, area, frame);
}
fn render_info_widget(
title: &str,
paragraph: Paragraph,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_widget(
paragraph
.style(UiStyle::style(active, false))
.block(Self::block_with_borders(title, active, false)),
area,
);
}
fn render_overlay_widget(
title: &str,
paragraph: Paragraph,
area: Rect,
error: bool,
frame: &mut Frame,
) {
frame.render_widget(Clear, area);
frame.render_widget(
paragraph
.style(UiStyle::style(true, error))
.block(Self::block_with_borders(title, true, error)),
area,
);
}
fn columns(paragraphs: Vec<Paragraph>, min: u16, area: Rect) -> Vec<Column> {
let mut x = area.x;
let mut width = area.width;
let mut remaining = paragraphs.len() as u16;
if remaining < min {
remaining = min;
}
let mut blocks = vec![];
for paragraph in paragraphs.into_iter() {
let block_width = width / remaining;
blocks.push(Column {
paragraph,
area: Rect {
x,
y: area.y,
width: block_width,
height: area.height,
},
});
x = x.saturating_add(block_width);
width = width.saturating_sub(block_width);
remaining -= 1;
}
blocks
}
fn render_columns(
paragraphs: Vec<Paragraph>,
min: u16,
active: bool,
area: Rect,
frame: &mut Frame,
) {
for column in Self::columns(paragraphs, min, area).into_iter() {
frame.render_widget(
column
.paragraph
.style(UiStyle::style(active, false))
.block(Self::block(active, false)),
column.area,
);
}
}
fn render_artist_column(st: ArtistState, ar: ArtistArea, fr: &mut Frame) { fn render_artist_column(st: ArtistState, ar: ArtistArea, fr: &mut Frame) {
Self::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr); UiWidget::render_list_widget("Artists", st.list, st.state, st.active, ar.list, fr);
} }
fn render_album_column(st: AlbumState, ar: AlbumArea, fr: &mut Frame) { fn render_album_column(st: AlbumState, ar: AlbumArea, fr: &mut Frame) {
Self::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr); UiWidget::render_list_widget("Albums", st.list, st.state, st.active, ar.list, fr);
Self::render_info_widget("Album info", st.info, st.active, ar.info, fr); UiWidget::render_info_widget("Album info", st.info, st.active, ar.info, fr);
} }
fn render_track_column(st: TrackState, ar: TrackArea, fr: &mut Frame) { fn render_track_column(st: TrackState, ar: TrackArea, fr: &mut Frame) {
Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr); UiWidget::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr);
Self::render_info_widget("Track info", st.info, st.active, ar.info, fr); UiWidget::render_info_widget("Track info", st.info, st.active, ar.info, fr);
} }
fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame) { fn render_minibuffer(state: &AppPublicState, ar: Rect, fr: &mut Frame) {
@ -185,8 +60,8 @@ impl Ui {
height: 1, height: 1,
}; };
Self::render_info_widget("Minibuffer", Paragraph::new(""), false, ar, fr); UiWidget::render_info_widget("Minibuffer", Paragraph::new(""), false, ar, fr);
Self::render_columns(mb.paragraphs, mb.columns, false, area, fr); UiWidget::render_columns(mb.paragraphs, mb.columns, false, area, fr);
} }
fn render_browse_frame( fn render_browse_frame(
@ -239,16 +114,16 @@ impl Ui {
let area = OverlayBuilder::default().build(frame.size()); let area = OverlayBuilder::default().build(frame.size());
if selection.category() == Category::Artist { if selection.category() == Category::Artist {
let artist_overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list); let overlay = ArtistOverlay::new(artists, &selection.widget_state_artist().list);
Self::render_overlay_widget("Artist", artist_overlay.properties, area, false, frame); UiWidget::render_overlay_widget("Artist", overlay.properties, area, false, frame);
} else { } else {
let no_albums: Vec<Album> = vec![]; let no_albums: Vec<Album> = vec![];
let albums = selection let albums = selection
.state_album(artists) .state_album(artists)
.map(|st| st.list) .map(|st| st.list)
.unwrap_or_else(|| &no_albums); .unwrap_or_else(|| &no_albums);
let album_overlay = AlbumOverlay::new(albums, &selection.widget_state_album().list); let overlay = AlbumOverlay::new(albums, &selection.widget_state_album().list);
Self::render_overlay_widget("Album", album_overlay.properties, area, false, frame); UiWidget::render_overlay_widget("Album", overlay.properties, area, false, frame);
} }
} }
@ -261,7 +136,7 @@ impl Ui {
let area = OverlayBuilder::default().build(frame.size()); let area = OverlayBuilder::default().build(frame.size());
let matching_string = UiDisplay::display_matching_info(matching); let matching_string = UiDisplay::display_matching_info(matching);
let st = AlbumMatchesState::new(matches.unwrap_or_default(), state); let st = AlbumMatchesState::new(matches.unwrap_or_default(), state);
Self::render_overlay_list_widget(&matching_string, st.list, st.state, true, area, frame) UiWidget::render_overlay_list_widget(&matching_string, st.list, st.state, true, area, frame)
} }
fn render_reload_overlay(frame: &mut Frame) { fn render_reload_overlay(frame: &mut Frame) {
@ -272,7 +147,7 @@ impl Ui {
let reload_text = ReloadMenu::paragraph().alignment(Alignment::Center); let reload_text = ReloadMenu::paragraph().alignment(Alignment::Center);
Self::render_overlay_widget("Reload", reload_text, area, false, frame); UiWidget::render_overlay_widget("Reload", reload_text, area, false, frame);
} }
fn render_error_overlay<S: AsRef<str>>(title: S, msg: S, frame: &mut Frame) { fn render_error_overlay<S: AsRef<str>>(title: S, msg: S, frame: &mut Frame) {
@ -284,7 +159,7 @@ impl Ui {
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
Self::render_overlay_widget(title.as_ref(), error_text, area, true, frame); UiWidget::render_overlay_widget(title.as_ref(), error_text, area, true, frame);
} }
} }

138
src/tui/ui/widgets.rs Normal file
View File

@ -0,0 +1,138 @@
use ratatui::{
layout::{Alignment, Rect},
widgets::{Block, BorderType, Borders, Clear, List, Paragraph},
Frame,
};
use crate::tui::{app::WidgetState, ui::style::UiStyle};
struct Column<'a> {
paragraph: Paragraph<'a>,
area: Rect,
}
pub struct UiWidget;
impl UiWidget {
fn block<'a>(active: bool, error: bool) -> Block<'a> {
Block::default().style(UiStyle::block_style(active, error))
}
fn block_with_borders<'a>(title: &str, active: bool, error: bool) -> Block<'a> {
Self::block(active, error)
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(format!(" {title} "))
}
pub fn render_list_widget(
title: &str,
list: List,
state: &mut WidgetState,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_stateful_widget(
list.highlight_style(UiStyle::highlight_style(active))
.highlight_symbol(">> ")
.style(UiStyle::style(active, false))
.block(Self::block_with_borders(title, active, false)),
area,
&mut state.list,
);
state.height = area.height.saturating_sub(2) as usize;
}
pub fn render_overlay_list_widget(
title: &str,
list: List,
state: &mut WidgetState,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_widget(Clear, area);
Self::render_list_widget(title, list, state, active, area, frame);
}
pub fn render_info_widget(
title: &str,
paragraph: Paragraph,
active: bool,
area: Rect,
frame: &mut Frame,
) {
frame.render_widget(
paragraph
.style(UiStyle::style(active, false))
.block(Self::block_with_borders(title, active, false)),
area,
);
}
pub fn render_overlay_widget(
title: &str,
paragraph: Paragraph,
area: Rect,
error: bool,
frame: &mut Frame,
) {
frame.render_widget(Clear, area);
frame.render_widget(
paragraph
.style(UiStyle::style(true, error))
.block(Self::block_with_borders(title, true, error)),
area,
);
}
fn columns(paragraphs: Vec<Paragraph>, min: u16, area: Rect) -> Vec<Column> {
let mut x = area.x;
let mut width = area.width;
let mut remaining = paragraphs.len() as u16;
if remaining < min {
remaining = min;
}
let mut blocks = vec![];
for paragraph in paragraphs.into_iter() {
let block_width = width / remaining;
blocks.push(Column {
paragraph,
area: Rect {
x,
y: area.y,
width: block_width,
height: area.height,
},
});
x = x.saturating_add(block_width);
width = width.saturating_sub(block_width);
remaining -= 1;
}
blocks
}
pub fn render_columns(
paragraphs: Vec<Paragraph>,
min: u16,
active: bool,
area: Rect,
frame: &mut Frame,
) {
for column in Self::columns(paragraphs, min, area).into_iter() {
frame.render_widget(
column
.paragraph
.style(UiStyle::style(active, false))
.block(Self::block(active, false)),
column.area,
);
}
}
}