diff --git a/bin/ui/widget.rs b/bin/ui/widget.rs index 650527f2..6c03d1aa 100644 --- a/bin/ui/widget.rs +++ b/bin/ui/widget.rs @@ -1,4 +1,5 @@ use std::marker::PhantomData; +use std::str::FromStr; use radicle::issue::{self, CloseReason}; use ratatui::layout::{Constraint, Layout}; @@ -9,6 +10,7 @@ use ratatui::Frame; use radicle_tui as tui; +use tui::store; use tui::ui::theme::style; use tui::ui::widget::{RenderProps, View, ViewProps}; use tui::ui::{layout, span}; @@ -16,6 +18,125 @@ use tui::ui::{layout, span}; use super::format; use super::items::IssueItem; +use crate::ui::items::Filter; + +/// A `BrowserState` represents the internal state of a browser widget. +/// A browser widget would consist of 2 child widgets: a list of items and a +/// buffered search field. The search fields value is used to build an +/// item filter that the item list reacts on dynamically. +#[derive(Clone, Debug)] +pub struct BrowserState { + items: Vec, + selected: Option, + filter: F, + search: store::StateValue, + show_search: bool, +} + +impl Default for BrowserState +where + I: Clone, + F: Filter + Default + FromStr, +{ + fn default() -> Self { + Self { + items: vec![], + selected: None, + filter: F::default(), + search: store::StateValue::new(String::default()), + show_search: false, + } + } +} + +impl BrowserState +where + I: Clone, + F: Filter + Default + FromStr, +{ + pub fn build(items: Vec, filter: F, search: store::StateValue) -> Self { + let selected = items.first().map(|_| 0); + + Self { + items, + selected, + filter, + search, + ..Default::default() + } + } + + pub fn items(&self) -> Vec { + self.items_ref().into_iter().cloned().collect() + } + + pub fn items_ref(&self) -> Vec<&I> { + self.items + .iter() + .filter(|patch| self.filter.matches(patch)) + .collect() + } + + pub fn selected(&self) -> Option { + self.selected + } + + pub fn selected_item(&self) -> Option<&I> { + self.selected + .and_then(|selected| self.items_ref().get(selected).copied()) + } + + pub fn select_item(&mut self, selected: Option) -> Option<&I> { + self.selected = selected; + self.selected_item() + } + + pub fn select_first_item(&mut self) -> Option<&I> { + self.selected.and_then(|selected| { + if selected > self.items_ref().len() { + self.selected = Some(0); + self.items_ref().first().cloned() + } else { + self.items_ref().get(selected).cloned() + } + }) + } + + fn filter_items(&mut self) { + self.filter = F::from_str(&self.search.read()).unwrap_or_default(); + } + + pub fn update_search(&mut self, value: String) { + self.search.write(value); + self.filter_items(); + } + + pub fn show_search(&mut self) { + self.show_search = true; + } + + pub fn hide_search(&mut self) { + self.show_search = false; + } + + pub fn apply_search(&mut self) { + self.search.apply(); + } + + pub fn reset_search(&mut self) { + self.search.reset(); + self.filter_items(); + } + + pub fn is_search_shown(&self) -> bool { + self.show_search + } + + pub fn read_search(&self) -> String { + self.search.read() + } +} + #[derive(Clone, Default)] pub struct IssueDetailsProps { issue: Option,