1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-04-19 11:24:24 +02:00

feat(workspaces): support for using images in name_map

This commit is contained in:
Jake Stanger 2023-01-29 22:48:42 +00:00
parent 3cf9be89fd
commit b054c17d14
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
13 changed files with 132 additions and 64 deletions

15
docs/Images.md Normal file
View file

@ -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.

View file

@ -2,6 +2,7 @@
- [Configuration guide](configuration-guide)
- [Scripts](scripts)
- [Images](images)
- [Styling guide](styling-guide)
# Examples

View file

@ -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,

View file

@ -8,11 +8,11 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
> Type: `workspaces`
| Name | Type | Default | Description |
|----------------|---------------------------|----------------|----------------------------------------------------------------------------------------------------------------------|
| `name_map` | `Map<string, string>` | `{}` | 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<string, string/image>` | `{}` | 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. |
<details>
<summary>JSON</summary>
@ -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
}
]
}
```

48
src/image/gtk.rs Normal file
View file

@ -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
}

5
src/image/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod gtk;
mod provider;
pub use self::gtk::*;
pub use provider::ImageProvider;

View file

@ -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<Self> {
pub fn parse(input: &str, theme: &'a IconTheme, size: i32) -> Result<Self> {
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<ImageLocation> {
fn get_location(input: &str, theme: &'a IconTheme, size: i32) -> Result<ImageLocation<'a>> {
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<Pixbuf> {
// 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)
}

View file

@ -120,6 +120,8 @@ impl Module<Button> for ClockModule {
});
}
container.show_all();
Some(container)
}
}

View file

@ -103,9 +103,9 @@ impl Widget {
}
if let Some(widgets) = self.widgets {
widgets.into_iter().for_each(|widget| {
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme)
});
for widget in widgets {
widget.add_to(&container, tx.clone(), bar_orientation, icon_theme);
}
}
container
@ -185,7 +185,7 @@ impl Widget {
if let Some(src) = self.src {
let size = self.size.unwrap_or(32);
if let Err(err) = ImageProvider::parse(src, icon_theme, size)
if let Err(err) = ImageProvider::parse(&src, icon_theme, size)
.and_then(|image| image.load_into_image(gtk_image.clone()))
{
error!("{err:?}");
@ -292,16 +292,18 @@ impl Module<gtk::Box> for CustomModule {
}
if let Some(popup) = self.popup {
popup.into_iter().for_each(|widget| {
for widget in popup {
widget.add_to(
&container,
tx.clone(),
Orientation::Horizontal,
info.icon_theme,
)
});
);
}
}
container.show_all();
Some(container)
}
}

View file

@ -111,7 +111,7 @@ impl Module<gtk::Box> for FocusedModule {
let icon_theme = icon_theme.clone();
context.widget_rx.attach(None, move |(name, id)| {
if self.show_icon {
if let Err(err) = ImageProvider::parse(id, &icon_theme, self.icon_size)
if let Err(err) = ImageProvider::parse(&id, &icon_theme, self.icon_size)
.and_then(|image| image.load_into_image(icon.clone()))
{
error!("{err:?}");

View file

@ -156,7 +156,7 @@ impl ItemButton {
if show_icons {
let gtk_image = gtk::Image::new();
let image = ImageProvider::parse(item.app_id.clone(), icon_theme, 32);
let image = ImageProvider::parse(&item.app_id.clone(), icon_theme, 32);
match image {
Ok(image) => {
button.set_image(Some(&gtk_image));

View file

@ -1,10 +1,11 @@
use crate::clients::compositor::{Compositor, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Button;
use gtk::{Button, IconTheme};
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
@ -49,12 +50,13 @@ fn create_button(
name: &str,
focused: bool,
name_map: &HashMap<String, String>,
icon_theme: &IconTheme,
tx: &Sender<String>,
) -> Button {
let button = Button::builder()
.label(name_map.get(name).map_or(name, String::as_str))
.name(name)
.build();
let label = name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, icon_theme, 32);
button.set_widget_name(name);
let style_context = button.style_context();
style_context.add_class("item");
@ -154,6 +156,7 @@ impl Module<gtk::Box> for WorkspacesModule {
{
let container = container.clone();
let output_name = info.output_name.to_string();
let icon_theme = info.icon_theme.clone();
// keep track of whether init event has fired previously
// since it fires for every workspace subscriber
@ -170,6 +173,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&workspace.name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
container.add(&item);
@ -204,6 +208,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);
@ -227,6 +232,7 @@ impl Module<gtk::Box> for WorkspacesModule {
&name,
workspace.focused,
&name_map,
&icon_theme,
&context.controller_tx,
);

View file

@ -133,7 +133,7 @@ impl Popup {
/// Shows the popup
pub fn show(&self, geometry: ButtonGeometry) {
self.window.show_all();
self.window.show();
self.set_pos(geometry);
}