diff --git a/docs/Images.md b/docs/Images.md new file mode 100644 index 0000000..9522d6f --- /dev/null +++ b/docs/Images.md @@ -0,0 +1,15 @@ +Ironbar is capable of loading images from multiple sources. +In any situation where an option takes text or an icon, +you can use a string in any of the following formats, and it will automatically be detected as an image: + +| Source | Example | +|-------------------------------|---------------------------------| +| GTK icon theme | `icon:firefox` | +| Local file | `file:///path/to/file.jpg` | +| Remote file (over HTTP/HTTPS) | `https://example.com/image.jpg` | + +Remote images are loaded asynchronously to avoid blocking the UI thread. +Be aware this can cause elements to change size upon load if the image is large enough. + +Note that mixing text and images is not supported. +Your best option here is to use Nerd Font icons instead. \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 32641df..a493976 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -2,6 +2,7 @@ - [Configuration guide](configuration-guide) - [Scripts](scripts) + - [Images](images) - [Styling guide](styling-guide) # Examples diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md index ac55900..1b91d82 100644 --- a/docs/modules/Custom.md +++ b/docs/modules/Custom.md @@ -25,7 +25,7 @@ It is well worth looking at the examples. | `class` | `string` | `null` | Widget class name. | | `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. | | `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). | -| `src` | `string` | `null` | [`image`] Image source. More on this [below](#images). | +| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. | | `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. | | `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. | | `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. | @@ -58,17 +58,6 @@ The following bar commands are supported: - `popup:open` - `popup:close` -### Images - -Ironbar is capable of loading images from multiple sources: - -- GTK icons: `icon:firefox` -- Local files: `file:///path/to/file.jpg` -- Remote files (over HTTP/HTTPS): `https://example.com/image.jpg` - -Remote images are loaded asynchronously to avoid blocking the UI thread. -Be aware this can cause elements to change size upon load if the image is large enough. - --- XML is arguably better-suited and easier to read for this sort of markup, diff --git a/docs/modules/Workspaces.md b/docs/modules/Workspaces.md index b58295f..0caac3a 100644 --- a/docs/modules/Workspaces.md +++ b/docs/modules/Workspaces.md @@ -8,11 +8,11 @@ Shows all current workspaces. Clicking a workspace changes focus to it. > Type: `workspaces` -| Name | Type | Default | Description | -|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------| -| `name_map` | `Map` | `{}` | A map of actual workspace names to their display labels. Workspaces use their actual name if not present in the map. | -| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. | -| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. | +| Name | Type | Default | Description | +|----------------|-----------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name_map` | `Map` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. | +| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. | +| `sort` | `added` or `alphanumeric` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
JSON @@ -72,15 +72,15 @@ end: ```corn { - end = [ - { - type = "workspaces", - name_map.1 = "" - name_map.2 = "" - name_map.3 = "" - all_monitors = false - } - ] + end = [ + { + type = "workspaces", + name_map.1 = "" + name_map.2 = "" + name_map.3 = "" + all_monitors = false + } + ] } ``` diff --git a/src/image/gtk.rs b/src/image/gtk.rs new file mode 100644 index 0000000..49d3b6f --- /dev/null +++ b/src/image/gtk.rs @@ -0,0 +1,48 @@ +use super::ImageProvider; +use gtk::prelude::*; +use gtk::{Button, IconTheme, Image, Label, Orientation}; +use tracing::error; + +pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button { + let button = Button::new(); + + if ImageProvider::is_definitely_image_input(input) { + let image = Image::new(); + match ImageProvider::parse(input, icon_theme, size) + .and_then(|provider| provider.load_into_image(image.clone())) + { + Ok(_) => { + button.set_image(Some(&image)); + button.set_always_show_image(true); + } + Err(err) => { + error!("{err:?}"); + button.set_label(input); + } + } + } else { + button.set_label(input); + } + + button +} + +pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box { + let container = gtk::Box::new(Orientation::Horizontal, 0); + + if ImageProvider::is_definitely_image_input(input) { + let image = Image::new(); + container.add(&image); + + if let Err(err) = ImageProvider::parse(input, icon_theme, size) + .and_then(|provider| provider.load_into_image(image)) + { + error!("{err:?}"); + } + } else { + let label = Label::new(Some(input)); + container.add(&label); + } + + container +} diff --git a/src/image/mod.rs b/src/image/mod.rs new file mode 100644 index 0000000..5e46764 --- /dev/null +++ b/src/image/mod.rs @@ -0,0 +1,5 @@ +mod gtk; +mod provider; + +pub use self::gtk::*; +pub use provider::ImageProvider; diff --git a/src/image.rs b/src/image/provider.rs similarity index 84% rename from src/image.rs rename to src/image/provider.rs index 696c227..5aa6c13 100644 --- a/src/image.rs +++ b/src/image/provider.rs @@ -30,7 +30,7 @@ impl<'a> ImageProvider<'a> { /// /// Note this checks that icons exist in theme, or files exist on disk /// but no other check is performed. - pub fn parse(input: String, theme: &'a IconTheme, size: i32) -> Result { + pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result { let location = Self::get_location(input, theme, size)?; Ok(Self { location, size }) } @@ -45,11 +45,10 @@ impl<'a> ImageProvider<'a> { || input.starts_with("https://") } - fn get_location(input: String, theme: &'a IconTheme, size: i32) -> Result { + fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result> { let (input_type, input_name) = input .split_once(':') - .map(|(t, n)| (Some(t), n)) - .unwrap_or((None, &input)); + .map_or((None, input), |(t, n)| (Some(t), n)); match input_type { Some(input_type) if input_type == "icon" => Ok(ImageLocation::Icon { @@ -66,7 +65,7 @@ impl<'a> ImageProvider<'a> { input_name.chars().skip("steam_app_".len()).collect(), )), None if theme - .lookup_icon(&input, size, IconLookupFlags::empty()) + .lookup_icon(input, size, IconLookupFlags::empty()) .is_some() => { Ok(ImageLocation::Icon { @@ -78,10 +77,10 @@ impl<'a> ImageProvider<'a> { None if PathBuf::from(input_name).exists() => { Ok(ImageLocation::Local(PathBuf::from(input_name))) } - None => match get_desktop_icon_name(input_name) { - Some(input) => Self::get_location(input, theme, size), - None => Err(Report::msg("Unknown image type")), - }, + None => get_desktop_icon_name(input_name).map_or_else( + || Err(Report::msg("Unknown image type")), + |input| Self::get_location(&input, theme, size), + ), } } @@ -120,8 +119,6 @@ impl<'a> ImageProvider<'a> { Continue(false) }); } - - Ok(()) } else { let pixbuf = match &self.location { ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme), @@ -131,8 +128,9 @@ impl<'a> ImageProvider<'a> { }?; image.set_pixbuf(Some(&pixbuf)); - Ok(()) - } + }; + + Ok(()) } /// Attempts to get a `Pixbuf` from the GTK icon theme. @@ -142,10 +140,10 @@ impl<'a> ImageProvider<'a> { None => Ok(None), }?; - match pixbuf { - Some(pixbuf) => Ok(pixbuf), - None => Err(Report::msg("Icon theme does not contain icon '{name}'")), - } + pixbuf.map_or_else( + || Err(Report::msg("Icon theme does not contain icon '{name}'")), + Ok, + ) } /// Attempts to get a `Pixbuf` from a local file. @@ -158,12 +156,14 @@ impl<'a> ImageProvider<'a> { /// using the Steam game ID to look it up. fn get_from_steam_id(&self, steam_id: &str) -> Result { // TODO: Can we load this from icon theme with app id `steam_icon_{}`? - let path = match dirs::data_dir() { - Some(dir) => Ok(dir.join(format!( - "icons/hicolor/32x32/apps/steam_icon_{steam_id}.png" - ))), - None => Err(Report::msg("Missing XDG data dir")), - }?; + let path = dirs::data_dir().map_or_else( + || Err(Report::msg("Missing XDG data dir")), + |dir| { + Ok(dir.join(format!( + "icons/hicolor/32x32/apps/steam_icon_{steam_id}.png" + ))) + }, + )?; self.get_from_file(&path) } diff --git a/src/modules/clock.rs b/src/modules/clock.rs index 2d6d6bd..7dac728 100644 --- a/src/modules/clock.rs +++ b/src/modules/clock.rs @@ -120,6 +120,8 @@ impl Module