Skip to content

Commit

Permalink
Add search to recipe list
Browse files Browse the repository at this point in the history
Right now it selects the first recipe that contains the query in its name (caseless). This is subject to change though, I need to play with it more.
  • Loading branch information
LucasPickering committed Dec 18, 2024
1 parent e272ab1 commit bd0ebd6
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 7 deletions.
70 changes: 63 additions & 7 deletions crates/tui/src/view/component/recipe_list.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use crate::{
context::TuiContext,
view::{
common::{actions::ActionsModal, list::List, modal::ModalHandle, Pane},
common::{
actions::ActionsModal,
list::List,
modal::ModalHandle,
text_box::{TextBox, TextBoxEvent},
Pane,
},
component::recipe_pane::RecipeMenuAction,
context::UpdateContext,
draw::{Draw, DrawMetadata, Generate},
Expand All @@ -13,7 +19,11 @@ use crate::{
};
use derive_more::{Deref, DerefMut};
use persisted::{PersistedKey, SingletonKey};
use ratatui::{text::Text, Frame};
use ratatui::{
layout::{Constraint, Layout},
text::Text,
Frame,
};
use serde::{Deserialize, Serialize};
use slumber_config::Action;
use slumber_core::collection::{
Expand Down Expand Up @@ -48,6 +58,11 @@ pub struct RecipeListPane {
/// issue though, it just means it'll be pre-collapsed if the user ever
/// adds the folder back. Not worth working around.
collapsed: Persisted<SingletonKey<Collapsed>>,

// TODO persistence
search: Component<TextBox>,
search_focused: bool,

actions_handle: ModalHandle<ActionsModal<RecipeMenuAction>>,
}

Expand All @@ -58,18 +73,25 @@ struct SelectedRecipeKey;

impl RecipeListPane {
pub fn new(recipes: &RecipeTree) -> Self {
let input_engine = &TuiContext::get().input_engine;
let binding = input_engine.binding_display(Action::Search);

// This clone is unfortunate, but we can't hold onto a reference to the
// recipes
let collapsed: Persisted<SingletonKey<Collapsed>> =
Persisted::default();
let persistent = PersistedLazy::new(
let select = PersistedLazy::new(
SelectedRecipeKey,
collapsed.build_select_state(recipes),
);
let search =
TextBox::default().placeholder(format!("{binding} search"));
Self {
emitter_id: EmitterId::new(),
select: persistent.into(),
select: select.into(),
collapsed,
search: search.into(),
search_focused: false,
actions_handle: ModalHandle::default(),
}
}
Expand Down Expand Up @@ -124,6 +146,16 @@ impl RecipeListPane {

changed
}

/// Apply the search query, selecting the first recipe/folder in the list
/// that contains the query in its name
fn search(&mut self) {
let query = self.search.data().text().trim().to_lowercase();
self.select
.data_mut()
.get_mut()
.find(|item| item.name.to_lowercase().contains(&query));
}
}

impl EventHandler for RecipeListPane {
Expand All @@ -137,6 +169,9 @@ impl EventHandler for RecipeListPane {
Action::Right => {
self.set_selected_collapsed(CollapseState::Expand);
}
Action::Search => {
self.search_focused = true;
}
Action::OpenActions => {
let recipe = self
.select
Expand Down Expand Up @@ -174,6 +209,14 @@ impl EventHandler for RecipeListPane {
self.set_selected_collapsed(CollapseState::Toggle);
}
}
} else if let Some(event) = self.search.emitted(&event) {
match event {
TextBoxEvent::Focus => self.search_focused = true,
TextBoxEvent::Change => self.search(),
TextBoxEvent::Cancel | TextBoxEvent::Submit => {
self.search_focused = false
}
}
} else if let Some(menu_action) = self.actions_handle.emitted(&event) {
// Menu actions are handled by the parent, so forward them
self.emit(RecipeListPaneEvent::MenuAction(*menu_action));
Expand All @@ -185,7 +228,7 @@ impl EventHandler for RecipeListPane {
}

fn children(&mut self) -> Vec<Component<Child<'_>>> {
vec![self.select.to_child_mut()]
vec![self.select.to_child_mut(), self.search.to_child_mut()]
}
}

Expand All @@ -204,8 +247,19 @@ impl Draw for RecipeListPane {
let area = block.inner(metadata.area());
frame.render_widget(block, metadata.area());

self.select
.draw(frame, List::from(&**self.select.data()), area, true);
let [select_area, search_area] =
Layout::vertical([Constraint::Min(0), Constraint::Length(1)])
.areas(area);

self.select.draw(
frame,
List::from(&**self.select.data()),
select_area,
!self.search_focused,
);

self.search
.draw(frame, (), search_area, self.search_focused);
}
}

Expand Down Expand Up @@ -345,3 +399,5 @@ impl Collapsed {
.build()
}
}

// TODO search tests
9 changes: 9 additions & 0 deletions crates/tui/src/view/state/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@ impl<Item, State: SelectStateData> SelectState<Item, State> {
self.select_delta(1);
}

/// Select the first item in the list that matches the given predicate. If
/// no items match, the selection remains unchanged
pub fn find(&mut self, predicate: impl Fn(&Item) -> bool) {
let match_index = self.items().position(predicate);
if let Some(index) = match_index {
self.select_index(index);
}
}

/// Select an item by index
fn select_index(&mut self, index: usize) {
let state = self.state.get_mut();
Expand Down

0 comments on commit bd0ebd6

Please sign in to comment.