worf/src/gui.rs
2025-04-06 22:34:04 +02:00

244 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use gdk4::gio::File;
use gdk4::glib::Propagation;
use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
use gdk4::Display;
use gtk4::prelude::{ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt};
use gtk4::{Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, Label, ListBox, ListBoxRow, PolicyType, ScrolledWindow, SearchEntry, Widget};
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
use gtk4_layer_shell::{KeyboardMode, LayerShell};
use std::process::exit;
use anyhow::Context;
use log::error;
use crate::config::Config;
pub struct EntryElement {
pub label: String, // todo support empty label?
pub icon_path: Option<String>,
pub action: Box<dyn Fn() + Send + 'static>,
pub sub_elements: Option<Vec<EntryElement>>,
}
pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
// Load CSS
let provider = CssProvider::new();
let css_file_path = File::for_path("/home/me/.config/wofi/style.css");
provider.load_from_file(&css_file_path);
// Apply CSS to the display
let display = Display::default().expect("Could not connect to a display");
gtk4::style_context_add_provider_for_display(
&display,
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
let display = Display::default().expect("Could not connect to a display");
// Apply CSS to the display
gtk4::style_context_add_provider_for_display(
&display,
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
// No need for application_id unless you want portal support
let app = Application::builder()
.application_id("ravi")
.build();
app.connect_activate(move |app| {
// Create a toplevel undecorated window
let window = ApplicationWindow::builder()
.application(app)
.decorated(false)
.resizable(false)
.default_width(20)
.default_height(20)
.build();
window.init_layer_shell();
window.set_keyboard_mode(KeyboardMode::Exclusive);
window.set_widget_name("window");
window.set_layer(gtk4_layer_shell::Layer::Overlay);
window.set_namespace(Some("ravi"));
let outer_box = gtk4::Box::new(Orientation::Vertical, 0);
outer_box.set_widget_name("outer-box");
window.set_child(Some(&outer_box));
let entry = SearchEntry::new();
entry.set_widget_name("input");
entry.set_css_classes(&["input"]);
entry.set_placeholder_text(Some("Enter search..."));
// Create key event controller
let entry_clone = entry.clone();
setup_key_event_handler(&window, entry_clone);
// Example `search` and `password_char` usage
let password_char = Some('*');
entry.set_placeholder_text(Some("placeholder"));
// todo
// if let Some(c) = password_char {
// let entry_casted: Entry = entry.clone().upcast();
// entry_casted.set_visibility(false);
// entry_casted.set_invisible_char(c);
// }
outer_box.append(&entry);
let scroll = ScrolledWindow::new();
scroll.set_widget_name("scroll");
scroll.set_hexpand(true);
scroll.set_vexpand(true);
let hide_scroll = false; // todo
if hide_scroll {
scroll.set_policy(PolicyType::External, PolicyType::External);
}
outer_box.append(&scroll);
let inner_box = FlowBox::new();
inner_box.set_widget_name("inner-box");
inner_box.set_css_classes(&["inner-box"]);
inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
inner_box.set_max_children_per_line(1); // todo change to `columns` variable
//inner_box.set_orientation(Orientation::Horizontal); // or Vertical
inner_box.set_halign(Align::Fill);
inner_box.set_valign(Align::Start);
inner_box.set_activate_on_single_click(true);
for entry in &elements {
add_entry_element(&inner_box, &entry);
}
// todo
// Set focus after everything is realized
inner_box.connect_map(|fb| {
fb.grab_focus();
});
let wrapper_box = gtk4::Box::new(Orientation::Vertical, 0);
wrapper_box.set_homogeneous(true);
wrapper_box.append(&inner_box);
scroll.set_child(Some(&wrapper_box));
// todo
// // Dummy filter and sort funcs replace with actual logic
// inner_box.set_filter_func(Some(Box::new(|_child| {
// true // filter logic here
// })));
// todo
// inner_box.set_sort_func(Some(Box::new(|child1, child2| {
// child1.widget_name().cmp(&child2.widget_name())
// })));
window.show();
// Get the display where the window resides
let display = window.display();
// Get the monitor that the window is on (use window's coordinates to find this)
window.surface().map(|surface| {
let monitor = display.monitor_at_surface(&surface);
if let Some(monitor) = monitor {
let geometry = monitor.geometry();
if let Some(w) = percent_or_absolute(&config.width.clone().unwrap_or("800".to_owned()), geometry.width()) {
window.set_width_request(w);
}
if let Some(h) = percent_or_absolute(&config.height.clone().unwrap_or("500".to_owned()), geometry.height()) {
window.set_height_request(h);
}
} else {
error!("failed to get monitor to init window size");
}
});
});
let empty_array: [&str; 0] = [];
app.run_with_args(&empty_array);
Ok(())
}
fn setup_key_event_handler(window: &ApplicationWindow, entry_clone: SearchEntry) {
let key_controller = EventControllerKey::new();
let x = key_controller.connect_key_pressed(move |_controller, key_value, code, mode| {
if code == 9 {
// todo find better way to handle escape
exit(1);
}
if let Some(c) = key_value.name() {
// Only proceed if it's a single alphanumeric character
if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) {
let current = entry_clone.text().to_string();
entry_clone.set_text(&format!("{current}{c}"));
}
}
Propagation::Proceed
});
// Add the controller to the window
window.add_controller(key_controller);
}
fn add_entry_element(inner_box: &gtk4::FlowBox, entry_element: &EntryElement) {
let parent: Widget = if entry_element.sub_elements.is_some() {
let expander = Expander::new(None);
// Inline label as expander label
let label = Label::new(Some(&entry_element.label));
expander.set_label_widget(Some(&label));
let list_box = ListBox::new();
// todo subelements do not fill full space yet.
// todo multi nesting is not supported yet.
for x in entry_element.sub_elements.iter().flatten() {
let row = ListBoxRow::new();
row.set_widget_name("entry");
let label = Label::new(Some(&x.label));
row.set_child(Some(&label));
list_box.append(&row);
}
expander.set_child(Some(&list_box));
expander.upcast()
} else {
Label::new(Some(&entry_element.label)).upcast()
};
parent.set_halign(Align::Start);
let child = FlowBoxChild::new();
child.set_widget_name("entry");
child.set_child(Some(&parent));
inner_box.append(&child);
}
fn percent_or_absolute(value: &String, base_value: i32) -> Option<i32> {
if value.contains("%") {
let value = value.replace("%", "");
let value = value.trim();
match value.parse::<i32>() {
Ok(n) => {
let result = ((n as f32/ 100.0) * base_value as f32) as i32;
Some(result)
},
Err(_) => None,
}
} else {
value.parse::<i32>().ok()
}
}