shepherd-launcher/crates/shepherd-launcher-ui/src/grid.rs
2026-02-07 18:16:00 -05:00

170 lines
4.8 KiB
Rust

//! 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<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>;
pub struct LauncherGrid {
pub flow_box: gtk4::FlowBox,
pub tiles: RefCell<Vec<LauncherTile>>,
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<imp::LauncherGrid>)
@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<F: Fn(EntryId) + 'static>(&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<EntryView>) {
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()
}
}