kimun_notes/components/
query_list_panel.rs1use ratatui::Frame;
11use ratatui::crossterm::event::KeyCode;
12use ratatui::layout::{Constraint, Direction, Layout, Rect};
13
14use crate::components::event_state::EventState;
15use crate::components::events::{AppEvent, AppTx, InputEvent, redraw_callback};
16use crate::components::panel::panel_block;
17use crate::components::search_list::{
18 Filter, KeyReaction, RowSource, SearchList, SearchMouse, SearchRow,
19};
20use crate::settings::icons::Icons;
21use crate::settings::themes::Theme;
22
23pub trait ListPanelSpec {
26 type Row: SearchRow + Clone + Send + Sync + 'static;
27
28 const TITLE: &'static str;
30 const HAS_FILTER: bool = true;
34
35 fn submit(row: &Self::Row, tx: &AppTx);
37
38 fn context_event(_row: &Self::Row) -> Option<AppEvent> {
41 None
42 }
43
44 fn hints() -> Vec<(String, String)>;
45}
46
47pub struct QueryListPanel<S: ListPanelSpec> {
50 icons: Icons,
51 list: Option<SearchList<S::Row>>,
52}
53
54impl<S: ListPanelSpec> QueryListPanel<S> {
55 pub fn new(icons: Icons) -> Self {
56 Self { icons, list: None }
57 }
58
59 pub fn set_source(&mut self, source: impl RowSource<S::Row> + 'static, tx: &AppTx) {
62 let mut builder = SearchList::builder(source, redraw_callback(tx.clone()));
63 if S::HAS_FILTER {
64 builder = builder.filter(Filter::Fuzzy);
65 }
66 self.list = Some(builder.icons(self.icons.clone()).build());
67 }
68
69 pub fn is_loaded(&self) -> bool {
70 self.list.is_some()
71 }
72
73 pub fn selected_row(&self) -> Option<&S::Row> {
74 self.list.as_ref().and_then(|l| l.selected_row())
75 }
76
77 pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
78 S::hints()
79 }
80
81 fn submit_selected(&self, tx: &AppTx) {
82 if let Some(row) = self.selected_row() {
83 S::submit(row, tx);
84 }
85 }
86
87 pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
88 match event {
89 InputEvent::Key(key) => {
90 let Some(list) = &mut self.list else {
91 return EventState::NotConsumed;
92 };
93 if S::HAS_FILTER {
94 match list.handle_key(key) {
95 KeyReaction::Submit => {
96 self.submit_selected(tx);
97 EventState::Consumed
98 }
99 KeyReaction::Consumed | KeyReaction::Cancel => EventState::Consumed,
100 KeyReaction::Intercepted(_) | KeyReaction::Unhandled => {
101 EventState::NotConsumed
102 }
103 }
104 } else {
105 match key.code {
108 KeyCode::Up
109 | KeyCode::Down
110 | KeyCode::PageUp
111 | KeyCode::PageDown
112 | KeyCode::Home
113 | KeyCode::End => {
114 list.handle_key(key);
115 EventState::Consumed
116 }
117 KeyCode::Enter => {
118 self.submit_selected(tx);
119 EventState::Consumed
120 }
121 _ => EventState::NotConsumed,
122 }
123 }
124 }
125 InputEvent::Mouse(mouse) => {
126 let Some(list) = &mut self.list else {
127 return EventState::NotConsumed;
128 };
129 match list.handle_mouse(mouse) {
130 SearchMouse::Activated(_) => self.submit_selected(tx),
131 SearchMouse::Context(_) => {
132 if let Some(event) = list.selected_row().and_then(S::context_event) {
133 tx.send(event).ok();
134 }
135 }
136 _ => {}
137 }
138 EventState::Consumed
139 }
140 _ => EventState::NotConsumed,
141 }
142 }
143
144 pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
146 let block = panel_block(S::TITLE, theme, focused);
147 let inner = block.inner(rect);
148 f.render_widget(block, rect);
149 self.render_in(f, inner, rect, theme, focused);
150 }
151
152 pub fn render_in(
156 &mut self,
157 f: &mut Frame,
158 body: Rect,
159 panel: Rect,
160 theme: &Theme,
161 focused: bool,
162 ) {
163 let Some(list) = &mut self.list else {
164 return;
165 };
166 if S::HAS_FILTER {
167 let rows = Layout::default()
168 .direction(Direction::Vertical)
169 .constraints([Constraint::Length(1), Constraint::Min(0)])
170 .split(body);
171 list.render_query(f, rows[0], theme, focused);
172 list.render(f, rows[1], theme, focused);
173 list.set_list_rect(rows[1]);
174 } else {
175 list.render(f, body, theme, focused);
176 list.set_list_rect(body);
177 }
178 list.set_panel_rect(panel);
179 }
180
181 #[cfg(test)]
183 pub(crate) fn list_mut(&mut self) -> Option<&mut SearchList<S::Row>> {
184 self.list.as_mut()
185 }
186
187 #[cfg(test)]
188 pub(crate) fn list(&self) -> Option<&SearchList<S::Row>> {
189 self.list.as_ref()
190 }
191}