gravel_frontend_fltk/
lib.rsmod builder;
mod config;
mod scroll;
mod scrollbar;
mod structs;
#[cfg_attr(target_os = "linux", path = "native/linux.rs")]
#[cfg_attr(windows, path = "native/windows.rs")]
mod native;
use crate::structs::{Event, HitUi, Ui};
use crate::{config::Config, scroll::Scroll};
use fltk::{enums::FrameType, prelude::*};
use gravel_ffi::prelude::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
struct FltkFrontend {
config: Config,
ui: Ui,
context: BoxDynFrontendContext,
result: QueryResult,
query_token: Option<u32>,
scroll: Scroll,
visible: bool,
last_hide_time: SystemTime,
}
#[gravel_frontend("fltk")]
impl Frontend for FltkFrontend {
fn new(context: BoxDynFrontendContext, config: &PluginConfigAdapter<'_>) -> Self {
let config = config::get(config);
let ui = builder::build(&config);
let max_view_size = config.layout.max_hits;
let visible = !config.behaviour.start_hidden;
if visible {
native::activate_window(&ui.window);
}
Self {
config,
ui,
context,
result: QueryResult::default(),
query_token: None,
scroll: Scroll::new(0, max_view_size),
visible,
last_hide_time: UNIX_EPOCH,
}
}
fn run(&mut self) -> FrontendExitStatus {
self.update_window_position();
self.run_event_loop()
}
}
impl FltkFrontend {
fn run_event_loop(&mut self) -> FrontendExitStatus {
loop {
if let Err(e) = fltk::app::wait_for(0.001) {
log::error!("fltk wait_for error: {e}");
}
if fltk::app::should_program_quit() {
return self.quit(FrontendExitStatus::Exit);
}
if let Some(exit) = self.receive_message() {
return self.quit(exit);
}
if self.query_token.is_some() {
continue;
}
if let Some(exit) = self.receive_event() {
return self.quit(exit);
}
}
}
fn receive_event(&mut self) -> Option<FrontendExitStatus> {
match self.ui.receiver.recv()? {
Event::Query => self.query(),
Event::Confirm(kind) => self.confirm(kind),
Event::CursorUp => self.cursor_up(),
Event::CursorDown => self.cursor_down(),
Event::PageUp => self.cursor_page_up(),
Event::PageDown => self.cursor_page_down(),
Event::CursorTop => self.cursor_top(),
Event::CursorBottom => self.cursor_bottom(),
Event::Cancel | Event::HideWindow => self.hide(),
Event::Exit => return Some(FrontendExitStatus::Exit),
}
None
}
fn receive_message(&mut self) -> Option<FrontendExitStatus> {
use FrontendMessage as M;
match self.context.recv()? {
M::QueryResult(token, result) => self.update_result(token, result),
M::ShowOrHide => self.show_or_hide(),
M::Show => self.show(),
M::Hide => self.hide(),
M::ShowWithQuery(query) => self.show_with(&query),
M::Refresh => self.force_query(),
M::Exit => return Some(FrontendExitStatus::Exit),
M::Restart => return Some(FrontendExitStatus::Restart),
M::ClearCaches => (),
}
None
}
fn quit(&self, exit: FrontendExitStatus) -> FrontendExitStatus {
fltk::app::quit();
fltk::app::wait();
exit
}
fn show_or_hide(&mut self) {
if self.visible {
self.hide();
} else {
self.show();
}
}
fn hide(&mut self) {
if self.config.behaviour.exit_on_hide {
self.ui.sender.send(Event::Exit);
return;
}
log::trace!("hiding frontend");
self.ui.window.hide();
self.visible = false;
self.last_hide_time = SystemTime::now();
}
fn show(&mut self) {
if self.should_ignore_show() {
log::trace!("ignoring show request");
return;
}
log::trace!("showing frontend");
self.input_select_all();
self.update_window_position();
self.ui.window.show();
self.visible = true;
native::activate_window(&self.ui.window);
}
fn should_ignore_show(&self) -> bool {
let Some(millis) = self.config.behaviour.window_hide_debounce else {
return false;
};
let block_duration = Duration::from_millis(millis);
let elapsed = SystemTime::now()
.duration_since(self.last_hide_time)
.unwrap_or(Duration::MAX);
elapsed <= block_duration
}
fn show_with(&mut self, query: &str) {
self.show();
self.ui.input.set_value(query);
self.force_query();
}
fn input_select_all(&mut self) {
self.ui.input.set_position(i32::MIN).ok();
self.ui.input.set_mark(i32::MAX).ok();
}
fn query(&mut self) {
if self.ui.input.changed() {
self.force_query();
}
}
fn force_query(&mut self) {
let token = self.context.query(self.ui.input.value().into_c());
self.ui.input.clear_changed();
self.query_token = Some(token);
}
fn update_result(&mut self, token: u32, result: QueryResult) {
if self.query_token != Some(token) {
return;
}
self.result = result;
self.query_token = None;
self.update_window_height();
self.update_hits();
}
fn confirm(&self, kind: ActionKind) {
if self.result.hits.is_empty() {
return;
}
let cursor = self.scroll.cursor();
let hit = &self.result.hits[cursor as usize].hit;
self.context.run_hit_action(hit, kind);
}
fn cursor_up(&mut self) {
self.scroll.cursor_up();
self.update_hits();
}
fn cursor_down(&mut self) {
self.scroll.cursor_down();
self.update_hits();
}
fn cursor_page_up(&mut self) {
self.scroll.page_up();
self.update_hits();
}
fn cursor_page_down(&mut self) {
self.scroll.page_down();
self.update_hits();
}
fn cursor_top(&mut self) {
self.scroll.top();
self.update_hits();
}
fn cursor_bottom(&mut self) {
self.scroll.bottom();
self.update_hits();
}
fn update_hits(&mut self) {
for (i, hit_ui) in self.ui.hits.iter_mut().enumerate() {
let position = self.scroll.scroll() + i as i32;
let selected = position == self.scroll.cursor();
let hit = self.result.hits.get(position as usize);
update_hit(hit_ui, hit, selected, self.config.behaviour.show_scores);
}
self.update_scrollbar();
}
fn update_scrollbar(&mut self) {
if self.scroll.view_size() >= self.result.hits.len() as i32 {
self.ui.scrollbar.hide();
} else {
self.ui.scrollbar.show();
}
let pos = self.scroll.scroll() as f32 / self.scroll.length() as f32;
let size = self.scroll.view_size() as f32 / self.scroll.length() as f32;
self.ui.scrollbar.set_slider_position(pos);
self.ui.scrollbar.set_slider_size(size);
}
fn update_window_height(&mut self) {
self.scroll.set_length(self.result.hits.len() as i32);
let height = builder::get_window_height(&self.config, self.scroll.view_size());
self.ui.window.set_size(self.config.layout.window_width, height);
}
fn update_window_position(&mut self) {
if !self.config.behaviour.auto_center_window {
return;
}
let width = self.config.layout.window_width;
let max_height = builder::get_window_height(&self.config, self.config.layout.max_hits);
let (mx, my) = fltk::app::get_mouse();
let screen_num = fltk::app::screen_num(mx, my);
let (sx, sy, sw, sh) = fltk::app::screen_xywh(screen_num);
let x = sx + (sw - width) / 2;
let y = sy + (sh - max_height) / 2;
log::trace!("setting position: ({x}, {y}) on screen {screen_num} ({sx}, {sy}, {sw}, {sh})");
self.ui.window.set_screen_num(screen_num);
self.ui.window.set_pos(x, y);
}
}
fn update_hit(hit_ui: &mut HitUi, hit: Option<&ScoredHit>, selected: bool, show_score: bool) {
let title = hit.map_or("", |h| h.hit.title().as_str());
let subtitle = hit.map_or("", |h| h.hit.subtitle().as_str());
hit_ui.title.set_label(title);
if show_score {
let format = format!("[{}] {}", hit.map_or(0, |h| h.score), subtitle);
hit_ui.subtitle.set_label(&format);
} else {
hit_ui.subtitle.set_label(subtitle);
}
let frame_type = match selected {
true => FrameType::FlatBox,
false => FrameType::NoBox,
};
hit_ui.group.set_frame(frame_type);
}