use std::path::{Path, PathBuf};
use std::rc::Rc;
use gtk4_layer_shell::{KeyboardMode, Layer, LayerShell};
use relm4::gtk;
use relm4::gtk::{gdk, gio, glib, pango, prelude::*};
use crate::config::{Bookmark, expand_bookmark_path};
#[derive(Debug)]
pub enum BookmarkEvent {
Open(PathBuf),
Edit(usize),
Delete(usize),
Hide,
}
pub fn bookmarks_section(
bookmarks: &[Bookmark],
on_event: Rc<dyn Fn(BookmarkEvent)>,
) -> gtk::Widget {
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
root.add_css_class("bookmarks");
let list = gtk::Box::new(gtk::Orientation::Vertical, 0);
list.add_css_class("bookmarks-list");
if bookmarks.is_empty() {
let empty = gtk::Label::new(Some("No bookmarks — use Add Bookmark"));
empty.add_css_class("bookmarks-empty");
empty.set_xalign(0.0);
list.append(&empty);
}
for (index, bookmark) in bookmarks.iter().enumerate() {
list.append(&bookmark_row(index, bookmark, on_event.clone()));
}
let scroller = gtk::ScrolledWindow::new();
scroller.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
scroller.set_propagate_natural_height(true);
scroller.set_max_content_height(220);
scroller.set_child(Some(&list));
let overlay = gtk::Overlay::new();
overlay.set_child(Some(&scroller));
let close = gtk::Button::from_icon_name("window-close-symbolic");
close.add_css_class("bookmarks-close");
close.set_tooltip_text(Some("Hide bookmarks"));
close.set_halign(gtk::Align::End);
close.set_valign(gtk::Align::Start);
{
let on_event = on_event.clone();
close.connect_clicked(move |_| on_event(BookmarkEvent::Hide));
}
overlay.add_overlay(&close);
root.append(&overlay);
root.upcast()
}
fn bookmark_row(index: usize, bookmark: &Bookmark, on_event: Rc<dyn Fn(BookmarkEvent)>) -> gtk::Widget {
let path = expand_bookmark_path(&bookmark.path);
let row = gtk::Button::new();
row.add_css_class("bookmark-row");
row.set_tooltip_text(Some(&path.display().to_string()));
let content = gtk::Box::new(gtk::Orientation::Horizontal, 8);
let icon = gtk::Image::from_icon_name("user-bookmarks-symbolic");
icon.add_css_class("bookmark-icon");
let label = gtk::Label::new(Some(&bookmark.name));
label.set_xalign(0.0);
label.set_hexpand(true);
label.set_ellipsize(pango::EllipsizeMode::End);
label.add_css_class("bookmark-label");
content.append(&icon);
content.append(&label);
row.set_child(Some(&content));
{
let on_event = on_event.clone();
let path = path.clone();
row.connect_clicked(move |_| on_event(BookmarkEvent::Open(path.clone())));
}
let popover = gtk::Popover::new();
popover.add_css_class("bookmark-menu-popover");
popover.set_has_arrow(true);
let menu = gtk::Box::new(gtk::Orientation::Vertical, 0);
menu.add_css_class("bookmark-menu");
let open_item = menu_button("Open");
let edit_item = menu_button("Edit...");
let delete_item = menu_button("Delete");
menu.append(&open_item);
menu.append(&edit_item);
menu.append(&delete_item);
popover.set_child(Some(&menu));
{
let on_event = on_event.clone();
let popover = popover.clone();
let path = path.clone();
open_item.connect_clicked(move |_| {
popover.popdown();
on_event(BookmarkEvent::Open(path.clone()));
});
}
{
let on_event = on_event.clone();
let popover = popover.clone();
edit_item.connect_clicked(move |_| {
popover.popdown();
on_event(BookmarkEvent::Edit(index));
});
}
{
let on_event = on_event.clone();
let popover = popover.clone();
delete_item.connect_clicked(move |_| {
popover.popdown();
on_event(BookmarkEvent::Delete(index));
});
}
popover.set_parent(&row);
{
let popover = popover.clone();
row.connect_destroy(move |_| popover.unparent());
}
let gesture = gtk::GestureClick::new();
gesture.set_button(gdk::BUTTON_SECONDARY);
{
let popover = popover.clone();
gesture.connect_pressed(move |gesture, _, x, y| {
gesture.set_state(gtk::EventSequenceState::Claimed);
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
popover.popup();
});
}
row.add_controller(gesture);
row.upcast()
}
fn menu_button(label: &str) -> gtk::Button {
let button = gtk::Button::with_label(label);
button.add_css_class("flat");
button.set_halign(gtk::Align::Fill);
if let Some(child) = button.child().and_downcast::<gtk::Label>() {
child.set_xalign(0.0);
}
button
}
pub fn show_bookmark_editor(
parent: >k::Window,
name: &str,
path: &Path,
on_save: impl Fn(String, PathBuf) + 'static,
) {
let window = gtk::Window::new();
window.set_title(Some("Edit Bookmark"));
window.set_default_size(420, -1);
window.set_transient_for(Some(parent));
if gtk4_layer_shell::is_supported() && !window.is_layer_window() {
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_keyboard_mode(KeyboardMode::Exclusive);
}
let form = gtk::Box::new(gtk::Orientation::Vertical, 8);
form.add_css_class("bookmark-editor");
let name_label = gtk::Label::new(Some("Name"));
name_label.set_xalign(0.0);
name_label.add_css_class("bookmark-field-label");
let name_entry = gtk::Entry::new();
name_entry.set_text(name);
name_entry.set_hexpand(true);
let path_label = gtk::Label::new(Some("Path"));
path_label.set_xalign(0.0);
path_label.add_css_class("bookmark-field-label");
let path_row = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let path_entry = gtk::Entry::new();
path_entry.set_text(&path.display().to_string());
path_entry.set_hexpand(true);
let browse = gtk::Button::with_label("Browse...");
path_row.append(&path_entry);
path_row.append(&browse);
form.append(&name_label);
form.append(&name_entry);
form.append(&path_label);
form.append(&path_row);
let footer = gtk::Box::new(gtk::Orientation::Horizontal, 6);
footer.set_halign(gtk::Align::End);
footer.add_css_class("bookmark-editor-footer");
let cancel = gtk::Button::with_label("Cancel");
let save = gtk::Button::with_label("Save");
save.add_css_class("suggested-action");
footer.append(&cancel);
footer.append(&save);
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
root.add_css_class("bookmark-editor-root");
root.append(&form);
root.append(>k::Separator::new(gtk::Orientation::Horizontal));
root.append(&footer);
window.set_child(Some(&root));
{
let path_entry = path_entry.clone();
let window = window.clone();
browse.connect_clicked(move |_| {
let dialog = gtk::FileDialog::new();
let path_entry = path_entry.clone();
dialog.select_folder(Some(&window), None::<&gio::Cancellable>, move |res| {
if let Ok(file) = res
&& let Some(p) = file.path()
{
path_entry.set_text(&p.display().to_string());
}
});
});
}
let submit: Rc<dyn Fn()> = {
let name_entry = name_entry.clone();
let path_entry = path_entry.clone();
let window = window.clone();
let on_save = Rc::new(on_save);
Rc::new(move || {
let raw = path_entry.text().trim().to_string();
if raw.is_empty() {
return;
}
let path = expand_bookmark_path(Path::new(&raw));
let typed = name_entry.text().trim().to_string();
let name = if typed.is_empty() { Bookmark::default_name(&path) } else { typed };
on_save(name, path);
window.close();
})
};
{
let submit = submit.clone();
save.connect_clicked(move |_| submit());
}
{
let submit = submit.clone();
name_entry.connect_activate(move |_| submit());
}
{
let submit = submit.clone();
path_entry.connect_activate(move |_| submit());
}
{
let window = window.clone();
cancel.connect_clicked(move |_| window.close());
}
{
let keys = gtk::EventControllerKey::new();
window.add_controller(keys.clone());
let window = window.clone();
keys.connect_key_pressed(move |_, key, _, _| {
if key == gdk::Key::Escape {
window.close();
glib::Propagation::Stop
} else {
glib::Propagation::Proceed
}
});
}
window.present();
}