feat: filter modes (fuzzy, contains, and exact)
1 file changed, +65 -2
+65-2src/widgets/miller.rs
| @@ -110,10 +110,52 @@ pub struct MillerFilterResponse { | ||
| 110 | 110 | |
| 111 | 111 | /// Always-visible filter bar for a miller column. |
| 112 | 112 | /// Shows a text input with placeholder. Focus with shortcut key, Escape unfocuses. |
| 113 | +#[derive(Debug, Clone, Copy, PartialEq)] | |
| 114 | +pub enum FilterMode { | |
| 115 | + Fuzzy, // chars in order anywhere | |
| 116 | + Contains, // case-insensitive substring | |
| 117 | + Exact, // case-insensitive full match | |
| 118 | +} | |
| 119 | + | |
| 120 | +impl FilterMode { | |
| 121 | + fn next(self) -> Self { | |
| 122 | + match self { | |
| 123 | + FilterMode::Fuzzy => FilterMode::Contains, | |
| 124 | + FilterMode::Contains => FilterMode::Exact, | |
| 125 | + FilterMode::Exact => FilterMode::Fuzzy, | |
| 126 | + } | |
| 127 | + } | |
| 128 | + | |
| 129 | + fn icon(self) -> &'static str { | |
| 130 | + match self { | |
| 131 | + FilterMode::Fuzzy => egui_phosphor::regular::WAVES, | |
| 132 | + FilterMode::Contains => egui_phosphor::regular::TEXTBOX, | |
| 133 | + FilterMode::Exact => egui_phosphor::regular::EQUALS, | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + fn color(self) -> egui::Color32 { | |
| 138 | + match self { | |
| 139 | + FilterMode::Fuzzy => crate::theme::CatppuccinMocha::PEACH, | |
| 140 | + FilterMode::Contains => crate::theme::CatppuccinMocha::BLUE, | |
| 141 | + FilterMode::Exact => crate::theme::CatppuccinMocha::GREEN, | |
| 142 | + } | |
| 143 | + } | |
| 144 | + | |
| 145 | + fn tooltip(self) -> &'static str { | |
| 146 | + match self { | |
| 147 | + FilterMode::Fuzzy => "Fuzzy (ctrl-f to cycle)", | |
| 148 | + FilterMode::Contains => "Contains (ctrl-f to cycle)", | |
| 149 | + FilterMode::Exact => "Exact (ctrl-f to cycle)", | |
| 150 | + } | |
| 151 | + } | |
| 152 | +} | |
| 153 | + | |
| 113 | 154 | pub struct MillerFilter { |
| 114 | 155 | pub query: String, |
| 115 | 156 | id: &'static str, |
| 116 | 157 | focus_next: bool, |
| 158 | + pub mode: FilterMode, | |
| 117 | 159 | } |
| 118 | 160 | |
| 119 | 161 | impl MillerFilter { |
| @@ -122,6 +164,7 @@ impl MillerFilter { | ||
| 122 | 164 | query: String::new(), |
| 123 | 165 | id, |
| 124 | 166 | focus_next: false, |
| 167 | + mode: FilterMode::Fuzzy, | |
| 125 | 168 | } |
| 126 | 169 | } |
| 127 | 170 | |
| @@ -156,6 +199,17 @@ impl MillerFilter { | ||
| 156 | 199 | .size(12.0), |
| 157 | 200 | ); |
| 158 | 201 | |
| 202 | + // Filter mode toggle | |
| 203 | + if ui.add( | |
| 204 | + egui::Button::new( | |
| 205 | + egui::RichText::new(self.mode.icon()) | |
| 206 | + .color(self.mode.color()) | |
| 207 | + .size(11.0) | |
| 208 | + ).frame(false) | |
| 209 | + ).on_hover_text(self.mode.tooltip()).clicked() { | |
| 210 | + self.mode = self.mode.next(); | |
| 211 | + } | |
| 212 | + | |
| 159 | 213 | let id = egui::Id::new(self.id); |
| 160 | 214 | let r = ui.add( |
| 161 | 215 | egui::TextEdit::singleline(&mut self.query) |
| @@ -180,6 +234,10 @@ impl MillerFilter { | ||
| 180 | 234 | |
| 181 | 235 | if has_focus || lost_focus { |
| 182 | 236 | if has_focus { |
| 237 | + // Ctrl-F: cycle filter mode | |
| 238 | + if ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, egui::Key::F)) { | |
| 239 | + self.mode = self.mode.next(); | |
| 240 | + } | |
| 183 | 241 | // Escape: unfocus (don't clear) |
| 184 | 242 | if ui.input(|i| i.key_pressed(egui::Key::Escape)) { |
| 185 | 243 | r.surrender_focus(); |
| @@ -202,7 +260,7 @@ impl MillerFilter { | ||
| 202 | 260 | resp |
| 203 | 261 | } |
| 204 | 262 | |
| 205 | - /// Fuzzy match a label against the query. Returns true if it matches (or query is empty). | |
| 263 | + /// Match a label against the query using the current filter mode. | |
| 206 | 264 | /// Supports `|` to OR multiple terms (e.g. "start|report" matches either). |
| 207 | 265 | pub fn matches(&self, label: &str) -> bool { |
| 208 | 266 | if self.query.is_empty() { |
| @@ -211,7 +269,12 @@ impl MillerFilter { | ||
| 211 | 269 | self.query.split('|') |
| 212 | 270 | .any(|term| { |
| 213 | 271 | let term = term.trim(); |
| 214 | - !term.is_empty() && fuzzy_matches(term, label) | |
| 272 | + if term.is_empty() { return false; } | |
| 273 | + match self.mode { | |
| 274 | + FilterMode::Fuzzy => fuzzy_matches(term, label), | |
| 275 | + FilterMode::Contains => label.to_lowercase().contains(&term.to_lowercase()), | |
| 276 | + FilterMode::Exact => label.to_lowercase() == term.to_lowercase(), | |
| 277 | + } | |
| 215 | 278 | }) |
| 216 | 279 | } |
| 217 | 280 |