use gtk::prelude::*; /// Create a status badge pill label with the given text and style class. /// Style classes: "success", "warning", "error", "info", "neutral" pub fn status_badge(text: &str, style_class: &str) -> gtk::Label { let label = gtk::Label::new(Some(text)); label.add_css_class("status-badge"); label.add_css_class(style_class); label.set_accessible_role(gtk::AccessibleRole::Status); label } /// Create a status badge with a symbolic icon prefix for accessibility. /// The badge contains both an icon and text so information is not color-dependent. pub fn status_badge_with_icon(icon_name: &str, text: &str, style_class: &str) -> gtk::Box { let hbox = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(4) .accessible_role(gtk::AccessibleRole::Status) .build(); hbox.add_css_class("status-badge-with-icon"); hbox.add_css_class(style_class); hbox.update_property(&[gtk::accessible::Property::Label(text)]); let icon = gtk::Image::from_icon_name(icon_name); icon.set_pixel_size(12); hbox.append(&icon); let label = gtk::Label::new(Some(text)); hbox.append(&label); hbox } /// Create a badge showing integration status. #[allow(dead_code)] pub fn integration_badge(integrated: bool) -> gtk::Label { if integrated { status_badge("Integrated", "success") } else { status_badge("Not integrated", "neutral") } } /// Format bytes into a human-readable string. pub fn format_size(bytes: i64) -> String { humansize::format_size(bytes as u64, humansize::BINARY) } /// Build an app icon widget with letter-circle fallback. /// If the icon_path exists and is loadable, show the real icon. /// Otherwise, generate a colored circle with the first letter of the app name. pub fn app_icon(icon_path: Option<&str>, app_name: &str, pixel_size: i32) -> gtk::Widget { // Try to load from path if let Some(icon_path) = icon_path { let path = std::path::Path::new(icon_path); if path.exists() { if let Ok(texture) = gtk::gdk::Texture::from_filename(path) { let image = gtk::Image::builder() .pixel_size(pixel_size) .build(); image.set_paintable(Some(&texture)); return image.upcast(); } } } // Letter-circle fallback build_letter_icon(app_name, pixel_size) } /// Build a colored circle with the first letter of the name as a fallback icon. fn build_letter_icon(name: &str, size: i32) -> gtk::Widget { let letter = name .chars() .find(|c| c.is_alphanumeric()) .unwrap_or('?') .to_uppercase() .next() .unwrap_or('?'); // Pick a color based on the name hash for consistency let color_index = name.bytes().fold(0u32, |acc, b| acc.wrapping_add(b as u32)) % 6; let bg_color = match color_index { 0 => "@accent_bg_color", 1 => "@success_bg_color", 2 => "@warning_bg_color", 3 => "@error_bg_color", 4 => "@accent_bg_color", _ => "@success_bg_color", }; let fg_color = match color_index { 0 => "@accent_fg_color", 1 => "@success_fg_color", 2 => "@warning_fg_color", 3 => "@error_fg_color", 4 => "@accent_fg_color", _ => "@success_fg_color", }; // Use a label styled as a circle with the letter let label = gtk::Label::builder() .label(&letter.to_string()) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .width_request(size) .height_request(size) .build(); // Apply inline CSS via a provider on the display let css_provider = gtk::CssProvider::new(); let unique_class = format!("letter-icon-{}", color_index); let css = format!( "label.{} {{ background: {}; color: {}; border-radius: 50%; min-width: {}px; min-height: {}px; font-size: {}px; font-weight: 700; }}", unique_class, bg_color, fg_color, size, size, size * 4 / 10 ); css_provider.load_from_string(&css); if let Some(display) = gtk::gdk::Display::default() { gtk::style_context_add_provider_for_display( &display, &css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 1, ); } label.add_css_class(&unique_class); label.upcast() } /// Create a copy-to-clipboard button that shows a toast on success. pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>) -> gtk::Button { let btn = gtk::Button::builder() .icon_name("edit-copy-symbolic") .tooltip_text("Copy to clipboard") .valign(gtk::Align::Center) .build(); btn.add_css_class("flat"); btn.update_property(&[gtk::accessible::Property::Label("Copy to clipboard")]); let text = text_to_copy.to_string(); let toast = toast_overlay.cloned(); btn.connect_clicked(move |button| { if let Some(display) = button.display().into() { let clipboard = gtk::gdk::Display::clipboard(&display); clipboard.set_text(&text); if let Some(ref overlay) = toast { overlay.add_toast(adw::Toast::new("Copied to clipboard")); } } }); btn } /// Create a screen-reader live region announcement. /// Inserts a hidden label with AccessibleRole::Alert into the given container, /// which causes AT-SPI to announce the text to screen readers. /// The label auto-removes after a short delay. #[allow(dead_code)] pub fn announce(container: &impl gtk::prelude::IsA, text: &str) { let label = gtk::Label::builder() .label(text) .visible(false) .accessible_role(gtk::AccessibleRole::Alert) .build(); label.update_property(&[gtk::accessible::Property::Label(text)]); if let Some(box_widget) = container.dynamic_cast_ref::() { box_widget.append(&label); label.set_visible(true); let label_clone = label.clone(); let box_clone = box_widget.clone(); glib::timeout_add_local_once(std::time::Duration::from_millis(500), move || { box_clone.remove(&label_clone); }); } }