1use gdk4::Display;
2use gtk4::prelude::*;
3use gtk4::{
4 Application, ApplicationWindow, Box, Entry, Image, Label as GtkLabel, ListBox, ListBoxRow,
5 ScrolledWindow, glib,
6};
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11use crate::item_list::load_all_items;
12use crate::launch_item::LaunchItem;
13
14#[derive(Debug)]
16pub struct LaunchItemRow {
17 row: ListBoxRow,
18 item: LaunchItem,
19}
20
21impl LaunchItemRow {
22 pub fn new(item: LaunchItem) -> Self {
23 let row = ListBoxRow::new();
24 let hbox = Box::new(gtk4::Orientation::Horizontal, 12);
25 hbox.set_margin_top(8);
26 hbox.set_margin_bottom(8);
27 hbox.set_margin_start(12);
28 hbox.set_margin_end(12);
29
30 let icon = if let Some(icon_name) = &item.icon_name {
32 Image::from_icon_name(icon_name)
33 } else {
34 Image::from_icon_name("application-x-executable")
35 };
36 icon.set_pixel_size(32);
37
38 let vbox = Box::new(gtk4::Orientation::Vertical, 2);
40
41 let name_label = GtkLabel::new(Some(&item.name));
42 name_label.set_halign(gtk4::Align::Start);
43 name_label.set_markup(&format!("<b>{}</b>", glib::markup_escape_text(&item.name)));
44
45 let desc_label = GtkLabel::new(Some(&item.description));
46 desc_label.set_halign(gtk4::Align::Start);
47 desc_label.add_css_class("dim-label");
48 desc_label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
49
50 vbox.append(&name_label);
51 vbox.append(&desc_label);
52
53 hbox.append(&icon);
54 hbox.append(&vbox);
55
56 row.set_child(Some(&hbox));
57
58 Self { row, item }
59 }
60
61 pub fn list_box_row(&self) -> &ListBoxRow {
62 &self.row
63 }
64
65 pub fn launch_item(&self) -> &LaunchItem {
66 &self.item
67 }
68}
69
70pub fn build_ui(app: &Application) {
71 let window = ApplicationWindow::builder()
73 .application(app)
74 .title("Free Launch")
75 .default_width(600)
76 .default_height(400)
77 .resizable(true)
78 .build();
79
80 center_window(&window);
82
83 let content_box = Box::new(gtk4::Orientation::Vertical, 12);
85 content_box.set_margin_top(20);
86 content_box.set_margin_bottom(20);
87 content_box.set_margin_start(20);
88 content_box.set_margin_end(20);
89
90 let search_entry = Entry::builder()
92 .placeholder_text("Type to search applications...")
93 .build();
94
95 let scrolled_window = ScrolledWindow::builder()
97 .hscrollbar_policy(gtk4::PolicyType::Never)
98 .vscrollbar_policy(gtk4::PolicyType::Automatic)
99 .vexpand(true)
100 .build();
101
102 let list_box = ListBox::new();
104 list_box.set_selection_mode(gtk4::SelectionMode::Single);
105 scrolled_window.set_child(Some(&list_box));
106
107 let items = Rc::new(RefCell::new(load_all_items()));
109 let filtered_items = Rc::new(RefCell::new(Vec::new()));
110 let launch_rows = Rc::new(RefCell::new(HashMap::<u64, LaunchItemRow>::new()));
111
112 populate_list(
114 &list_box,
115 &items.borrow(),
116 &mut filtered_items.borrow_mut(),
117 &mut launch_rows.borrow_mut(),
118 );
119
120 if let Some(first_row) = list_box.first_child() {
122 if let Ok(row) = first_row.downcast::<ListBoxRow>() {
123 list_box.select_row(Some(&row));
124 }
125 }
126
127 let list_box_clone = list_box.clone();
129 let items_clone = items.clone();
130 let filtered_items_clone = filtered_items.clone();
131 let launch_rows_clone = launch_rows.clone();
132
133 search_entry.connect_changed(move |entry| {
134 let query = entry.text().to_lowercase();
135 filter_and_update_list(
136 &list_box_clone,
137 &items_clone.borrow(),
138 &mut filtered_items_clone.borrow_mut(),
139 &mut launch_rows_clone.borrow_mut(),
140 &query,
141 );
142 });
143
144 content_box.append(&search_entry);
146 content_box.append(&scrolled_window);
147 window.set_child(Some(&content_box));
148
149 setup_list_box_click_handler(&list_box, &window, launch_rows.clone());
151
152 setup_keyboard_shortcuts(&window, &search_entry, &list_box, launch_rows.clone());
154 setup_search_entry_shortcuts(&search_entry, &list_box, &window, launch_rows.clone());
155 setup_search_entry_activate(&search_entry, &list_box, &window, launch_rows);
156
157 search_entry.grab_focus();
159
160 window.present();
162}
163
164fn center_window(window: &ApplicationWindow) {
165 if let Some(display) = Display::default() {
167 if let Some(monitor) = display.monitors().item(0) {
169 if let Some(monitor) = monitor.downcast_ref::<gdk4::Monitor>() {
170 let geometry = monitor.geometry();
171 let window_width = geometry.width() / 2;
172 let window_height = geometry.height() / 2;
173
174 window.set_default_size(window_width, window_height);
177 }
178 }
179 }
180}
181
182fn populate_list(
183 list_box: &ListBox,
184 items: &[LaunchItem],
185 filtered_items: &mut Vec<LaunchItem>,
186 launch_rows: &mut HashMap<u64, LaunchItemRow>,
187) {
188 while let Some(child) = list_box.first_child() {
190 list_box.remove(&child);
191 }
192
193 filtered_items.clear();
194 filtered_items.extend_from_slice(items);
195 launch_rows.clear();
196
197 for item in items {
199 let launch_row = LaunchItemRow::new(item.clone());
200 let row_ptr = launch_row.list_box_row().as_ptr() as u64;
201 list_box.append(launch_row.list_box_row());
202 launch_rows.insert(row_ptr, launch_row);
203 }
204}
205
206fn filter_and_update_list(
207 list_box: &ListBox,
208 items: &[LaunchItem],
209 filtered_items: &mut Vec<LaunchItem>,
210 launch_rows: &mut HashMap<u64, LaunchItemRow>,
211 query: &str,
212) {
213 while let Some(child) = list_box.first_child() {
215 list_box.remove(&child);
216 }
217
218 filtered_items.clear();
219 launch_rows.clear();
220
221 for item in items {
223 if query.is_empty()
224 || item.name.to_lowercase().contains(query)
225 || item.description.to_lowercase().contains(query)
226 {
227 filtered_items.push(item.clone());
228 }
229 }
230
231 for item in filtered_items.iter() {
233 let launch_row = LaunchItemRow::new(item.clone());
234 let row_ptr = launch_row.list_box_row().as_ptr() as u64;
235 list_box.append(launch_row.list_box_row());
236 launch_rows.insert(row_ptr, launch_row);
237 }
238
239 if let Some(first_row) = list_box.first_child() {
241 if let Ok(row) = first_row.downcast::<ListBoxRow>() {
242 list_box.select_row(Some(&row));
243 }
244 }
245}
246
247fn setup_keyboard_shortcuts(
248 window: &ApplicationWindow,
249 search_entry: &Entry,
250 list_box: &ListBox,
251 launch_rows: Rc<RefCell<HashMap<u64, LaunchItemRow>>>,
252) {
253 let key_controller = gtk4::EventControllerKey::new();
255
256 let window_clone = window.clone();
258 let list_box_clone = list_box.clone();
259 let launch_rows_clone = launch_rows.clone();
260
261 key_controller.connect_key_pressed(move |_controller, key, _code, modifier| {
262 if modifier.contains(gdk4::ModifierType::CONTROL_MASK) {
264 match key {
265 gdk4::Key::q | gdk4::Key::Q | gdk4::Key::w | gdk4::Key::W => {
266 window_clone.close();
267 return glib::Propagation::Stop;
268 }
269 _ => {}
270 }
271 }
272
273 match key {
275 gdk4::Key::Down => {
276 if let Some(selected) = list_box_clone.selected_row() {
277 if let Some(next) = selected.next_sibling() {
278 if let Ok(next_row) = next.downcast::<ListBoxRow>() {
279 list_box_clone.select_row(Some(&next_row));
280 }
281 }
282 }
283 return glib::Propagation::Stop;
284 }
285 gdk4::Key::Up => {
286 if let Some(selected) = list_box_clone.selected_row() {
287 if let Some(prev) = selected.prev_sibling() {
288 if let Ok(prev_row) = prev.downcast::<ListBoxRow>() {
289 list_box_clone.select_row(Some(&prev_row));
290 }
291 }
292 }
293 return glib::Propagation::Stop;
294 }
295 gdk4::Key::Page_Up => {
296 handle_page_up(&list_box_clone);
297 return glib::Propagation::Stop;
298 }
299 gdk4::Key::Page_Down => {
300 handle_page_down(&list_box_clone);
301 return glib::Propagation::Stop;
302 }
303 gdk4::Key::Return | gdk4::Key::KP_Enter => {
304 eprintln!("Launching item...");
306 if let Some(selected) = list_box_clone.selected_row() {
307 eprintln!("Found a selected item...");
308 let row_ptr = selected.as_ptr() as u64;
309 if let Some(launch_row) = launch_rows_clone.borrow().get(&row_ptr) {
310 eprintln!("Calling launch on item...");
311 launch_row.launch_item().launch();
312 std::process::exit(0);
313 }
314 }
315 window_clone.close();
316 return glib::Propagation::Stop;
317 }
318 gdk4::Key::Escape => {
319 window_clone.close();
320 return glib::Propagation::Stop;
321 }
322 _ => {}
323 }
324
325 glib::Propagation::Proceed
326 });
327
328 window.add_controller(key_controller);
329}
330
331fn setup_search_entry_shortcuts(
332 search_entry: &Entry,
333 list_box: &ListBox,
334 window: &ApplicationWindow,
335 launch_rows: Rc<RefCell<HashMap<u64, LaunchItemRow>>>,
336) {
337 let key_controller = gtk4::EventControllerKey::new();
338
339 let list_box_clone = list_box.clone();
340 let window_clone = window.clone();
341 let launch_rows_clone = launch_rows.clone();
342
343 key_controller.connect_key_pressed(move |_controller, key, _code, _modifier| {
344 match key {
345 gdk4::Key::Return | gdk4::Key::KP_Enter => {
346 if let Some(selected) = list_box_clone.selected_row() {
348 let row_ptr = selected.as_ptr() as u64;
349 if let Some(launch_row) = launch_rows_clone.borrow().get(&row_ptr) {
350 launch_row.launch_item().launch();
351 std::process::exit(0);
352 }
353 }
354 return glib::Propagation::Stop;
355 }
356 gdk4::Key::Down => {
357 if let Some(selected) = list_box_clone.selected_row() {
358 if let Some(next) = selected.next_sibling() {
359 if let Ok(next_row) = next.downcast::<ListBoxRow>() {
360 list_box_clone.select_row(Some(&next_row));
361 }
362 }
363 }
364 return glib::Propagation::Stop;
365 }
366 gdk4::Key::Up => {
367 if let Some(selected) = list_box_clone.selected_row() {
368 if let Some(prev) = selected.prev_sibling() {
369 if let Ok(prev_row) = prev.downcast::<ListBoxRow>() {
370 list_box_clone.select_row(Some(&prev_row));
371 }
372 }
373 }
374 return glib::Propagation::Stop;
375 }
376 gdk4::Key::Page_Up => {
377 handle_page_up(&list_box_clone);
378 return glib::Propagation::Stop;
379 }
380 gdk4::Key::Page_Down => {
381 handle_page_down(&list_box_clone);
382 return glib::Propagation::Stop;
383 }
384 gdk4::Key::Escape => {
385 window_clone.close();
386 return glib::Propagation::Stop;
387 }
388 _ => {}
389 }
390
391 glib::Propagation::Proceed
392 });
393
394 search_entry.add_controller(key_controller);
395}
396
397fn setup_list_box_click_handler(
398 list_box: &ListBox,
399 window: &ApplicationWindow,
400 launch_rows: Rc<RefCell<HashMap<u64, LaunchItemRow>>>,
401) {
402 let window_clone = window.clone();
403 let launch_rows_clone = launch_rows.clone();
404
405 list_box.connect_row_activated(move |_list_box, row| {
406 let row_ptr = row.as_ptr() as u64;
407 if let Some(launch_row) = launch_rows_clone.borrow().get(&row_ptr) {
408 launch_row.launch_item().launch();
409 std::process::exit(0);
410 }
411 });
412}
413
414fn setup_search_entry_activate(
415 search_entry: &Entry,
416 list_box: &ListBox,
417 window: &ApplicationWindow,
418 launch_rows: Rc<RefCell<HashMap<u64, LaunchItemRow>>>,
419) {
420 let list_box_clone = list_box.clone();
421 let window_clone = window.clone();
422 let launch_rows_clone = launch_rows.clone();
423
424 search_entry.connect_activate(move |_entry| {
425 if let Some(selected) = list_box_clone.selected_row() {
427 let row_ptr = selected.as_ptr() as u64;
428 if let Some(launch_row) = launch_rows_clone.borrow().get(&row_ptr) {
429 launch_row.launch_item().launch();
430 std::process::exit(0);
431 }
432 }
433 });
434}
435
436fn handle_page_up(list_box: &ListBox) {
437 let (first_visible, last_visible) = get_visible_row_range(list_box);
439
440 if first_visible.is_none() || last_visible.is_none() {
441 return; }
443
444 let selected = list_box.selected_row();
445 let first_child = list_box.first_child();
446
447 let first_child_as_row = first_child.and_then(|w| w.downcast::<ListBoxRow>().ok());
449 if selected.is_none() || (selected == first_child_as_row) {
450 let visible_count = count_visible_rows(list_box);
452 if visible_count > 0 {
453 scroll_up_by_page(list_box, visible_count);
454 }
455 } else {
456 if let Some(first_visible_row) = first_visible {
458 list_box.select_row(Some(&first_visible_row));
459 }
460 }
461}
462
463fn handle_page_down(list_box: &ListBox) {
464 let (first_visible, last_visible) = get_visible_row_range(list_box);
466
467 if first_visible.is_none() || last_visible.is_none() {
468 return; }
470
471 let selected = list_box.selected_row();
472 let last_child = get_last_child(list_box);
473
474 if selected.is_none() || (selected == last_child) {
476 let visible_count = count_visible_rows(list_box);
478 if visible_count > 0 {
479 scroll_down_by_page(list_box, visible_count);
480 }
481 } else {
482 if let Some(last_visible_row) = last_visible {
484 list_box.select_row(Some(&last_visible_row));
485 }
486 }
487}
488
489fn get_visible_row_range(list_box: &ListBox) -> (Option<ListBoxRow>, Option<ListBoxRow>) {
490 let mut parent = list_box.parent();
492 let scrolled_window = loop {
493 match parent {
494 Some(ref p) if p.type_().name() == "GtkScrolledWindow" => {
495 break p.clone();
496 }
497 Some(p) => {
498 parent = p.parent();
499 }
500 None => return (None, None),
501 }
502 };
503
504 let allocation = scrolled_window.allocation();
505 let scroll_height = allocation.height();
506
507 let mut first_visible = None;
508 let mut last_visible = None;
509 let mut current_child = list_box.first_child();
510
511 while let Some(child) = current_child {
512 if let Some(row) = child.downcast_ref::<ListBoxRow>() {
513 let row_allocation = row.allocation();
514 let row_y = row_allocation.y();
515 let row_height = row_allocation.height();
516
517 if row_y + row_height > 0 && row_y < scroll_height {
519 if first_visible.is_none() {
520 first_visible = Some(row.clone());
521 }
522 last_visible = Some(row.clone());
523 }
524 }
525 current_child = child.next_sibling();
526 }
527
528 (first_visible, last_visible)
529}
530
531fn count_visible_rows(list_box: &ListBox) -> i32 {
532 let (first_visible, _) = get_visible_row_range(list_box);
533 if first_visible.is_none() {
534 return 0;
535 }
536
537 let mut parent = list_box.parent();
539 let scrolled_window = loop {
540 match parent {
541 Some(ref p) if p.type_().name() == "GtkScrolledWindow" => {
542 break p.clone();
543 }
544 Some(p) => {
545 parent = p.parent();
546 }
547 None => return 0,
548 }
549 };
550
551 let allocation = scrolled_window.allocation();
552 let scroll_height = allocation.height();
553
554 let mut count = 0;
555 let mut current_child = list_box.first_child();
556
557 while let Some(child) = current_child {
558 if let Some(row) = child.downcast_ref::<ListBoxRow>() {
559 let row_allocation = row.allocation();
560 let row_y = row_allocation.y();
561 let row_height = row_allocation.height();
562
563 if row_y + row_height > 0 && row_y < scroll_height {
565 count += 1;
566 }
567 }
568 current_child = child.next_sibling();
569 }
570
571 count
572}
573
574fn scroll_up_by_page(list_box: &ListBox, page_size: i32) {
575 let selected = list_box.selected_row();
576 let mut target_row = None;
577 let mut steps = 0;
578
579 if let Some(current) = selected {
580 let mut current_row = Some(current);
581
582 while steps < page_size && current_row.is_some() {
584 if let Some(prev) = current_row.as_ref().and_then(|r| r.prev_sibling()) {
585 if let Ok(prev_row) = prev.downcast::<ListBoxRow>() {
586 current_row = Some(prev_row);
587 steps += 1;
588 } else {
589 break;
590 }
591 } else {
592 break;
593 }
594 }
595
596 target_row = current_row;
597 } else {
598 if let Some(first) = list_box.first_child() {
600 if let Ok(first_row) = first.downcast::<ListBoxRow>() {
601 target_row = Some(first_row);
602 }
603 }
604 }
605
606 if let Some(row) = target_row {
607 list_box.select_row(Some(&row));
608 }
609}
610
611fn scroll_down_by_page(list_box: &ListBox, page_size: i32) {
612 let selected = list_box.selected_row();
613 let mut target_row = None;
614 let mut steps = 0;
615
616 if let Some(current) = selected {
617 let mut current_row = Some(current);
618
619 while steps < page_size && current_row.is_some() {
621 if let Some(next) = current_row.as_ref().and_then(|r| r.next_sibling()) {
622 if let Ok(next_row) = next.downcast::<ListBoxRow>() {
623 current_row = Some(next_row);
624 steps += 1;
625 } else {
626 break;
627 }
628 } else {
629 break;
630 }
631 }
632
633 target_row = current_row;
634 } else {
635 target_row = get_last_child(list_box);
637 }
638
639 if let Some(row) = target_row {
640 list_box.select_row(Some(&row));
641 }
642}
643
644fn get_last_child(list_box: &ListBox) -> Option<ListBoxRow> {
645 let mut current = list_box.first_child();
646 let mut last_row = None;
647
648 while let Some(child) = current {
649 let next_sibling = child.next_sibling();
650 if let Ok(row) = child.downcast::<ListBoxRow>() {
651 last_row = Some(row);
652 }
653 current = next_sibling;
654 }
655
656 last_row
657}