gravel_frontend_fltk/
lib.rs1mod builder;
4mod config;
5mod scroll;
6mod scrollbar;
7mod structs;
8
9#[cfg_attr(target_os = "linux", path = "native/linux.rs")]
10#[cfg_attr(windows, path = "native/windows.rs")]
11mod native;
12
13use crate::structs::{Event, HitUi, Ui};
14use crate::{config::Config, scroll::Scroll};
15use fltk::{enums::FrameType, prelude::*};
16use gravel_ffi::prelude::*;
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18
19struct FltkFrontend {
20 config: Config,
21 ui: Ui,
22 context: BoxDynFrontendContext,
23 result: QueryResult,
24 query_token: Option<u32>,
25 scroll: Scroll,
26 visible: bool,
27 last_hide_time: SystemTime,
28}
29
30#[gravel_frontend("fltk")]
31impl Frontend for FltkFrontend {
32 fn new(context: BoxDynFrontendContext, config: &PluginConfigAdapter<'_>) -> Self {
33 let config = config::get(config);
34 let ui = builder::build(&config);
35 let max_view_size = config.layout.max_hits;
36 let visible = !config.behaviour.start_hidden;
37
38 if visible {
39 native::activate_window(&ui.window);
40 }
41
42 Self {
43 config,
44 ui,
45 context,
46 result: QueryResult::default(),
47 query_token: None,
48 scroll: Scroll::new(0, max_view_size),
49 visible,
50 last_hide_time: UNIX_EPOCH,
51 }
52 }
53
54 fn run(&mut self) -> FrontendExitStatus {
55 self.update_window_position();
56
57 self.run_event_loop()
58 }
59}
60
61impl FltkFrontend {
62 fn run_event_loop(&mut self) -> FrontendExitStatus {
64 loop {
65 if let Err(e) = fltk::app::wait_for(0.001) {
66 log::error!("fltk wait_for error: {e}");
68 }
69
70 if fltk::app::should_program_quit() {
71 return self.quit(FrontendExitStatus::Exit);
72 }
73
74 if let Some(exit) = self.receive_message() {
75 return self.quit(exit);
76 }
77
78 if self.query_token.is_some() {
80 continue;
81 }
82
83 if let Some(exit) = self.receive_event() {
84 return self.quit(exit);
85 }
86 }
87 }
88
89 fn receive_event(&mut self) -> Option<FrontendExitStatus> {
90 match self.ui.receiver.recv()? {
91 Event::Query => self.query(),
92 Event::Confirm(kind) => self.confirm(kind),
93 Event::CursorUp => self.cursor_up(),
94 Event::CursorDown => self.cursor_down(),
95 Event::PageUp => self.cursor_page_up(),
96 Event::PageDown => self.cursor_page_down(),
97 Event::CursorTop => self.cursor_top(),
98 Event::CursorBottom => self.cursor_bottom(),
99 Event::Cancel | Event::HideWindow => self.hide(),
100 Event::Exit => return Some(FrontendExitStatus::Exit),
101 }
102
103 None
104 }
105
106 fn receive_message(&mut self) -> Option<FrontendExitStatus> {
107 use FrontendMessage as M;
108 match self.context.recv()? {
109 M::QueryResult(token, result) => self.update_result(token, result),
110 M::ShowOrHide => self.show_or_hide(),
111 M::Show => self.show(),
112 M::Hide => self.hide(),
113 M::ShowWithQuery(query) => self.show_with(&query),
114 M::Refresh => self.force_query(),
115 M::Exit => return Some(FrontendExitStatus::Exit),
116 M::Restart => return Some(FrontendExitStatus::Restart),
117 M::ClearCaches => (),
118 }
119
120 None
121 }
122
123 fn quit(&self, exit: FrontendExitStatus) -> FrontendExitStatus {
124 fltk::app::quit();
125 fltk::app::wait();
126 exit
127 }
128
129 fn show_or_hide(&mut self) {
130 if self.visible {
131 self.hide();
132 } else {
133 self.show();
134 }
135 }
136
137 fn hide(&mut self) {
138 if self.config.behaviour.exit_on_hide {
139 self.ui.sender.send(Event::Exit);
140 return;
141 }
142
143 log::trace!("hiding frontend");
144 self.ui.window.hide();
145 self.visible = false;
146 self.last_hide_time = SystemTime::now();
147 }
148
149 fn show(&mut self) {
150 if self.should_ignore_show() {
151 log::trace!("ignoring show request");
152 return;
153 }
154
155 log::trace!("showing frontend");
156
157 self.input_select_all();
159
160 self.update_window_position();
161 self.ui.window.show();
162 self.visible = true;
163
164 native::activate_window(&self.ui.window);
166 }
167
168 fn should_ignore_show(&self) -> bool {
173 let Some(millis) = self.config.behaviour.window_hide_debounce else {
174 return false;
175 };
176
177 let block_duration = Duration::from_millis(millis);
178 let elapsed = SystemTime::now()
179 .duration_since(self.last_hide_time)
180 .unwrap_or(Duration::MAX);
181
182 elapsed <= block_duration
183 }
184
185 fn show_with(&mut self, query: &str) {
187 self.show();
188 self.ui.input.set_value(query);
189 self.force_query();
190 }
191
192 fn input_select_all(&mut self) {
193 self.ui.input.set_position(i32::MIN).ok();
194 self.ui.input.set_mark(i32::MAX).ok();
195 }
196
197 fn query(&mut self) {
199 if self.ui.input.changed() {
200 self.force_query();
201 }
202 }
203
204 fn force_query(&mut self) {
205 let token = self.context.query(self.ui.input.value().into_c());
206 self.ui.input.clear_changed();
207
208 self.query_token = Some(token);
209 }
210
211 fn update_result(&mut self, token: u32, result: QueryResult) {
212 if self.query_token != Some(token) {
213 return;
214 }
215
216 self.result = result;
217 self.query_token = None;
218
219 self.update_window_height();
220 self.update_hits();
221 }
222
223 fn confirm(&self, kind: ActionKind) {
225 if self.result.hits.is_empty() {
226 return;
227 }
228
229 let cursor = self.scroll.cursor();
230 let hit = &self.result.hits[cursor as usize].hit;
231
232 self.context.run_hit_action(hit, kind);
233 }
234
235 fn cursor_up(&mut self) {
236 self.scroll.cursor_up();
237 self.update_hits();
238 }
239
240 fn cursor_down(&mut self) {
241 self.scroll.cursor_down();
242 self.update_hits();
243 }
244
245 fn cursor_page_up(&mut self) {
246 self.scroll.page_up();
247 self.update_hits();
248 }
249
250 fn cursor_page_down(&mut self) {
251 self.scroll.page_down();
252 self.update_hits();
253 }
254
255 fn cursor_top(&mut self) {
256 self.scroll.top();
257 self.update_hits();
258 }
259
260 fn cursor_bottom(&mut self) {
261 self.scroll.bottom();
262 self.update_hits();
263 }
264
265 fn update_hits(&mut self) {
267 for (i, hit_ui) in self.ui.hits.iter_mut().enumerate() {
268 let position = self.scroll.scroll() + i as i32;
269 let selected = position == self.scroll.cursor();
270
271 let hit = self.result.hits.get(position as usize);
272 update_hit(hit_ui, hit, selected, self.config.behaviour.show_scores);
273 }
274
275 self.update_scrollbar();
276 }
277
278 fn update_scrollbar(&mut self) {
280 if self.scroll.view_size() >= self.result.hits.len() as i32 {
282 self.ui.scrollbar.hide();
283 } else {
284 self.ui.scrollbar.show();
285 }
286
287 let pos = self.scroll.scroll() as f32 / self.scroll.length() as f32;
288 let size = self.scroll.view_size() as f32 / self.scroll.length() as f32;
289 self.ui.scrollbar.set_slider_position(pos);
290 self.ui.scrollbar.set_slider_size(size);
291 }
292
293 fn update_window_height(&mut self) {
295 self.scroll.set_length(self.result.hits.len() as i32);
296 let height = builder::get_window_height(&self.config, self.scroll.view_size());
297 self.ui.window.set_size(self.config.layout.window_width, height);
298 }
299
300 fn update_window_position(&mut self) {
301 if !self.config.behaviour.auto_center_window {
302 return;
303 }
304
305 let width = self.config.layout.window_width;
306 let max_height = builder::get_window_height(&self.config, self.config.layout.max_hits);
307
308 let (mx, my) = fltk::app::get_mouse();
309 let screen_num = fltk::app::screen_num(mx, my);
310
311 let (sx, sy, sw, sh) = fltk::app::screen_xywh(screen_num);
312
313 let x = sx + (sw - width) / 2;
314 let y = sy + (sh - max_height) / 2;
315
316 log::trace!("setting position: ({x}, {y}) on screen {screen_num} ({sx}, {sy}, {sw}, {sh})");
317
318 self.ui.window.set_screen_num(screen_num);
319 self.ui.window.set_pos(x, y);
320 }
321}
322
323fn update_hit(hit_ui: &mut HitUi, hit: Option<&ScoredHit>, selected: bool, show_score: bool) {
327 let title = hit.map_or("", |h| h.hit.title().as_str());
328 let subtitle = hit.map_or("", |h| h.hit.subtitle().as_str());
329
330 hit_ui.title.set_label(title);
331
332 if show_score {
333 let format = format!("[{}] {}", hit.map_or(0, |h| h.score), subtitle);
334 hit_ui.subtitle.set_label(&format);
335 } else {
336 hit_ui.subtitle.set_label(subtitle);
337 }
338
339 let frame_type = match selected {
340 true => FrameType::FlatBox,
341 false => FrameType::NoBox,
342 };
343
344 hit_ui.group.set_frame(frame_type);
345}