gravel_frontend_fltk/
builder.rs1use crate::config::Config;
2use crate::scrollbar::Scrollbar;
3use crate::structs::{Event, HitUi, Ui};
4use fltk::enums::{Align, Event as FltkEvent, FrameType, Key};
5use fltk::{app, app::Sender, frame::Frame, group::Group, input::Input, prelude::*, window::Window};
6use gravel_ffi::ActionKind;
7
8const WINDOW_TITLE: &str = "gravel";
9const WM_CLASS: &str = "gravel";
10
11pub fn get_window_height(config: &Config, hit_count: i32) -> i32 {
13 let padding = match hit_count {
14 0 => 0,
15 _ => config.layout.padding,
16 };
17
18 config.layout.window_min_height + config.layout.hit_height * hit_count + padding
19}
20
21pub fn build(config: &Config) -> Ui {
23 let app = app::App::default().with_scheme(app::Scheme::Gtk);
24 app::set_visible_focus(false);
25
26 let (sender, receiver) = app::channel::<Event>();
27
28 let mut window = build_window(config);
29 let mut input = build_input(config);
30
31 let do_auto_hide = config.behaviour.auto_hide;
32 window.handle(move |_window, event| on_window_event(event, &sender, do_auto_hide));
33
34 input.handle(move |_input, event| on_input_event(event, &sender));
35
36 let hits = (0..config.layout.max_hits).map(|i| build_hit(i, config)).collect();
37 let scrollbar = build_scrollbar(config);
38
39 window.end();
40 window.show();
41
42 if config.behaviour.start_hidden {
43 app::add_timeout3(0.05, move |_handle| sender.send(Event::HideWindow));
46 }
47
48 Ui {
49 window,
50 _app: app,
51 input,
52 scrollbar,
53 hits,
54 receiver,
55 sender,
56 }
57}
58
59fn build_window(config: &Config) -> Window {
60 let mut window = Window::default()
61 .with_size(config.layout.window_width, get_window_height(config, 0))
62 .with_label(WINDOW_TITLE);
63
64 window.set_color(config.colors.background);
65 window.set_border(config.layout.window_decorations);
66 window.set_xclass(WM_CLASS);
67
68 if config.layout.window_border {
69 window.set_frame(FrameType::BorderBox);
70 }
71
72 window
73}
74
75fn build_input(config: &Config) -> Input {
76 let mut input = Input::default()
77 .with_pos(config.layout.padding, config.layout.padding)
78 .with_size(config.layout.query_width, config.layout.query_height);
79
80 input.set_text_size(config.layout.query_font_size);
81 input.set_frame(FrameType::FlatBox);
82 input.set_color(config.colors.query_background);
83 input.set_text_color(config.colors.query_text);
84 input.set_selection_color(config.colors.query_highlight);
85 input.set_cursor_color(config.colors.query_cursor);
86
87 input
88}
89
90fn build_scrollbar(config: &Config) -> Scrollbar {
91 Scrollbar::default()
92 .with_pos(config.layout.scrollbar_x, config.layout.scrollbar_y)
93 .with_size(config.layout.scrollbar_width, config.layout.scrollbar_height)
94 .with_padding(config.layout.scrollbar_padding)
95 .with_colors(config.colors.background, config.colors.scrollbar)
96}
97
98fn build_hit(i: i32, config: &Config) -> HitUi {
99 let y = config.layout.hit_start_y + config.layout.hit_height * i;
100
101 let mut group = Group::default()
102 .with_pos(config.layout.padding, y)
103 .with_size(config.layout.hit_width, config.layout.hit_height);
104
105 group.set_color(config.colors.hit_highlight);
106 group.set_frame(FrameType::FlatBox);
107
108 let mut title = Frame::default()
109 .with_pos(config.layout.padding, y)
110 .with_size(config.layout.hit_width, config.layout.hit_title_height)
111 .with_align(Align::BottomLeft | Align::Inside | Align::Clip);
112
113 title.set_label_size(config.layout.hit_title_font_size);
114 title.set_label_color(config.colors.hit_title);
115
116 let mut subtitle = Frame::default()
117 .with_pos(config.layout.padding, y + config.layout.hit_title_height)
118 .with_size(config.layout.hit_width, config.layout.hit_subtitle_height)
119 .with_align(Align::TopLeft | Align::Inside | Align::Clip);
120
121 subtitle.set_label_size(config.layout.hit_subtitle_font_size);
122 subtitle.set_label_color(config.colors.hit_subtitle);
123
124 group.show();
125 group.end();
126
127 HitUi { group, title, subtitle }
128}
129
130fn on_window_event(event: FltkEvent, sender: &Sender<Event>, do_auto_hide: bool) -> bool {
131 let message = match event {
132 FltkEvent::Unfocus if do_auto_hide => Event::HideWindow,
133 _ => return false,
134 };
135
136 sender.send(message);
137
138 true
139}
140
141fn on_input_event(event: FltkEvent, sender: &Sender<Event>) -> bool {
142 match event {
143 FltkEvent::KeyDown | FltkEvent::Paste => on_input_keydown(app::event_key(), sender),
144 _ => return false,
145 };
146
147 true
148}
149
150fn on_input_keydown(key: Key, sender: &Sender<Event>) {
151 let message = match key {
152 Key::Escape => Event::Cancel,
153 Key::Enter | Key::KPEnter if shift_down() => Event::Confirm(ActionKind::Secondary),
154 Key::Enter | Key::KPEnter => Event::Confirm(ActionKind::Primary),
155 Key::Up => Event::CursorUp,
156 Key::Down => Event::CursorDown,
157 Key::PageUp => Event::PageUp,
158 Key::PageDown => Event::PageDown,
159 Key::Home if ctrl_down() => Event::CursorTop,
160 Key::End if ctrl_down() => Event::CursorBottom,
161 _ => Event::Query,
162 };
163
164 sender.send(message);
165}
166
167fn ctrl_down() -> bool {
168 app::event_key_down(Key::ControlL) || app::event_key_down(Key::ControlR)
169}
170
171fn shift_down() -> bool {
172 app::event_key_down(Key::ShiftL) || app::event_key_down(Key::ShiftR)
173}