Adopt bread_utils::gtk_popup for the launcher overlay
All checks were successful
check / check (push) Successful in 54s
dev release / build (push) Successful in 1m19s

Replace the hand-rolled layer-shell window, visible-row Up/Down
navigation, and click-outside-to-close gesture with the shared
helpers already used by breadclip. Keep singleton::toggle_or_kill
and bind_window_auto.
This commit is contained in:
Breadway 2026-08-23 14:39:11 +08:00
parent b52bf77ed3
commit 7c55ea4756
3 changed files with 44 additions and 73 deletions

2
Cargo.lock generated
View file

@ -121,6 +121,8 @@ source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517
dependencies = [ dependencies = [
"bread-shared", "bread-shared",
"dirs", "dirs",
"gtk4",
"gtk4-layer-shell",
"serde", "serde",
"serde_json", "serde_json",
] ]

View file

@ -10,7 +10,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] } bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] }
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client", "gtk"] }
# Capture primitives for `--screenshot` mode — see src/screenshot.rs. # Capture primitives for `--screenshot` mode — see src/screenshot.rs.
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
breadbox-shared = { path = "../breadbox-shared" } breadbox-shared = { path = "../breadbox-shared" }

View file

@ -3,8 +3,7 @@ use bread_utils::bread_client::BreadClient;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::HashMap, collections::HashMap,
env, env, fs,
fs,
io::{Read, Write}, io::{Read, Write},
os::unix::net::UnixStream, os::unix::net::UnixStream,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -21,13 +20,9 @@ use breadbox_shared::{
config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory,
}; };
use gtk4::{ use gtk4::{
glib, glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider, Entry,
pango::EllipsizeMode, EventControllerKey, Label, ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode,
prelude::*,
Application, ApplicationWindow, Box as GBox, CssProvider, Entry, EventControllerKey, Label,
ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode,
}; };
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
mod listen; mod listen;
mod screenshot; mod screenshot;
@ -91,7 +86,9 @@ fn load_sorted_entries(
(None, Some(_)) => std::cmp::Ordering::Greater, (None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => { (None, None) => {
// Most-launched first, then alphabetical // Most-launched first, then alphabetical
history.count(&b.name).cmp(&history.count(&a.name)) history
.count(&b.name)
.cmp(&history.count(&a.name))
.then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) .then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
} }
} }
@ -385,10 +382,18 @@ fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 {
let q = query.to_lowercase(); let q = query.to_lowercase();
let name = entry.name.to_lowercase(); let name = entry.name.to_lowercase();
let wm = entry.wm_class.as_deref().unwrap_or("").to_lowercase(); let wm = entry.wm_class.as_deref().unwrap_or("").to_lowercase();
if name == q || wm == q { return 0; } if name == q || wm == q {
if name.starts_with(&q) { return 1; } return 0;
if name.contains(&q) { return 2; } }
if wm.starts_with(&q) || wm.contains(&q) { return 3; } if name.starts_with(&q) {
return 1;
}
if name.contains(&q) {
return 2;
}
if wm.starts_with(&q) || wm.contains(&q) {
return 3;
}
4 // subsequence match 4 // subsequence match
} }
@ -454,16 +459,8 @@ fn run_ui(
bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); bread_theme::gtk::apply_user_css(&user_css_path, &user_cell);
} }
// Full-screen transparent window; clicks outside the launcher panel close it. // Full-screen transparent overlay; panel widget is positioned inside it.
let window = ApplicationWindow::builder().application(app).build(); let window = bread_utils::gtk_popup::new_overlay_window(app, "breadbox");
window.init_layer_shell();
window.set_namespace(Some("breadbox"));
window.set_layer(Layer::Overlay);
window.set_keyboard_mode(KeyboardMode::Exclusive);
for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] {
window.set_anchor(edge, true);
}
window.set_exclusive_zone(0);
bread_theme::gtk::bind_window_auto(&window); bread_theme::gtk::bind_window_auto(&window);
let close_all: Rc<dyn Fn()> = Rc::new({ let close_all: Rc<dyn Fn()> = Rc::new({
@ -557,8 +554,16 @@ fn run_ui(
list.set_sort_func(move |row_a, row_b| { list.set_sort_func(move |row_a, row_b| {
let query = sort_query.borrow(); let query = sort_query.borrow();
if query.is_empty() { if query.is_empty() {
let oa = unsafe { row_a.data::<u32>("initial_order").map_or(u32::MAX, |p| *p.as_ref()) }; let oa = unsafe {
let ob = unsafe { row_b.data::<u32>("initial_order").map_or(u32::MAX, |p| *p.as_ref()) }; row_a
.data::<u32>("initial_order")
.map_or(u32::MAX, |p| *p.as_ref())
};
let ob = unsafe {
row_b
.data::<u32>("initial_order")
.map_or(u32::MAX, |p| *p.as_ref())
};
return oa.cmp(&ob).into(); return oa.cmp(&ob).into();
} }
let (Some(ea), Some(eb)) = (get_row_entry(row_a), get_row_entry(row_b)) else { let (Some(ea), Some(eb)) = (get_row_entry(row_a), get_row_entry(row_b)) else {
@ -624,9 +629,8 @@ fn run_ui(
i += 1; i += 1;
} }
list_f.invalidate_sort(); list_f.invalidate_sort();
let first_vis = (0i32..).find_map(|j| { let first_vis =
list_f.row_at_index(j).filter(|r| r.is_visible()) (0i32..).find_map(|j| list_f.row_at_index(j).filter(|r| r.is_visible()));
});
list_f.select_row(first_vis.as_ref()); list_f.select_row(first_vis.as_ref());
set_footer_count(&footer_f, visible_row_count(&list_f)); set_footer_count(&footer_f, visible_row_count(&list_f));
if !vbox_f.has_css_class("just-opened") { if !vbox_f.has_css_class("just-opened") {
@ -669,36 +673,11 @@ fn run_ui(
glib::Propagation::Stop glib::Propagation::Stop
} }
Key::Down => { Key::Down => {
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); bread_utils::gtk_popup::select_next_visible(&list_k);
let mut i = cur + 1;
loop {
match list_k.row_at_index(i) {
Some(r) if r.is_visible() => {
list_k.select_row(Some(&r));
break;
}
Some(_) => i += 1,
None => break,
}
}
glib::Propagation::Stop glib::Propagation::Stop
} }
Key::Up => { Key::Up => {
let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); bread_utils::gtk_popup::select_prev_visible(&list_k);
let mut i = cur - 1;
loop {
if i < 0 {
break;
}
match list_k.row_at_index(i) {
Some(r) if r.is_visible() => {
list_k.select_row(Some(&r));
break;
}
Some(_) => i -= 1,
None => break,
}
}
glib::Propagation::Stop glib::Propagation::Stop
} }
_ => glib::Propagation::Proceed, _ => glib::Propagation::Proceed,
@ -719,22 +698,10 @@ fn run_ui(
}); });
// Click outside launcher panel → close // Click outside launcher panel → close
let close_outside = Rc::clone(&close_all);
let vbox_ref = vbox.clone();
let win_ref = window.clone();
let outside_click = gtk4::GestureClick::new();
outside_click.connect_pressed(move |_, _, x, y| {
if let Some(b) = vbox_ref.compute_bounds(&win_ref) {
if x < b.x() as f64
|| x > (b.x() + b.width()) as f64
|| y < b.y() as f64
|| y > (b.y() + b.height()) as f64
{ {
close_outside(); let close_outside = Rc::clone(&close_all);
bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside());
} }
}
});
window.add_controller(outside_click);
if let Some(req) = screenshot_req.clone() { if let Some(req) = screenshot_req.clone() {
screenshot::dispatch(&window, req); screenshot::dispatch(&window, req);
@ -782,7 +749,9 @@ fn main() {
Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard), Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard),
Ok(bread_utils::singleton::Toggle::KilledExisting) => return, Ok(bread_utils::singleton::Toggle::KilledExisting) => return,
Err(e) => { Err(e) => {
eprintln!("breadbox: single-instance lock unavailable ({e}); continuing without it"); eprintln!(
"breadbox: single-instance lock unavailable ({e}); continuing without it"
);
None None
} }
} }