diff --git a/crates/synd_term/src/ui/components/entries.rs b/crates/synd_term/src/ui/components/entries.rs index 8ecc44d7..fa3a3da8 100644 --- a/crates/synd_term/src/ui/components/entries.rs +++ b/crates/synd_term/src/ui/components/entries.rs @@ -8,7 +8,7 @@ use crate::{ self, components::{collections::FilterableVec, filter::FeedFilterer}, icon, - widgets::scrollbar::Scrollbar, + widgets::{scrollbar::Scrollbar, table::Table}, Context, }, }; @@ -16,10 +16,7 @@ use ratatui::{ prelude::{Alignment, Buffer, Constraint, Layout, Rect}, style::Stylize, text::{Line, Span, Text}, - widgets::{ - Block, BorderType, Borders, Cell, Padding, Paragraph, Row, StatefulWidget, Table, - TableState, Widget, Wrap, - }, + widgets::{Block, BorderType, Borders, Cell, Padding, Paragraph, Row, Widget, Wrap}, }; use synd_feed::types::FeedUrl; @@ -95,25 +92,17 @@ impl Entries { fn render_entries(&self, area: Rect, buf: &mut Buffer, cx: &Context<'_>) { let entries_area = Block::new().padding(Padding::top(1)).inner(area); - let mut entries_state = TableState::new() - .with_offset(0) - .with_selected(self.entries.selected_index()); - let (header, widths, rows) = self.entry_rows(cx); - let entries = Table::new(rows, widths) - .header(header.style(cx.theme.entries.header)) - .column_spacing(2) - .highlight_symbol(ui::TABLE_HIGHLIGHT_SYMBOL) - .highlight_style( - cx.theme - .entries - .selected_entry - .add_modifier(cx.table_highlight_modifier()), - ) - .highlight_spacing(ratatui::widgets::HighlightSpacing::Always); - - StatefulWidget::render(entries, entries_area, buf, &mut entries_state); + Table::builder() + .header(header) + .widths(widths) + .rows(rows) + .theme(&cx.theme.entries) + .selected_idx(self.entries.selected_index()) + .highlight_modifier(cx.table_highlight_modifier()) + .build() + .render(entries_area, buf); let header_rows = 2; #[allow(clippy::cast_possible_truncation)] diff --git a/crates/synd_term/src/ui/components/gh_notifications/mod.rs b/crates/synd_term/src/ui/components/gh_notifications/mod.rs index 127c854c..4ae7a20a 100644 --- a/crates/synd_term/src/ui/components/gh_notifications/mod.rs +++ b/crates/synd_term/src/ui/components/gh_notifications/mod.rs @@ -7,10 +7,7 @@ use ratatui::{ layout::{Alignment, Constraint, Layout, Rect}, style::{Modifier, Style, Styled, Stylize}, text::{Line, Span}, - widgets::{ - Block, BorderType, Borders, Cell, Padding, Paragraph, Row, StatefulWidget, Table, - TableState, Widget, Wrap, - }, + widgets::{Block, BorderType, Borders, Cell, Padding, Paragraph, Row, Widget, Wrap}, }; use serde::{Deserialize, Serialize}; @@ -29,14 +26,13 @@ use crate::{ TimeExt, }, ui::{ - self, components::{ collections::FilterableVec, filter::{CategoryFilterer, ComposedFilterer, MatcherFilterer}, }, extension::RectExt, icon, - widgets::scrollbar::Scrollbar, + widgets::{scrollbar::Scrollbar, table::Table}, Context, }, }; @@ -342,23 +338,17 @@ impl GhNotifications { fn render_notifications(&self, area: Rect, buf: &mut Buffer, cx: &Context<'_>) { let notifications_area = Block::new().padding(Padding::top(1)).inner(area); - let mut notifications_state = TableState::new() - .with_offset(0) - .with_selected(self.notifications.selected_index()); let (header, widths, rows) = self.notification_rows(cx); - let notifications = Table::new(rows, widths) - .header(header.style(cx.theme.entries.header)) - .column_spacing(2) - .highlight_symbol(ui::TABLE_HIGHLIGHT_SYMBOL) - .highlight_style(cx.theme.entries.selected_entry) - .highlight_spacing(ratatui::widgets::HighlightSpacing::Always); - - StatefulWidget::render( - notifications, - notifications_area, - buf, - &mut notifications_state, - ); + + Table::builder() + .header(header) + .widths(widths) + .rows(rows) + .theme(&cx.theme.entries) + .selected_idx(self.notifications.selected_index()) + .highlight_modifier(cx.table_highlight_modifier()) + .build() + .render(notifications_area, buf); let header_rows = 2; #[allow(clippy::cast_possible_truncation)] diff --git a/crates/synd_term/src/ui/components/subscription.rs b/crates/synd_term/src/ui/components/subscription.rs index 092f6b08..9f1f828e 100644 --- a/crates/synd_term/src/ui/components/subscription.rs +++ b/crates/synd_term/src/ui/components/subscription.rs @@ -6,8 +6,8 @@ use ratatui::{ style::{Modifier, Style, Stylize}, text::{Line, Span}, widgets::{ - Block, BorderType, Borders, Cell, HighlightSpacing, Padding, Paragraph, Row, - StatefulWidget, Table, TableState, Tabs, Widget, + Block, BorderType, Borders, Cell, Padding, Paragraph, Row, Table as RatatuiTable, Tabs, + Widget, }, }; use synd_feed::types::{FeedType, FeedUrl}; @@ -20,7 +20,7 @@ use crate::{ self, components::{collections::FilterableVec, filter::FeedFilterer}, extension::RectExt, - widgets::scrollbar::Scrollbar, + widgets::{scrollbar::Scrollbar, table::Table}, Context, }, }; @@ -153,32 +153,17 @@ impl Subscription { fn render_feeds(&self, area: Rect, buf: &mut Buffer, cx: &Context<'_>) { let feeds_area = Block::new().padding(Padding::top(1)).inner(area); - let mut feeds_state = TableState::new() - .with_offset(0) - .with_selected(self.feeds.selected_index()); - let (header, widths, rows) = self.feed_rows(cx); - let feeds = Table::new(rows, widths) - .block(Block::new().padding(Padding { - left: 0, - right: 0, - top: 0, - bottom: 0, - })) - .header(header.style(cx.theme.subscription.header)) - .column_spacing(2) - .style(cx.theme.subscription.background) - .highlight_symbol(ui::TABLE_HIGHLIGHT_SYMBOL) - .highlight_style( - cx.theme - .subscription - .selected_feed - .add_modifier(cx.table_highlight_modifier()), - ) - .highlight_spacing(HighlightSpacing::Always); - - StatefulWidget::render(feeds, feeds_area, buf, &mut feeds_state); + Table::builder() + .header(header) + .widths(widths) + .rows(rows) + .theme(&cx.theme.entries) + .selected_idx(self.feeds.selected_index()) + .highlight_modifier(cx.table_highlight_modifier()) + .build() + .render(feeds_area, buf); let header_rows = 2; #[allow(clippy::cast_possible_truncation)] @@ -353,7 +338,7 @@ impl Subscription { ]), ]; - let table = Table::new(meta_rows, widths) + let table = RatatuiTable::new(meta_rows, widths) .column_spacing(1) .style(cx.theme.subscription.background); Widget::render(table, meta_area, buf); @@ -381,7 +366,7 @@ impl Subscription { ]); let rows = feed.entries.iter().map(entry); - let table = Table::new(rows, widths) + let table = RatatuiTable::new(rows, widths) .header(header.style(cx.theme.subscription.header)) .column_spacing(1) .style(cx.theme.subscription.background); diff --git a/crates/synd_term/src/ui/widgets/mod.rs b/crates/synd_term/src/ui/widgets/mod.rs index 1648c7d3..8ba619b0 100644 --- a/crates/synd_term/src/ui/widgets/mod.rs +++ b/crates/synd_term/src/ui/widgets/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod prompt; pub(crate) mod scrollbar; +pub(crate) mod table; pub(crate) mod throbber; diff --git a/crates/synd_term/src/ui/widgets/table.rs b/crates/synd_term/src/ui/widgets/table.rs new file mode 100644 index 00000000..0fc2c4bc --- /dev/null +++ b/crates/synd_term/src/ui/widgets/table.rs @@ -0,0 +1,162 @@ +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::Modifier, + widgets::{Row, StatefulWidget, TableState}, +}; + +use crate::ui::{self, theme::EntriesTheme}; + +pub(crate) struct TableBuilder { + header: H, + rows: R, + widths: C, + theme: T, + selected_idx: S, + highlight_modifier: M, +} + +impl Default for TableBuilder<(), (), (), (), (), ()> { + fn default() -> Self { + Self { + header: (), + rows: (), + widths: (), + theme: (), + selected_idx: (), + highlight_modifier: (), + } + } +} + +impl TableBuilder<(), R, C, T, S, M> { + pub(crate) fn header(self, header: Row<'_>) -> TableBuilder, R, C, T, S, M> { + TableBuilder { + header, + rows: self.rows, + widths: self.widths, + theme: self.theme, + selected_idx: self.selected_idx, + highlight_modifier: self.highlight_modifier, + } + } +} + +impl TableBuilder { + pub(crate) fn rows<'a, Rows>(self, rows: Rows) -> TableBuilder>, C, T, S, M> + where + Rows: IntoIterator, + Rows::Item: Into>, + { + TableBuilder { + header: self.header, + rows: rows.into_iter().map(Into::into).collect(), + widths: self.widths, + theme: self.theme, + selected_idx: self.selected_idx, + highlight_modifier: self.highlight_modifier, + } + } +} + +impl TableBuilder { + pub(crate) fn widths(self, widths: C) -> TableBuilder, T, S, M> + where + C: IntoIterator, + C::Item: Into, + { + TableBuilder { + header: self.header, + rows: self.rows, + widths: widths.into_iter().map(Into::into).collect(), + theme: self.theme, + selected_idx: self.selected_idx, + highlight_modifier: self.highlight_modifier, + } + } +} + +impl TableBuilder { + pub(crate) fn theme(self, theme: &EntriesTheme) -> TableBuilder + where + C: IntoIterator, + C::Item: Into, + { + TableBuilder { + header: self.header, + rows: self.rows, + widths: self.widths, + theme, + selected_idx: self.selected_idx, + highlight_modifier: self.highlight_modifier, + } + } +} + +impl TableBuilder { + pub(crate) fn selected_idx(self, selected_idx: usize) -> TableBuilder { + TableBuilder { + header: self.header, + rows: self.rows, + widths: self.widths, + theme: self.theme, + selected_idx, + highlight_modifier: self.highlight_modifier, + } + } +} + +impl TableBuilder { + pub(crate) fn highlight_modifier( + self, + highlight_modifier: Modifier, + ) -> TableBuilder { + TableBuilder { + header: self.header, + rows: self.rows, + widths: self.widths, + theme: self.theme, + selected_idx: self.selected_idx, + highlight_modifier, + } + } +} + +impl<'a> TableBuilder, Vec>, Vec, &'a EntriesTheme, usize, Modifier> { + pub(crate) fn build(self) -> Table<'a> { + let TableBuilder { + header, + rows, + widths, + theme, + selected_idx, + highlight_modifier, + } = self; + + let table = ratatui::widgets::Table::new(rows, widths) + .header(header.style(theme.header)) + .column_spacing(2) + .highlight_symbol(ui::TABLE_HIGHLIGHT_SYMBOL) + .highlight_style(theme.selected_entry.add_modifier(highlight_modifier)) + .highlight_spacing(ratatui::widgets::HighlightSpacing::Always); + + let state = TableState::new().with_offset(0).with_selected(selected_idx); + + Table { table, state } + } +} + +pub(crate) struct Table<'a> { + table: ratatui::widgets::Table<'a>, + state: TableState, +} + +impl<'a> Table<'a> { + pub(crate) fn builder() -> TableBuilder<(), (), (), (), (), ()> { + TableBuilder::default() + } + + pub(crate) fn render(mut self, area: Rect, buf: &mut Buffer) { + StatefulWidget::render(self.table, area, buf, &mut self.state); + } +}