breadbox: fix spurious close via transparent backdrop window
EventControllerFocus::connect_leave was firing whenever the pointer left the launcher surface because Hyprland's focus-follows-mouse hands keyboard focus back to the window under the cursor (OnDemand mode). Fix: introduce a full-screen transparent backdrop window at Layer::Top. The launcher sits at Layer::Overlay (above it), so the backdrop never intercepts launcher clicks. Clicking anywhere outside the launcher hits the backdrop and closes both windows via a shared Rc<dyn Fn()> callback. The launcher returns to KeyboardMode::Exclusive so the compositor can no longer steal focus on pointer leave. EventControllerFocus is removed.
This commit is contained in:
parent
26431d5969
commit
74de775a9b
1 changed files with 47 additions and 21 deletions
68
src/main.rs
68
src/main.rs
|
|
@ -5,6 +5,7 @@ use std::{
|
||||||
io::{BufRead, BufReader, Write},
|
io::{BufRead, BufReader, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
|
rc::Rc,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -21,7 +22,10 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||||
const CACHE_TIMEOUT_SECS: u64 = 86400;
|
const CACHE_TIMEOUT_SECS: u64 = 86400;
|
||||||
|
|
||||||
const CSS: &str = "
|
const CSS: &str = "
|
||||||
window, .background {
|
window {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.launcher-bg {
|
||||||
background-color: #1e1e2e;
|
background-color: #1e1e2e;
|
||||||
}
|
}
|
||||||
searchentry {
|
searchentry {
|
||||||
|
|
@ -324,18 +328,45 @@ fn run_ui(entries: Vec<(String, String)>) {
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Full-screen transparent backdrop at Layer::Top — catches clicks
|
||||||
|
// outside the launcher and closes it. Sits below the launcher
|
||||||
|
// (Layer::Overlay) so it never intercepts clicks on the UI itself.
|
||||||
|
let backdrop = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.build();
|
||||||
|
backdrop.init_layer_shell();
|
||||||
|
backdrop.set_layer(Layer::Top);
|
||||||
|
backdrop.set_keyboard_mode(KeyboardMode::None);
|
||||||
|
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
|
||||||
|
backdrop.set_anchor(edge, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main launcher window
|
||||||
let window = ApplicationWindow::builder()
|
let window = ApplicationWindow::builder()
|
||||||
.application(app)
|
.application(app)
|
||||||
.default_width(700)
|
.default_width(700)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
window.init_layer_shell();
|
window.init_layer_shell();
|
||||||
window.set_layer(Layer::Overlay);
|
window.set_layer(Layer::Overlay);
|
||||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
// Exclusive: compositor won't hand focus to another window on pointer
|
||||||
|
// leave, so the backdrop (not a focus event) handles click-outside.
|
||||||
|
window.set_keyboard_mode(KeyboardMode::Exclusive);
|
||||||
window.set_anchor(Edge::Top, true);
|
window.set_anchor(Edge::Top, true);
|
||||||
window.set_exclusive_zone(-1);
|
window.set_exclusive_zone(-1);
|
||||||
|
|
||||||
|
// Shared close: dismisses both windows and cleans up the PID file.
|
||||||
|
let close_all: Rc<dyn Fn()> = Rc::new({
|
||||||
|
let w = window.clone();
|
||||||
|
let b = backdrop.clone();
|
||||||
|
move || {
|
||||||
|
cleanup_pid();
|
||||||
|
w.close();
|
||||||
|
b.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let vbox = GBox::new(Orientation::Vertical, 0);
|
let vbox = GBox::new(Orientation::Vertical, 0);
|
||||||
|
vbox.add_css_class("launcher-bg");
|
||||||
|
|
||||||
let search = SearchEntry::new();
|
let search = SearchEntry::new();
|
||||||
search.set_placeholder_text(Some("breadbox"));
|
search.set_placeholder_text(Some("breadbox"));
|
||||||
|
|
@ -406,14 +437,13 @@ fn run_ui(entries: Vec<(String, String)>) {
|
||||||
// intercept before SearchEntry's own handlers consume them
|
// intercept before SearchEntry's own handlers consume them
|
||||||
let key_ctrl = EventControllerKey::new();
|
let key_ctrl = EventControllerKey::new();
|
||||||
key_ctrl.set_propagation_phase(gtk4::PropagationPhase::Capture);
|
key_ctrl.set_propagation_phase(gtk4::PropagationPhase::Capture);
|
||||||
let window_k = window.clone();
|
let close_k = Rc::clone(&close_all);
|
||||||
let list_k = list.clone();
|
let list_k = list.clone();
|
||||||
key_ctrl.connect_key_pressed(move |_, key, _, _| {
|
key_ctrl.connect_key_pressed(move |_, key, _, _| {
|
||||||
use gtk4::gdk::Key;
|
use gtk4::gdk::Key;
|
||||||
match key {
|
match key {
|
||||||
Key::Escape => {
|
Key::Escape => {
|
||||||
cleanup_pid();
|
close_k();
|
||||||
window_k.close();
|
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
}
|
}
|
||||||
Key::Return | Key::KP_Enter => {
|
Key::Return | Key::KP_Enter => {
|
||||||
|
|
@ -421,8 +451,7 @@ fn run_ui(entries: Vec<(String, String)>) {
|
||||||
let action = get_row_data(&row, "action");
|
let action = get_row_data(&row, "action");
|
||||||
if !action.is_empty() {
|
if !action.is_empty() {
|
||||||
do_launch(&action);
|
do_launch(&action);
|
||||||
cleanup_pid();
|
close_k();
|
||||||
window_k.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
glib::Propagation::Stop
|
glib::Propagation::Stop
|
||||||
|
|
@ -465,29 +494,26 @@ fn run_ui(entries: Vec<(String, String)>) {
|
||||||
});
|
});
|
||||||
window.add_controller(key_ctrl);
|
window.add_controller(key_ctrl);
|
||||||
|
|
||||||
// Click to launch
|
// Row click / Enter activates launch
|
||||||
let window_a = window.clone();
|
let close_a = Rc::clone(&close_all);
|
||||||
list.connect_row_activated(move |_, row| {
|
list.connect_row_activated(move |_, row| {
|
||||||
let action = get_row_data(row, "action");
|
let action = get_row_data(row, "action");
|
||||||
if !action.is_empty() {
|
if !action.is_empty() {
|
||||||
do_launch(&action);
|
do_launch(&action);
|
||||||
cleanup_pid();
|
close_a();
|
||||||
window_a.close();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close when focus leaves the window (click outside, alt-tab, etc.)
|
// Backdrop click: user clicked outside the launcher
|
||||||
let window_foc = window.clone();
|
let close_bd = Rc::clone(&close_all);
|
||||||
let focus_ctrl = gtk4::EventControllerFocus::new();
|
let click = gtk4::GestureClick::new();
|
||||||
focus_ctrl.connect_leave(move |_| {
|
click.connect_released(move |_, _, _, _| close_bd());
|
||||||
cleanup_pid();
|
backdrop.add_controller(click);
|
||||||
window_foc.close();
|
|
||||||
});
|
|
||||||
window.add_controller(focus_ctrl);
|
|
||||||
|
|
||||||
// Cleanup pid when window is destroyed for any reason
|
// Safety net: clean up PID if the window is destroyed by the compositor
|
||||||
window.connect_destroy(|_| cleanup_pid());
|
window.connect_destroy(|_| cleanup_pid());
|
||||||
|
|
||||||
|
backdrop.present();
|
||||||
window.present();
|
window.present();
|
||||||
search.grab_focus();
|
search.grab_focus();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue