Inline some clones to make code more readable, add config, implement dimming -> locking -> sleeping into idle daemon, fix waiting on network thread, implement run menu with application launching and math evaluation,

This commit is contained in:
jmb05
2026-11-01 16:21:46 +01:00
parent 80b5cafb8c
commit a8fe99b345
14 changed files with 975 additions and 484 deletions

View File

@@ -1,27 +1,44 @@
use gtk::prelude::{BoxExt, GtkWindowExt, WidgetExt};
use crate::LOGGER;
use crate::backend::window_manager_hypr::HyprlandComms;
use freedesktop_desktop_entry::{default_paths, get_languages_from_env};
use glib::clone;
use glib::prelude::Cast;
use gtk::gdk::{Display, Monitor};
use gtk::prelude::{
BoxExt, ButtonExt, DisplayExt, EditableExt, EntryExt, GtkWindowExt, ListModelExt, MonitorExt,
WidgetExt,
};
use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk};
use gtk4_layer_shell::{KeyboardMode, Layer, LayerShell};
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use jmb_logger_rs::*;
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher};
use std::cell::RefCell;
use std::collections::HashMap;
use std::process::Command;
use std::rc::Rc;
#[derive(Clone)]
pub struct RunMenu {
window: ApplicationWindow,
entry: gtk::Entry,
entries: Rc<RefCell<HashMap<String, Vec<String>>>>,
}
impl RunMenu {
pub fn create(application: Application) -> Self {
let window = gtk::ApplicationWindow::builder()
let window = ApplicationWindow::builder()
.application(&application)
.title("JmbRunMenu")
.default_width(500)
.default_height(300)
.css_classes(["run-menu"])
.build();
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_halign(Align::Center);
window.set_valign(Align::Center);
window.set_size_request(500, 300);
window.set_width_request(500);
window.set_anchor(Edge::Top, true);
window.set_keyboard_mode(KeyboardMode::Exclusive);
let v_box = gtk::Box::new(Orientation::Vertical, 0);
@@ -31,28 +48,109 @@ impl RunMenu {
let scrolled = gtk::ScrolledWindow::new();
scrolled.set_policy(PolicyType::Never, PolicyType::Automatic);
scrolled.set_vexpand(true);
scrolled.set_valign(Align::Fill);
scrolled.set_vexpand(false);
scrolled.set_propagate_natural_height(true);
scrolled.set_min_content_height(0);
scrolled.set_max_content_height(300);
let content_box = gtk::Box::new(Orientation::Vertical, 0);
content_box.add_css_class("content-box");
for _ in 0..10 {
let label = gtk::Label::new(Some("Test"));
content_box.append(&label);
}
content_box.set_hexpand(true);
scrolled.set_child(Some(&content_box));
v_box.set_vexpand(false);
v_box.set_valign(Align::Start);
v_box.append(&entry);
v_box.append(&scrolled);
window.set_child(Some(&v_box));
let display = Display::default().expect("Couldn't get display");
let monitor = display
.monitors()
.item(HyprlandComms::current_monitor() as u32)
.expect("Couldn't get monitor")
.downcast::<Monitor>()
.ok();
window.set_monitor(monitor.as_ref());
if let Some(monitor) = monitor {
let geometry = monitor.geometry();
let (_, natural_height, _, _) =
window.child().unwrap().measure(Orientation::Vertical, -1);
println!("Monitor height: {}", geometry.height());
let top_margin = (geometry.height()) / 2;
window.set_margin(Edge::Top, top_margin - 100);
} else {
window.set_valign(Align::Center);
}
content_box
.observe_children()
.connect_items_changed(move |model, _, _, _| {
if model.n_items() == 0 {
println!("Hide");
} else {
println!("Reveal");
}
});
let window_clone = window.clone();
let instance = Self {
window: window_clone,
entry: entry.clone(),
entries: Rc::new(RefCell::new(HashMap::new())),
};
let instance_clone = instance.clone();
let matcher_rc = Rc::new(RefCell::new(Matcher::new(Config::DEFAULT)));
entry.connect_changed(clone!(
#[strong]
content_box,
move |e| {
while let Some(child) = content_box.first_child() {
content_box.remove(&child);
}
if e.text_length() > 0 {
let text = e.text();
if text.starts_with("=") {
if let Ok(eval) = meval::eval_str(&text[1..]) {
let label = gtk::Label::new(Some(&format!("{eval}")));
label.add_css_class("exec-button");
label.set_halign(Align::Start);
label.set_hexpand(true);
content_box.append(&label);
}
} else {
instance_clone.list_matching(e, &content_box, &mut matcher_rc.borrow_mut());
instance_clone.window.set_default_size(500, -1);
}
}
}
));
entry.connect_has_focus_notify(clone!(
#[strong]
content_box,
move |e| {
if let Some(child) = content_box.first_child() {
if e.has_focus() {
child.add_css_class("suggested");
} else {
child.remove_css_class("suggested");
}
}
}
));
entry.connect_activate(clone!(
#[strong]
content_box,
move |_| {
if let Some(child) = content_box.first_child()
&& let Ok(button) = child.downcast::<gtk::Button>()
{
button.activate();
}
}
));
let instance_clone = instance.clone();
let key_controller = gtk::EventControllerKey::new();
key_controller.connect_key_pressed(move |_, key, _, _| {
@@ -69,9 +167,82 @@ impl RunMenu {
pub fn hide(&self) {
self.window.set_visible(false);
self.entry.set_text("");
}
pub fn show(&self) {
pub fn show(&mut self) {
self.load_desktop_files();
self.window.present();
}
fn create_entry(&self, content_box: &gtk::Box, entry: &String) -> gtk::Button {
let entry_clone = entry.clone();
let button = gtk::Button::new();
button.set_halign(Align::Fill);
button.set_hexpand(true);
let label = gtk::Label::new(Some(entry));
label.set_halign(Align::Start);
button.set_child(Some(&label));
button.add_css_class("exec-button");
let entry_map = self.entries.clone();
let instance = self.clone();
button.connect_clicked(move |_| {
let map = entry_map.borrow();
let exec = map.get(&entry_clone);
if let Some(exec) = exec {
let executable = &exec[0];
if let Err(err) = Command::new(executable).args(&exec[1..]).spawn() {
warn!(&LOGGER, "Couldn't execute app: {err}");
}
instance.hide();
}
});
content_box.append(&button);
button
}
fn list_matching(
&self,
entry: &gtk::Entry,
content_box: &gtk::Box,
matcher: &mut Matcher,
) -> bool {
let keys: Vec<String> = self.entries.borrow().keys().map(|i| i.clone()).collect();
let mut first = true;
let mut added = false;
for (entry, _) in Pattern::parse(&entry.text(), CaseMatching::Ignore, Normalization::Smart)
.match_list(keys, matcher)
.iter()
{
let button = self.create_entry(content_box, &entry);
if first {
button.add_css_class("suggested");
first = false;
}
added = true;
}
added
}
fn load_desktop_files(&mut self) {
let locales = get_languages_from_env();
let entries = freedesktop_desktop_entry::Iter::new(default_paths())
.entries(Some(&locales))
.collect::<Vec<_>>();
let mut entry_map = self.entries.borrow_mut();
entry_map.clear();
for entry in entries {
if let Ok(exec) = entry.parse_exec() {
let name = entry.full_name(&locales);
if let Some(name) = name {
entry_map.insert(name.to_string(), exec);
}
}
}
}
}