diff --git a/src/image/gtk.rs b/src/image/gtk.rs index 6147349d..994258e4 100644 --- a/src/image/gtk.rs +++ b/src/image/gtk.rs @@ -1,14 +1,57 @@ use crate::gtk_helpers::IronbarLabelExt; use crate::image; use gtk::prelude::*; -use gtk::{Button, ContentFit, Label, Orientation, Picture}; +use gtk::{Button, ContentFit, Image, Label, Orientation, Picture}; +use std::cell::RefCell; use std::ops::Deref; +use std::rc::Rc; + +const ICON_PREFIX: &str = "icon:"; +const IMAGE_CLASSES: &[&str] = &["icon", "image"]; + +fn create_icon(input: &str, size: i32, provider: &image::Provider, on_result: F) +where + F: FnOnce(Result) + 'static, +{ + // Uses Image for themed icons (icon: prefix) or Picture for files (file://, http://). + if let Some(icon_name) = input.strip_prefix(ICON_PREFIX) { + let image = Image::builder().build(); + image.set_css_classes(IMAGE_CLASSES); + image.set_paintable(Some(&provider.lookup_icon( + icon_name, + size, + image.scale_factor(), + ))); + image.set_pixel_size(size); + on_result(Ok(image.upcast())); + } else { + let picture = Picture::builder() + .content_fit(ContentFit::ScaleDown) + .build(); + picture.set_css_classes(IMAGE_CLASSES); + + let provider = provider.clone(); + let input = input.to_owned(); + let picture_clone = picture.clone(); + + glib::spawn_future_local(async move { + if provider + .load_into_picture(&input, size, false, &picture_clone) + .await + .unwrap_or(false) + { + on_result(Ok(picture_clone.upcast())); + } else { + on_result(Err(())); + } + }); + } +} #[derive(Debug, Clone)] #[cfg(any( feature = "cairo", feature = "clipboard", - feature = "clipboard", feature = "keyboard", feature = "launcher", feature = "music", @@ -23,7 +66,6 @@ pub struct IconButton { #[cfg(any( feature = "cairo", feature = "clipboard", - feature = "clipboard", feature = "keyboard", feature = "launcher", feature = "music", @@ -33,31 +75,16 @@ pub struct IconButton { impl IconButton { pub fn new(input: &str, size: i32, image_provider: image::Provider) -> Self { let button = Button::new(); - let image = Picture::builder() - .content_fit(ContentFit::ScaleDown) - .build(); let label = Label::builder().use_markup(true).build(); label.set_label_escaped(input); if image::Provider::is_explicit_input(input) { - image.add_css_class("image"); - image.add_css_class("icon"); - - let image = image.clone(); - let label = label.clone(); - let button = button.clone(); - - let input = input.to_string(); // ew - - glib::spawn_future_local(async move { - if let Ok(true) = image_provider - .load_into_picture(&input, size, false, &image) - .await - { - button.set_child(Some(&image)); - } else { - button.set_child(Some(&label)); - } + let button_clone = button.clone(); + let label_clone = label.clone(); + + create_icon(input, size, &image_provider, move |result| match result { + Ok(widget) => button_clone.set_child(Some(&widget)), + Err(()) => button_clone.set_child(Some(&label_clone)), }); } else { button.set_child(Some(&label)); @@ -72,14 +99,13 @@ impl IconButton { } #[cfg(any( + feature = "cairo", feature = "clipboard", feature = "keyboard", + feature = "launcher", feature = "music", feature = "notifications", feature = "workspaces", - feature = "cairo", - feature = "clipboard", - feature = "launcher", ))] impl Deref for IconButton { type Target = Button; @@ -101,7 +127,7 @@ pub struct IconLabel { provider: image::Provider, container: gtk::Box, label: Label, - image: Picture, + current_icon: Rc>>, size: i32, } @@ -122,31 +148,23 @@ impl IconLabel { label.add_css_class("icon"); label.add_css_class("text-icon"); - let image = Picture::builder() - .content_fit(ContentFit::ScaleDown) - .build(); - image.add_css_class("icon"); - image.add_css_class("image"); - - container.append(&image); - container.append(&label); + let current_icon = Rc::new(RefCell::new(None)); if image::Provider::is_explicit_input(input) { - let image = image.clone(); - let label = label.clone(); - let image_provider = image_provider.clone(); - - let input = input.to_string(); - - glib::spawn_future_local(async move { - let res = image_provider - .load_into_picture(&input, size, false, &image) - .await; - if matches!(res, Ok(true)) { - image.set_visible(true); - } else { - label.set_label_escaped(&input); - label.set_visible(true); + let label_clone = label.clone(); + let input_str = input.to_owned(); + let container_clone = container.clone(); + let current_icon_clone = current_icon.clone(); + + create_icon(input, size, image_provider, move |result| match result { + Ok(widget) => { + // This executes after the label is appended below, so prepend is used to keep the order. + container_clone.prepend(&widget); + *current_icon_clone.borrow_mut() = Some(widget); + } + Err(()) => { + label_clone.set_label_escaped(&input_str); + label_clone.set_visible(true); } }); } else { @@ -154,51 +172,54 @@ impl IconLabel { label.set_visible(true); } + container.append(&label); + Self { provider: image_provider.clone(), container, label, - image, + current_icon, size, } } pub fn set_label(&self, input: Option<&str>) { - let label = &self.label; - let image = &self.image; - - if let Some(input) = input { - if image::Provider::is_explicit_input(input) { - let provider = self.provider.clone(); - let size = self.size; - - let label = label.clone(); - let image = image.clone(); - let input = input.to_string(); - - glib::spawn_future_local(async move { - let res = provider - .load_into_picture(&input, size, false, &image) - .await; - if matches!(res, Ok(true)) { - label.set_visible(false); - image.set_visible(true); - } else { - label.set_label_escaped(&input); - - image.set_visible(false); - label.set_visible(true); - } - }); - } else { - label.set_label_escaped(input); + // Remove old icon if present + if let Some(old) = self.current_icon.borrow_mut().take() { + self.container.remove(&old); + } - image.set_visible(false); - label.set_visible(true); + match input { + Some(input) if image::Provider::is_explicit_input(input) => { + self.label.set_visible(false); + let label_clone = self.label.clone(); + let input_str = input.to_owned(); + let container_clone = self.container.clone(); + let current_icon_clone = self.current_icon.clone(); + + create_icon( + input, + self.size, + &self.provider, + move |result| match result { + Ok(widget) => { + container_clone.prepend(&widget); + *current_icon_clone.borrow_mut() = Some(widget); + } + Err(()) => { + label_clone.set_label_escaped(&input_str); + label_clone.set_visible(true); + } + }, + ); + } + Some(input) => { + self.label.set_label_escaped(input); + self.label.set_visible(true); + } + None => { + self.label.set_visible(false); } - } else { - label.set_visible(false); - image.set_visible(false); } } diff --git a/src/image/provider.rs b/src/image/provider.rs index d798b684..5a01ce00 100644 --- a/src/image/provider.rs +++ b/src/image/provider.rs @@ -6,13 +6,24 @@ use glib::Bytes; use gtk::gdk::{Paintable, Texture}; use gtk::gdk_pixbuf::Pixbuf; use gtk::prelude::*; -use gtk::{IconLookupFlags, IconTheme, Picture, TextDirection}; +use gtk::{IconLookupFlags, IconPaintable, IconTheme, Picture, TextDirection}; use std::cell::RefCell; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tracing::{debug, trace, warn}; +fn lookup_icon(theme: &IconTheme, name: &str, size: i32, scale: i32) -> IconPaintable { + theme.lookup_icon( + name, + &[], + size, + scale, + TextDirection::None, + IconLookupFlags::empty(), + ) +} + #[derive(Debug, Clone, Eq, Hash, PartialEq)] struct ImageRef { size: i32, @@ -237,21 +248,9 @@ impl Provider { const FALLBACK_ICON_NAME: &str = "dialog-question-symbolic"; let buf = match &image_ref.location { - Some(ImageLocation::Icon(name)) => { - Ok(Some( - image_ref - .theme - .lookup_icon( - name, - &[], // setting fallback here causes issue loading some icons - image_ref.size, - scale, - TextDirection::None, - IconLookupFlags::empty(), - ) - .upcast::(), - )) - } + Some(ImageLocation::Icon(name)) => Ok(Some( + lookup_icon(&image_ref.theme, name, image_ref.size, scale).upcast::(), + )), Some(ImageLocation::Local(path)) if path.extension().unwrap_or_default() == "svg" => { let scaled_size = image_ref.size * scale; @@ -302,16 +301,7 @@ impl Provider { .map(|t| t.scale(image_ref.size as f64, image_ref.size as f64)) } None if use_fallback => Ok(Some( - image_ref - .theme - .lookup_icon( - FALLBACK_ICON_NAME, - &[], - image_ref.size, - scale, - TextDirection::None, - IconLookupFlags::empty(), - ) + lookup_icon(&image_ref.theme, FALLBACK_ICON_NAME, image_ref.size, scale) .upcast::(), )), None => Ok(None), @@ -338,6 +328,11 @@ impl Provider { .expect("theme should be set on bar init") } + /// Looks up an icon from the configured icon theme. + pub fn lookup_icon(&self, name: &str, size: i32, scale: i32) -> IconPaintable { + lookup_icon(&self.icon_theme(), name, size, scale) + } + /// Sets the custom icon theme name. /// If no name is provided, the system default is used. pub fn set_icon_theme(&self, theme: Option<&str>) {