diff --git a/src/ui/app_card.rs b/src/ui/app_card.rs index 2ecd49a..f91e634 100644 --- a/src/ui/app_card.rs +++ b/src/ui/app_card.rs @@ -1,4 +1,5 @@ use gtk::prelude::*; +use gtk::accessible::Property as AccessibleProperty; use crate::core::database::AppImageRecord; use crate::core::fuse::FuseStatus; @@ -17,57 +18,62 @@ pub fn build_app_card(record: &AppImageRecord) -> gtk::FlowBoxChild { .halign(gtk::Align::Center) .build(); card.add_css_class("app-card"); - card.set_size_request(160, -1); + card.set_size_request(180, -1); - // Icon (48x48) - let icon = if let Some(ref icon_path) = record.icon_path { - let path = std::path::Path::new(icon_path); - if path.exists() { - let paintable = gtk::gdk::Texture::from_filename(path).ok(); - let image = gtk::Image::builder() - .pixel_size(48) - .build(); - if let Some(texture) = paintable { - image.set_paintable(Some(&texture)); - } else { - image.set_icon_name(Some("application-x-executable-symbolic")); - } - image - } else { - gtk::Image::builder() - .icon_name("application-x-executable-symbolic") - .pixel_size(48) - .build() - } - } else { - gtk::Image::builder() - .icon_name("application-x-executable-symbolic") - .pixel_size(48) - .build() - }; - - // App name + // Icon (64x64) with integration emblem overlay let name = record.app_name.as_deref().unwrap_or(&record.filename); + let icon_widget = widgets::app_icon( + record.icon_path.as_deref(), + name, + 64, + ); + + // If integrated, overlay a small checkmark emblem + if record.integrated { + let overlay = gtk::Overlay::new(); + overlay.set_child(Some(&icon_widget)); + + let emblem = gtk::Image::from_icon_name("emblem-ok-symbolic"); + emblem.set_pixel_size(16); + emblem.add_css_class("integration-emblem"); + emblem.set_halign(gtk::Align::End); + emblem.set_valign(gtk::Align::End); + emblem.update_property(&[AccessibleProperty::Label("Integrated into desktop menu")]); + overlay.add_overlay(&emblem); + + card.append(&overlay); + } else { + card.append(&icon_widget); + } + + // App name - use libadwaita built-in heading class let name_label = gtk::Label::builder() .label(name) - .css_classes(["app-card-name"]) + .css_classes(["heading"]) .ellipsize(gtk::pango::EllipsizeMode::End) - .max_width_chars(18) + .max_width_chars(20) .build(); - // Version + // Version - use libadwaita built-in caption + dimmed let version_text = record.app_version.as_deref().unwrap_or(""); let version_label = gtk::Label::builder() .label(version_text) - .css_classes(["app-card-version"]) + .css_classes(["caption", "dimmed"]) .ellipsize(gtk::pango::EllipsizeMode::End) .build(); - card.append(&icon); + // File size as subtle caption + let size_text = widgets::format_size(record.size_bytes); + let size_label = gtk::Label::builder() + .label(&size_text) + .css_classes(["caption", "dimmed"]) + .build(); + card.append(&name_label); if !version_text.is_empty() { card.append(&version_label); } + card.append(&size_label); // Status badges row let badges = gtk::Box::builder() @@ -104,16 +110,40 @@ pub fn build_app_card(record: &AppImageRecord) -> gtk::FlowBoxChild { } } - // Integration badge (only show if not integrated, to reduce clutter) - if !record.integrated { - badges.append(&widgets::status_badge("Not integrated", "neutral")); - } - card.append(&badges); let child = gtk::FlowBoxChild::builder() .child(&card) .build(); + // Accessible label for screen readers + let accessible_name = build_accessible_label(record); + child.update_property(&[AccessibleProperty::Label(&accessible_name)]); + child } + +/// Build a descriptive accessible label for screen readers. +fn build_accessible_label(record: &AppImageRecord) -> String { + let name = record.app_name.as_deref().unwrap_or(&record.filename); + let mut parts = vec![name.to_string()]; + + if let Some(ref ver) = record.app_version { + parts.push(format!("version {}", ver)); + } + + parts.push(widgets::format_size(record.size_bytes)); + + if record.integrated { + parts.push("integrated".to_string()); + } + + if let Some(ref ws) = record.wayland_status { + let status = WaylandStatus::from_str(ws); + if status != WaylandStatus::Unknown { + parts.push(format!("Wayland: {}", status.label())); + } + } + + parts.join(", ") +}