//! Grid widget containing launcher tiles use gtk4::glib; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use shepherd_api::EntryView; use shepherd_util::EntryId; use std::cell::RefCell; use std::rc::Rc; use crate::tile::LauncherTile; mod imp { use super::*; type LaunchCallback = Rc>>>; pub struct LauncherGrid { pub flow_box: gtk4::FlowBox, pub tiles: RefCell>, pub on_launch: LaunchCallback, } impl Default for LauncherGrid { fn default() -> Self { Self { flow_box: gtk4::FlowBox::new(), tiles: RefCell::new(Vec::new()), on_launch: Rc::new(RefCell::new(None)), } } } #[glib::object_subclass] impl ObjectSubclass for LauncherGrid { const NAME: &'static str = "ShepherdLauncherGrid"; type Type = super::LauncherGrid; type ParentType = gtk4::Box; } impl ObjectImpl for LauncherGrid { fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.set_orientation(gtk4::Orientation::Vertical); obj.set_halign(gtk4::Align::Fill); obj.set_valign(gtk4::Align::Fill); obj.set_hexpand(true); obj.set_vexpand(true); // Configure flow box self.flow_box.set_homogeneous(true); self.flow_box.set_selection_mode(gtk4::SelectionMode::None); self.flow_box.set_max_children_per_line(6); self.flow_box.set_min_children_per_line(2); self.flow_box.set_row_spacing(24); self.flow_box.set_column_spacing(24); self.flow_box.set_halign(gtk4::Align::Center); self.flow_box.set_valign(gtk4::Align::Center); self.flow_box.set_hexpand(true); self.flow_box.set_vexpand(true); self.flow_box.set_can_focus(true); self.flow_box.add_css_class("launcher-grid"); // Wrap in a scrolled window let scrolled = gtk4::ScrolledWindow::new(); scrolled.set_policy(gtk4::PolicyType::Never, gtk4::PolicyType::Automatic); scrolled.set_child(Some(&self.flow_box)); scrolled.set_hexpand(true); scrolled.set_vexpand(true); obj.append(&scrolled); } } impl WidgetImpl for LauncherGrid {} impl BoxImpl for LauncherGrid {} } glib::wrapper! { pub struct LauncherGrid(ObjectSubclass) @extends gtk4::Box, gtk4::Widget, @implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable; } impl LauncherGrid { pub fn new() -> Self { glib::Object::builder().build() } /// Set the callback for when an entry is launched pub fn connect_launch(&self, callback: F) { *self.imp().on_launch.borrow_mut() = Some(Box::new(callback)); } /// Update the grid with new entries pub fn set_entries(&self, entries: Vec) { let imp = self.imp(); // Clear existing tiles while let Some(child) = imp.flow_box.first_child() { imp.flow_box.remove(&child); } imp.tiles.borrow_mut().clear(); // Create tiles for enabled entries for entry in entries { // Skip disabled entries if !entry.enabled { continue; } let tile = LauncherTile::new(); tile.set_entry(entry); // Connect click handler let on_launch = imp.on_launch.clone(); tile.connect_clicked(move |tile| { if let Some(entry_id) = tile.entry_id() && let Some(callback) = on_launch.borrow().as_ref() { callback(entry_id); } }); imp.flow_box.insert(&tile, -1); imp.tiles.borrow_mut().push(tile); } self.focus_first_tile(); } /// Enable or disable all tiles pub fn set_tiles_sensitive(&self, sensitive: bool) { for tile in self.imp().tiles.borrow().iter() { tile.set_sensitive(sensitive); } } pub fn move_focus(&self, direction: gtk4::DirectionType) { self.ensure_focus(); self.imp().flow_box.child_focus(direction); } fn ensure_focus(&self) { if self .imp() .tiles .borrow() .iter() .any(|tile| tile.has_focus()) { return; } self.focus_first_tile(); } fn focus_first_tile(&self) { if let Some(tile) = self.imp().tiles.borrow().first() { tile.grab_focus(); } } } impl Default for LauncherGrid { fn default() -> Self { Self::new() } }