gravel_frontend_fltk/
builder.rs

1use 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
11/// Get the window's target size given the number of hits displayed.
12pub 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
21/// Constructs the UI.
22pub 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		// HACK: hiding the window right after it's created doesn't work on linux
44		// and causes high cpu usage on windows, so wait a bit and then hide it.
45		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}