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

155 lines
5.1 KiB
Rust

//! Individual tile widget for the launcher grid
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use shepherd_api::EntryView;
use std::cell::RefCell;
mod imp {
use super::*;
#[derive(Default)]
pub struct LauncherTile {
pub entry: RefCell<Option<EntryView>>,
pub icon: gtk4::Image,
pub label: gtk4::Label,
}
#[glib::object_subclass]
impl ObjectSubclass for LauncherTile {
const NAME: &'static str = "ShepherdLauncherTile";
type Type = super::LauncherTile;
type ParentType = gtk4::Button;
}
impl ObjectImpl for LauncherTile {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
// Create layout
let content = gtk4::Box::new(gtk4::Orientation::Vertical, 8);
content.set_halign(gtk4::Align::Center);
content.set_valign(gtk4::Align::Center);
// Icon
self.icon.set_pixel_size(96);
self.icon.set_icon_name(Some("application-x-executable"));
content.append(&self.icon);
// Label
self.label.set_wrap(true);
self.label.set_wrap_mode(gtk4::pango::WrapMode::Word);
self.label.set_justify(gtk4::Justification::Center);
self.label.set_max_width_chars(12);
self.label.add_css_class("tile-label");
content.append(&self.label);
obj.set_child(Some(&content));
obj.add_css_class("launcher-tile");
obj.add_css_class("flat");
obj.set_size_request(160, 160);
obj.set_can_focus(true);
}
}
impl WidgetImpl for LauncherTile {}
impl ButtonImpl for LauncherTile {}
}
glib::wrapper! {
pub struct LauncherTile(ObjectSubclass<imp::LauncherTile>)
@extends gtk4::Button, gtk4::Widget,
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
}
impl LauncherTile {
pub fn new() -> Self {
glib::Object::builder().build()
}
pub fn set_entry(&self, entry: EntryView) {
let imp = self.imp();
// Set label
imp.label.set_text(&entry.label);
// Determine fallback icon based on entry kind
let fallback_icon = match entry.kind_tag {
shepherd_api::EntryKindTag::Process => "application-x-executable",
shepherd_api::EntryKindTag::Snap => "application-x-executable",
shepherd_api::EntryKindTag::Steam => "application-x-executable",
shepherd_api::EntryKindTag::Flatpak => "application-x-executable",
shepherd_api::EntryKindTag::Vm => "computer",
shepherd_api::EntryKindTag::Media => "video-x-generic",
shepherd_api::EntryKindTag::Custom => "applications-other",
};
// Set icon, first trying to load as an image file, then as an icon name
if let Some(ref icon_ref) = entry.icon_ref {
let mut loaded = false;
// First, try to load as an image file (JPG, PNG, etc.)
// Expand ~ to home directory if present
let expanded_path = if icon_ref.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
icon_ref.replacen("~", &home.to_string_lossy(), 1)
} else {
icon_ref.clone()
}
} else {
icon_ref.clone()
};
let path = std::path::Path::new(&expanded_path);
if path.exists() && path.is_file() {
// Try to load as an image file
imp.icon.set_from_file(Some(path));
loaded = true;
}
// If not loaded as a file, try as an icon name from the theme
if !loaded {
let icon_theme = gtk4::IconTheme::for_display(&self.display());
if icon_theme.has_icon(icon_ref) {
imp.icon.set_icon_name(Some(icon_ref));
} else {
imp.icon.set_icon_name(Some(fallback_icon));
}
}
} else {
imp.icon.set_icon_name(Some(fallback_icon));
}
// Entry is available if enabled and has no blocking reasons
let available = entry.enabled && entry.reasons.is_empty();
self.set_sensitive(available);
// Add tooltip with reason if not available
if !available && !entry.reasons.is_empty() {
// Format the first reason for tooltip
let reason_text = format!("{:?}", entry.reasons[0]);
self.set_tooltip_text(Some(&reason_text));
} else {
self.set_tooltip_text(None);
}
*imp.entry.borrow_mut() = Some(entry);
}
pub fn entry(&self) -> Option<EntryView> {
self.imp().entry.borrow().clone()
}
pub fn entry_id(&self) -> Option<shepherd_util::EntryId> {
self.imp().entry.borrow().as_ref().map(|e| e.entry_id.clone())
}
}
impl Default for LauncherTile {
fn default() -> Self {
Self::new()
}
}