Skip to content

Commit da13b9d

Browse files
committed
feat(launcher): truncate and truncate_popup config options
1 parent 8b05ed5 commit da13b9d

File tree

5 files changed

+123
-62
lines changed

5 files changed

+123
-62
lines changed

docs/modules/Launcher.md

+15-7
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ Optionally displays a launchable set of favourites.
1212

1313
> Type: `launcher`
1414
15-
| | Type | Default | Description |
16-
|--------------|------------|---------|-----------------------------------------------------------------------------------------------------|
17-
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
18-
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
19-
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
20-
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
21-
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
15+
| | Type | Default | Description |
16+
|-----------------------------|---------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------|
17+
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher |
18+
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
19+
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
20+
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
21+
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
22+
| `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. |
23+
| `truncate.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
24+
| `truncate.max_length` | `integer` | `null` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
25+
| `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. |
26+
| `truncate_popup.length` | `integer` | `null` | The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. |
27+
| `truncate_popup.max_length` | `integer` | `25` | The maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
28+
29+
2230
<details>
2331
<summary>JSON</summary>
2432

src/config/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use std::collections::HashMap;
4444
use schemars::JsonSchema;
4545

4646
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
47-
pub use self::truncate::TruncateMode;
47+
pub use self::truncate::{EllipsizeMode, TruncateMode};
4848

4949
#[derive(Debug, Deserialize, Clone)]
5050
#[serde(tag = "type", rename_all = "snake_case")]

src/config/truncate.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
22
use serde::Deserialize;
33

4-
#[derive(Debug, Deserialize, Clone, Copy)]
4+
#[derive(Debug, Deserialize, Clone, Copy, Default)]
55
#[serde(rename_all = "snake_case")]
66
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77
pub enum EllipsizeMode {
88
None,
99
Start,
1010
Middle,
11+
#[default]
1112
End,
1213
}
1314

@@ -28,6 +29,8 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
2829
///
2930
/// The option can be configured in one of two modes.
3031
///
32+
/// **Default**: `Auto (end)`
33+
///
3134
#[derive(Debug, Deserialize, Clone, Copy)]
3235
#[serde(untagged)]
3336
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
@@ -56,7 +59,7 @@ pub enum TruncateMode {
5659
///
5760
/// **Valid options**: `start`, `middle`, `end`
5861
/// <br>
59-
/// **Default**: `null`
62+
/// **Default**: `end`
6063
Auto(EllipsizeMode),
6164

6265
/// Length mode defines a fixed point at which to ellipsize.
@@ -100,6 +103,12 @@ pub enum TruncateMode {
100103
},
101104
}
102105

106+
impl Default for TruncateMode {
107+
fn default() -> Self {
108+
Self::Auto(EllipsizeMode::default())
109+
}
110+
}
111+
103112
impl TruncateMode {
104113
pub const fn length(&self) -> Option<i32> {
105114
match self {

src/modules/launcher/item.rs

+50-16
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
use super::open_state::OpenState;
22
use crate::clients::wayland::ToplevelInfo;
3-
use crate::config::BarPosition;
4-
use crate::gtk_helpers::IronbarGtkExt;
3+
use crate::config::{BarPosition, TruncateMode};
4+
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
55
use crate::image::ImageProvider;
66
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
77
use crate::modules::ModuleUpdateEvent;
88
use crate::{read_lock, try_send};
99
use glib::Propagation;
1010
use gtk::prelude::*;
11-
use gtk::{Button, IconTheme};
11+
use gtk::{Button, IconTheme, Image, Label, Orientation};
1212
use indexmap::IndexMap;
13+
use std::ops::Deref;
1314
use std::rc::Rc;
1415
use std::sync::RwLock;
1516
use tokio::sync::mpsc::Sender;
@@ -134,7 +135,7 @@ pub struct MenuState {
134135
}
135136

136137
pub struct ItemButton {
137-
pub button: Button,
138+
pub button: ImageTextButton,
138139
pub persistent: bool,
139140
pub show_names: bool,
140141
pub menu_state: Rc<RwLock<MenuState>>,
@@ -145,6 +146,7 @@ pub struct AppearanceOptions {
145146
pub show_names: bool,
146147
pub show_icons: bool,
147148
pub icon_size: i32,
149+
pub truncate: TruncateMode,
148150
}
149151

150152
impl ItemButton {
@@ -156,43 +158,39 @@ impl ItemButton {
156158
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
157159
controller_tx: &Sender<ItemEvent>,
158160
) -> Self {
159-
let mut button = Button::builder();
161+
let button = ImageTextButton::new();
160162

161163
if appearance.show_names {
162-
button = button.label(&item.name);
164+
button.label.set_label(&item.name);
165+
button.label.truncate(appearance.truncate);
163166
}
164167

165-
let button = button.build();
166-
167168
if appearance.show_icons {
168-
let gtk_image = gtk::Image::new();
169169
let input = if item.app_id.is_empty() {
170170
item.name.clone()
171171
} else {
172172
item.app_id.clone()
173173
};
174174
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
175175
if let Some(image) = image {
176-
button.set_image(Some(&gtk_image));
177176
button.set_always_show_image(true);
178177

179-
if let Err(err) = image.load_into_image(&gtk_image) {
178+
if let Err(err) = image.load_into_image(&button.image) {
180179
error!("{err:?}");
181180
}
182181
};
183182
}
184183

185-
let style_context = button.style_context();
186-
style_context.add_class("item");
184+
button.add_class("item");
187185

188186
if item.favorite {
189-
style_context.add_class("favorite");
187+
button.add_class("favorite");
190188
}
191189
if item.open_state.is_open() {
192-
style_context.add_class("open");
190+
button.add_class("open");
193191
}
194192
if item.open_state.is_focused() {
195-
style_context.add_class("focused");
193+
button.add_class("focused");
196194
}
197195

198196
{
@@ -297,3 +295,39 @@ impl ItemButton {
297295
}
298296
}
299297
}
298+
299+
#[derive(Debug, Clone)]
300+
pub struct ImageTextButton {
301+
pub(crate) button: Button,
302+
pub(crate) label: Label,
303+
image: Image,
304+
}
305+
306+
impl ImageTextButton {
307+
pub(crate) fn new() -> Self {
308+
let button = Button::new();
309+
let container = gtk::Box::new(Orientation::Horizontal, 0);
310+
311+
let label = Label::new(None);
312+
let image = Image::new();
313+
314+
button.add(&container);
315+
316+
container.add(&image);
317+
container.add(&label);
318+
319+
ImageTextButton {
320+
button,
321+
label,
322+
image,
323+
}
324+
}
325+
}
326+
327+
impl Deref for ImageTextButton {
328+
type Target = Button;
329+
330+
fn deref(&self) -> &Self::Target {
331+
&self.button
332+
}
333+
}

src/modules/launcher/mod.rs

+46-36
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use self::item::{AppearanceOptions, Item, ItemButton, Window};
55
use self::open_state::OpenState;
66
use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext};
77
use crate::clients::wayland::{self, ToplevelEvent};
8-
use crate::config::CommonConfig;
8+
use crate::config::{CommonConfig, EllipsizeMode, TruncateMode};
99
use crate::desktop_file::find_desktop_file;
10+
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
11+
use crate::modules::launcher::item::ImageTextButton;
1012
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
1113
use color_eyre::{Help, Report};
1214
use gtk::prelude::*;
@@ -54,6 +56,21 @@ pub struct LauncherModule {
5456
#[serde(default = "crate::config::default_false")]
5557
reversed: bool,
5658

59+
// -- common --
60+
/// Truncate application names on the bar if they get too long.
61+
/// See [truncate options](module-level-options#truncate-mode).
62+
///
63+
/// **Default**: `Auto (end)`
64+
#[serde(default)]
65+
truncate: TruncateMode,
66+
67+
/// Truncate application names in popups if they get too long.
68+
/// See [truncate options](module-level-options#truncate-mode).
69+
///
70+
/// **Default**: `{ mode = "middle" max_length = 25 }`
71+
#[serde(default = "default_truncate_popup")]
72+
truncate_popup: TruncateMode,
73+
5774
/// See [common options](module-level-options#common-options).
5875
#[serde(flatten)]
5976
pub common: Option<CommonConfig>,
@@ -63,6 +80,14 @@ const fn default_icon_size() -> i32 {
6380
32
6481
}
6582

83+
const fn default_truncate_popup() -> TruncateMode {
84+
TruncateMode::Length {
85+
mode: EllipsizeMode::Middle,
86+
length: None,
87+
max_length: Some(25),
88+
}
89+
}
90+
6691
#[derive(Debug, Clone)]
6792
pub enum LauncherUpdate {
6893
/// Adds item
@@ -342,6 +367,7 @@ impl Module<gtk::Box> for LauncherModule {
342367
show_names: self.show_names,
343368
show_icons: self.show_icons,
344369
icon_size: self.icon_size,
370+
truncate: self.truncate,
345371
};
346372

347373
let show_names = self.show_names;
@@ -370,9 +396,9 @@ impl Module<gtk::Box> for LauncherModule {
370396
);
371397

372398
if self.reversed {
373-
container.pack_end(&button.button, false, false, 0);
399+
container.pack_end(&button.button.button, false, false, 0);
374400
} else {
375-
container.add(&button.button);
401+
container.add(&button.button.button);
376402
}
377403

378404
buttons.insert(item.app_id, button);
@@ -393,10 +419,10 @@ impl Module<gtk::Box> for LauncherModule {
393419
if button.persistent {
394420
button.set_open(false);
395421
if button.show_names {
396-
button.button.set_label(&app_id);
422+
button.button.label.set_label(&app_id);
397423
}
398424
} else {
399-
container.remove(&button.button);
425+
container.remove(&button.button.button);
400426
buttons.shift_remove(&app_id);
401427
}
402428
}
@@ -423,7 +449,7 @@ impl Module<gtk::Box> for LauncherModule {
423449

424450
if show_names {
425451
if let Some(button) = buttons.get(&app_id) {
426-
button.button.set_label(&name);
452+
button.button.label.set_label(&name);
427453
}
428454
}
429455
}
@@ -459,7 +485,7 @@ impl Module<gtk::Box> for LauncherModule {
459485
placeholder.set_width_request(MAX_WIDTH);
460486
container.add(&placeholder);
461487

462-
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
488+
let mut buttons = IndexMap::<String, IndexMap<usize, ImageTextButton>>::new();
463489

464490
{
465491
let container = container.clone();
@@ -473,10 +499,11 @@ impl Module<gtk::Box> for LauncherModule {
473499
.windows
474500
.into_iter()
475501
.map(|(_, win)| {
476-
let button = Button::builder()
477-
.label(clamp(&win.name))
478-
.height_request(40)
479-
.build();
502+
// TODO: Currently has a useless image
503+
let button = ImageTextButton::new();
504+
button.set_height_request(40);
505+
button.label.set_label(&win.name);
506+
button.label.truncate(self.truncate_popup);
480507

481508
{
482509
let tx = controller_tx.clone();
@@ -498,10 +525,11 @@ impl Module<gtk::Box> for LauncherModule {
498525
);
499526

500527
if let Some(buttons) = buttons.get_mut(&app_id) {
501-
let button = Button::builder()
502-
.height_request(40)
503-
.label(clamp(&win.name))
504-
.build();
528+
// TODO: Currently has a useless image
529+
let button = ImageTextButton::new();
530+
button.set_height_request(40);
531+
button.label.set_label(&win.name);
532+
button.label.truncate(self.truncate_popup);
505533

506534
{
507535
let tx = controller_tx.clone();
@@ -527,7 +555,7 @@ impl Module<gtk::Box> for LauncherModule {
527555

528556
if let Some(buttons) = buttons.get_mut(&app_id) {
529557
if let Some(button) = buttons.get(&win_id) {
530-
button.set_label(&title);
558+
button.label.set_label(&title);
531559
}
532560
}
533561
}
@@ -540,8 +568,8 @@ impl Module<gtk::Box> for LauncherModule {
540568
// add app's buttons
541569
if let Some(buttons) = buttons.get(&app_id) {
542570
for (_, button) in buttons {
543-
button.style_context().add_class("popup-item");
544-
container.add(button);
571+
button.add_class("popup-item");
572+
container.add(&button.button);
545573
}
546574

547575
container.show_all();
@@ -556,21 +584,3 @@ impl Module<gtk::Box> for LauncherModule {
556584
Some(container)
557585
}
558586
}
559-
560-
/// Clamps a string at 24 characters.
561-
///
562-
/// This is a hacky number derived from
563-
/// "what fits inside the 250px popup"
564-
/// and probably won't hold up with wide fonts.
565-
///
566-
/// TODO: Migrate this to truncate system
567-
///
568-
fn clamp(str: &str) -> String {
569-
const MAX_CHARS: usize = 24;
570-
571-
if str.len() > MAX_CHARS {
572-
str.chars().take(MAX_CHARS - 3).collect::<String>() + "..."
573-
} else {
574-
str.to_string()
575-
}
576-
}

0 commit comments

Comments
 (0)