diff --git a/docs/modules/Launcher.md b/docs/modules/Launcher.md index 14040d2..f5bba43 100644 --- a/docs/modules/Launcher.md +++ b/docs/modules/Launcher.md @@ -12,13 +12,21 @@ Optionally displays a launchable set of favourites. > Type: `launcher` -| | Type | Default | Description | -|--------------|------------|---------|-----------------------------------------------------------------------------------------------------| -| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher | -| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. | -| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. | -| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | -| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items | +| | Type | Default | Description | +|-----------------------------|---------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------| +| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher | +| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. | +| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. | +| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | +| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items | +| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `end` | The location of the ellipses and where to truncate text from. Applies to application names when `show_names` is enabled. | +| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. | +| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | +| `truncate_popup.mode` | `'start'` or `'middle'` or `'end'` or `off` | `middle` | The location of the ellipses and where to truncate text from. Applies to window names within a group popup. | +| `truncate_popup.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. | +| `truncate_popup.max_length` | `integer` | `25` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | + +
JSON diff --git a/src/config/mod.rs b/src/config/mod.rs index 0c3ece6..16191f5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -44,7 +44,7 @@ use std::collections::HashMap; use schemars::JsonSchema; pub use self::common::{CommonConfig, ModuleOrientation, TransitionType}; -pub use self::truncate::TruncateMode; +pub use self::truncate::{EllipsizeMode, TruncateMode}; #[derive(Debug, Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] diff --git a/src/config/truncate.rs b/src/config/truncate.rs index 69fde92..b21ddf7 100644 --- a/src/config/truncate.rs +++ b/src/config/truncate.rs @@ -1,13 +1,14 @@ use gtk::pango::EllipsizeMode as GtkEllipsizeMode; use serde::Deserialize; -#[derive(Debug, Deserialize, Clone, Copy)] +#[derive(Debug, Deserialize, Clone, Copy, Default)] #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum EllipsizeMode { None, Start, Middle, + #[default] End, } @@ -28,6 +29,8 @@ impl From for GtkEllipsizeMode { /// /// The option can be configured in one of two modes. /// +/// **Default**: `Auto (end)` +/// #[derive(Debug, Deserialize, Clone, Copy)] #[serde(untagged)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -56,7 +59,7 @@ pub enum TruncateMode { /// /// **Valid options**: `start`, `middle`, `end` ///
- /// **Default**: `null` + /// **Default**: `end` Auto(EllipsizeMode), /// Length mode defines a fixed point at which to ellipsize. @@ -100,6 +103,12 @@ pub enum TruncateMode { }, } +impl Default for TruncateMode { + fn default() -> Self { + Self::Auto(EllipsizeMode::default()) + } +} + impl TruncateMode { pub const fn length(&self) -> Option { match self { diff --git a/src/modules/launcher/item.rs b/src/modules/launcher/item.rs index 409ed7a..8fa200b 100644 --- a/src/modules/launcher/item.rs +++ b/src/modules/launcher/item.rs @@ -1,15 +1,16 @@ use super::open_state::OpenState; use crate::clients::wayland::ToplevelInfo; -use crate::config::BarPosition; -use crate::gtk_helpers::IronbarGtkExt; +use crate::config::{BarPosition, TruncateMode}; +use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt}; use crate::image::ImageProvider; use crate::modules::launcher::{ItemEvent, LauncherUpdate}; use crate::modules::ModuleUpdateEvent; use crate::{read_lock, try_send}; use glib::Propagation; use gtk::prelude::*; -use gtk::{Button, IconTheme}; +use gtk::{Button, IconTheme, Image, Label, Orientation}; use indexmap::IndexMap; +use std::ops::Deref; use std::rc::Rc; use std::sync::RwLock; use tokio::sync::mpsc::Sender; @@ -134,7 +135,7 @@ pub struct MenuState { } pub struct ItemButton { - pub button: Button, + pub button: ImageTextButton, pub persistent: bool, pub show_names: bool, pub menu_state: Rc>, @@ -145,6 +146,7 @@ pub struct AppearanceOptions { pub show_names: bool, pub show_icons: bool, pub icon_size: i32, + pub truncate: TruncateMode, } impl ItemButton { @@ -156,16 +158,14 @@ impl ItemButton { tx: &Sender>, controller_tx: &Sender, ) -> Self { - let mut button = Button::builder(); + let button = ImageTextButton::new(); if appearance.show_names { - button = button.label(&item.name); + button.label.set_label(&item.name); + button.label.truncate(appearance.truncate); } - let button = button.build(); - if appearance.show_icons { - let gtk_image = gtk::Image::new(); let input = if item.app_id.is_empty() { item.name.clone() } else { @@ -173,26 +173,24 @@ impl ItemButton { }; let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size); if let Some(image) = image { - button.set_image(Some(>k_image)); button.set_always_show_image(true); - if let Err(err) = image.load_into_image(>k_image) { + if let Err(err) = image.load_into_image(&button.image) { error!("{err:?}"); } }; } - let style_context = button.style_context(); - style_context.add_class("item"); + button.add_class("item"); if item.favorite { - style_context.add_class("favorite"); + button.add_class("favorite"); } if item.open_state.is_open() { - style_context.add_class("open"); + button.add_class("open"); } if item.open_state.is_focused() { - style_context.add_class("focused"); + button.add_class("focused"); } { @@ -297,3 +295,39 @@ impl ItemButton { } } } + +#[derive(Debug, Clone)] +pub struct ImageTextButton { + pub(crate) button: Button, + pub(crate) label: Label, + image: Image, +} + +impl ImageTextButton { + pub(crate) fn new() -> Self { + let button = Button::new(); + let container = gtk::Box::new(Orientation::Horizontal, 0); + + let label = Label::new(None); + let image = Image::new(); + + button.add(&container); + + container.add(&image); + container.add(&label); + + ImageTextButton { + button, + label, + image, + } + } +} + +impl Deref for ImageTextButton { + type Target = Button; + + fn deref(&self) -> &Self::Target { + &self.button + } +} diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs index 70e4b56..0ed1c35 100644 --- a/src/modules/launcher/mod.rs +++ b/src/modules/launcher/mod.rs @@ -5,8 +5,10 @@ use self::item::{AppearanceOptions, Item, ItemButton, Window}; use self::open_state::OpenState; use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext}; use crate::clients::wayland::{self, ToplevelEvent}; -use crate::config::CommonConfig; +use crate::config::{CommonConfig, EllipsizeMode, TruncateMode}; use crate::desktop_file::find_desktop_file; +use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt}; +use crate::modules::launcher::item::ImageTextButton; use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock}; use color_eyre::{Help, Report}; use gtk::prelude::*; @@ -54,6 +56,21 @@ pub struct LauncherModule { #[serde(default = "crate::config::default_false")] reversed: bool, + // -- common -- + /// Truncate application names on the bar if they get too long. + /// See [truncate options](module-level-options#truncate-mode). + /// + /// **Default**: `Auto (end)` + #[serde(default)] + truncate: TruncateMode, + + /// Truncate application names in popups if they get too long. + /// See [truncate options](module-level-options#truncate-mode). + /// + /// **Default**: `{ mode = "middle" max_length = 25 }` + #[serde(default = "default_truncate_popup")] + truncate_popup: TruncateMode, + /// See [common options](module-level-options#common-options). #[serde(flatten)] pub common: Option, @@ -63,6 +80,14 @@ const fn default_icon_size() -> i32 { 32 } +const fn default_truncate_popup() -> TruncateMode { + TruncateMode::Length { + mode: EllipsizeMode::Middle, + length: None, + max_length: Some(25), + } +} + #[derive(Debug, Clone)] pub enum LauncherUpdate { /// Adds item @@ -342,6 +367,7 @@ impl Module for LauncherModule { show_names: self.show_names, show_icons: self.show_icons, icon_size: self.icon_size, + truncate: self.truncate, }; let show_names = self.show_names; @@ -370,9 +396,9 @@ impl Module for LauncherModule { ); if self.reversed { - container.pack_end(&button.button, false, false, 0); + container.pack_end(&button.button.button, false, false, 0); } else { - container.add(&button.button); + container.add(&button.button.button); } buttons.insert(item.app_id, button); @@ -393,10 +419,10 @@ impl Module for LauncherModule { if button.persistent { button.set_open(false); if button.show_names { - button.button.set_label(&app_id); + button.button.label.set_label(&app_id); } } else { - container.remove(&button.button); + container.remove(&button.button.button); buttons.shift_remove(&app_id); } } @@ -423,7 +449,7 @@ impl Module for LauncherModule { if show_names { if let Some(button) = buttons.get(&app_id) { - button.button.set_label(&name); + button.button.label.set_label(&name); } } } @@ -459,7 +485,7 @@ impl Module for LauncherModule { placeholder.set_width_request(MAX_WIDTH); container.add(&placeholder); - let mut buttons = IndexMap::>::new(); + let mut buttons = IndexMap::>::new(); { let container = container.clone(); @@ -473,10 +499,11 @@ impl Module for LauncherModule { .windows .into_iter() .map(|(_, win)| { - let button = Button::builder() - .label(clamp(&win.name)) - .height_request(40) - .build(); + // TODO: Currently has a useless image + let button = ImageTextButton::new(); + button.set_height_request(40); + button.label.set_label(&win.name); + button.label.truncate(self.truncate_popup); { let tx = controller_tx.clone(); @@ -498,10 +525,11 @@ impl Module for LauncherModule { ); if let Some(buttons) = buttons.get_mut(&app_id) { - let button = Button::builder() - .height_request(40) - .label(clamp(&win.name)) - .build(); + // TODO: Currently has a useless image + let button = ImageTextButton::new(); + button.set_height_request(40); + button.label.set_label(&win.name); + button.label.truncate(self.truncate_popup); { let tx = controller_tx.clone(); @@ -527,7 +555,7 @@ impl Module for LauncherModule { if let Some(buttons) = buttons.get_mut(&app_id) { if let Some(button) = buttons.get(&win_id) { - button.set_label(&title); + button.label.set_label(&title); } } } @@ -540,8 +568,8 @@ impl Module for LauncherModule { // add app's buttons if let Some(buttons) = buttons.get(&app_id) { for (_, button) in buttons { - button.style_context().add_class("popup-item"); - container.add(button); + button.add_class("popup-item"); + container.add(&button.button); } container.show_all(); @@ -556,21 +584,3 @@ impl Module for LauncherModule { Some(container) } } - -/// Clamps a string at 24 characters. -/// -/// This is a hacky number derived from -/// "what fits inside the 250px popup" -/// and probably won't hold up with wide fonts. -/// -/// TODO: Migrate this to truncate system -/// -fn clamp(str: &str) -> String { - const MAX_CHARS: usize = 24; - - if str.len() > MAX_CHARS { - str.chars().take(MAX_CHARS - 3).collect::() + "..." - } else { - str.to_string() - } -}