1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-16 14:21:03 +02:00

feat(launcher): pagination controls when item count is reached

Resolves #633
This commit is contained in:
Jake Stanger 2025-02-21 21:07:15 +00:00
parent 5049226289
commit 183ca402d4
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
3 changed files with 282 additions and 29 deletions

View file

@ -13,22 +13,23 @@ Optionally displays a launchable set of favourites.
> Type: `launcher` > Type: `launcher`
| | Type | Default | Description | | | Type | Default | Description |
|-----------------------------|---------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------| |-----------------------------|---------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `favorites` | `string[]` | `[]` | List of app IDs (or classes) to always show at the start of the launcher. | | `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_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. | | `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). | | `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items | | `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
| `minimize_focused` | `boolean` | `true` | Whether to minimize a focused window when its icon is clicked. Only minimizes single windows. | | `minimize_focused` | `boolean` | `true` | Whether to minimize a focused window when its icon is clicked. Only minimizes single windows. |
| `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.mode` | `'start'` or `'middle'` or `'end'` or `off` | `end` | 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.length` | `integer` | `null` | 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.max_length` | `integer` | `null` | 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.mode` | `'start'` or `'middle'` or `'end'` or `off` | `middle` | 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.length` | `integer` | `null` | 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. | | `truncate_popup.max_length` | `integer` | `25` | Maximum number of characters before truncating. Leave blank to let GTK automatically handle. |
| `page_size` | `integer` | `1000` | Number of items to show on a page. When the number of items is reached, controls appear which can be used to move forward/back through the list of items. |
| `icons.page_back` | `string` or [image](images) | `󰅁` | Icon to show for page back button. |
| `icons.page_forward` | `string` or [image](images) | `󰅂` | Icon to show for page forward button. |
<details> <details>
<summary>JSON</summary> <summary>JSON</summary>
@ -104,14 +105,17 @@ start:
## Styling ## Styling
| Selector | Description | | Selector | Description |
|-------------------------------|--------------------------| |--------------------------------------|---------------------------|
| `.launcher` | Launcher widget box | | `.launcher` | Launcher widget box |
| `.launcher .item` | App button | | `.launcher .item` | App button |
| `.launcher .item.open` | App button (open app) | | `.launcher .item.open` | App button (open app) |
| `.launcher .item.focused` | App button (focused app) | | `.launcher .item.focused` | App button (focused app) |
| `.launcher .item.urgent` | App button (urgent app) | | `.launcher .item.urgent` | App button (urgent app) |
| `.popup-launcher` | Popup container | | `.launcher .pagination` | Pagination controls box |
| `.popup-launcher .popup-item` | Window button in popup | | `.launcher .pagination .btn-back` | Pagination back button |
| `.launcher .pagination .btn-forward` | Pagination forward button |
| `.popup-launcher` | Popup container |
| `.popup-launcher .popup-item` | Window button in popup |
For more information on styling, please see the [styling guide](styling-guide). For more information on styling, please see the [styling guide](styling-guide).

View file

@ -1,5 +1,6 @@
mod item; mod item;
mod open_state; mod open_state;
mod pagination;
use self::item::{AppearanceOptions, Item, ItemButton, Window}; use self::item::{AppearanceOptions, Item, ItemButton, Window};
use self::open_state::OpenState; use self::open_state::OpenState;
@ -9,12 +10,14 @@ use crate::config::{CommonConfig, EllipsizeMode, TruncateMode};
use crate::desktop_file::find_desktop_file; use crate::desktop_file::find_desktop_file;
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt}; use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
use crate::modules::launcher::item::ImageTextButton; use crate::modules::launcher::item::ImageTextButton;
use crate::modules::launcher::pagination::{IconContext, Pagination};
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock}; use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
use color_eyre::{Help, Report}; use color_eyre::{Help, Report};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Button, Orientation}; use gtk::{Button, Orientation};
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::Deserialize; use serde::Deserialize;
use std::ops::Deref;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
@ -62,6 +65,31 @@ pub struct LauncherModule {
#[serde(default = "crate::config::default_true")] #[serde(default = "crate::config::default_true")]
minimize_focused: bool, minimize_focused: bool,
/// The number of items to show on a page.
///
/// When the number of items reaches the page size,
/// pagination controls appear at the start of the widget
/// which can be used to move forward/back through the list of items.
///
/// If there are too many to fit, the overflow will be truncated
/// by the next widget.
///
/// **Default**: `1000`.
#[serde(default = "default_page_size")]
page_size: usize,
/// Module UI icons (separate from app icons shown for items).
///
/// See [icons](#icons).
#[serde(default)]
icons: Icons,
/// Size in pixels to render pagination icons at (image icons only).
///
/// **Default**: `16`
#[serde(default = "default_icon_size_pagination")]
pagination_icon_size: i32,
// -- common -- // -- common --
/// Truncate application names on the bar if they get too long. /// Truncate application names on the bar if they get too long.
/// See [truncate options](module-level-options#truncate-mode). /// See [truncate options](module-level-options#truncate-mode).
@ -82,10 +110,51 @@ pub struct LauncherModule {
pub common: Option<CommonConfig>, pub common: Option<CommonConfig>,
} }
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct Icons {
/// Icon to show for page back button.
///
/// **Default**: `󰅁`
#[serde(default = "default_icon_page_back")]
page_back: String,
/// Icon to show for page back button.
///
/// **Default**: `>`
#[serde(default = "default_icon_page_forward")]
page_forward: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
page_back: default_icon_page_back(),
page_forward: default_icon_page_forward(),
}
}
}
const fn default_icon_size() -> i32 { const fn default_icon_size() -> i32 {
32 32
} }
const fn default_icon_size_pagination() -> i32 {
default_icon_size() / 2
}
const fn default_page_size() -> usize {
1000
}
fn default_icon_page_back() -> String {
String::from("󰅁")
}
fn default_icon_page_forward() -> String {
String::from("󰅂")
}
const fn default_truncate_popup() -> TruncateMode { const fn default_truncate_popup() -> TruncateMode {
TruncateMode::Length { TruncateMode::Length {
mode: EllipsizeMode::Middle, mode: EllipsizeMode::Middle,
@ -386,6 +455,19 @@ impl Module<gtk::Box> for LauncherModule {
let icon_theme = info.icon_theme; let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.orientation(), 0); let container = gtk::Box::new(info.bar_position.orientation(), 0);
let page_size = self.page_size;
let pagination = Pagination::new(
&container,
self.page_size,
info.bar_position.orientation(),
IconContext {
icon_back: &self.icons.page_back,
icon_fwd: &self.icons.page_forward,
icon_size: self.pagination_icon_size,
icon_theme: info.icon_theme,
},
);
{ {
let container = container.clone(); let container = container.clone();
@ -407,7 +489,15 @@ impl Module<gtk::Box> for LauncherModule {
let tx = context.tx.clone(); let tx = context.tx.clone();
let rx = context.subscribe(); let rx = context.subscribe();
glib_recv!(rx, event => {
let mut handle_event = move |event: LauncherUpdate| {
// all widgets show by default
// so check if pagination should be shown
// to ensure correct state on init.
if buttons.len() <= page_size {
pagination.hide();
}
match event { match event {
LauncherUpdate::AddItem(item) => { LauncherUpdate::AddItem(item) => {
debug!("Adding item with id '{}' to the bar: {item:?}", item.app_id); debug!("Adding item with id '{}' to the bar: {item:?}", item.app_id);
@ -426,9 +516,18 @@ impl Module<gtk::Box> for LauncherModule {
); );
if self.reversed { if self.reversed {
container.pack_end(&button.button.button, false, false, 0); container.pack_end(button.button.deref(), false, false, 0);
} else { } else {
container.add(&button.button.button); container.add(button.button.deref());
}
if buttons.len() + 1 >= pagination.offset() + page_size {
button.button.set_visible(false);
pagination.set_sensitive_fwd(true);
}
if buttons.len() + 1 > page_size {
pagination.show_all();
} }
buttons.insert(item.app_id, button); buttons.insert(item.app_id, button);
@ -456,6 +555,14 @@ impl Module<gtk::Box> for LauncherModule {
buttons.shift_remove(&app_id); buttons.shift_remove(&app_id);
} }
} }
if buttons.len() < pagination.offset() + page_size {
pagination.set_sensitive_fwd(false);
}
if buttons.len() <= page_size {
pagination.hide();
}
} }
LauncherUpdate::RemoveWindow(app_id, win_id) => { LauncherUpdate::RemoveWindow(app_id, win_id) => {
debug!("Removing window {win_id} with id {app_id}"); debug!("Removing window {win_id} with id {app_id}");
@ -485,7 +592,9 @@ impl Module<gtk::Box> for LauncherModule {
} }
LauncherUpdate::Hover(_) => {} LauncherUpdate::Hover(_) => {}
}; };
}); };
glib_recv!(rx, handle_event);
} }
let rx = context.subscribe(); let rx = context.subscribe();

View file

@ -0,0 +1,140 @@
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::new_icon_button;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Orientation};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
pub struct Pagination {
offset: Rc<RefCell<usize>>,
controls_container: gtk::Box,
btn_fwd: Button,
}
pub struct IconContext<'a> {
pub icon_back: &'a str,
pub icon_fwd: &'a str,
pub icon_size: i32,
pub icon_theme: &'a IconTheme,
}
impl Pagination {
pub fn new(
container: &gtk::Box,
page_size: usize,
orientation: Orientation,
icon_context: IconContext,
) -> Self {
let scroll_box = gtk::Box::new(orientation, 0);
let scroll_back = new_icon_button(
icon_context.icon_back,
icon_context.icon_theme,
icon_context.icon_size,
);
let scroll_fwd = new_icon_button(
icon_context.icon_fwd,
icon_context.icon_theme,
icon_context.icon_size,
);
scroll_back.set_sensitive(false);
scroll_fwd.set_sensitive(false);
scroll_box.add_class("pagination");
scroll_back.add_class("btn-back");
scroll_fwd.add_class("btn-forward");
scroll_box.add(&scroll_back);
scroll_box.add(&scroll_fwd);
container.add(&scroll_box);
let offset = Rc::new(RefCell::new(1));
{
let offset = offset.clone();
let container = container.clone();
let scroll_back = scroll_back.clone();
scroll_fwd.connect_clicked(move |btn| {
let mut offset = offset.borrow_mut();
let child_count = container.children().len();
*offset = std::cmp::min(child_count - 1, *offset + page_size);
Self::update_page(&container, *offset, page_size);
if *offset + page_size >= child_count {
btn.set_sensitive(false);
}
scroll_back.set_sensitive(true);
});
}
{
let offset = offset.clone();
let container = container.clone();
let scroll_fwd = scroll_fwd.clone();
scroll_back.connect_clicked(move |btn| {
let mut offset = offset.borrow_mut();
// avoid using std::cmp::max due to possible overflow
if page_size < *offset {
*offset -= page_size;
} else {
*offset = 1
};
Self::update_page(&container, *offset, page_size);
if *offset == 1 || *offset - page_size < 1 {
btn.set_sensitive(false);
}
scroll_fwd.set_sensitive(true);
});
}
Self {
offset,
controls_container: scroll_box,
btn_fwd: scroll_fwd,
}
}
fn update_page(container: &gtk::Box, offset: usize, page_size: usize) {
for (i, btn) in container.children().iter().enumerate() {
// skip offset buttons
if i == 0 {
continue;
}
if i >= offset && i < offset + page_size {
btn.show();
} else {
btn.hide();
}
}
}
pub fn set_sensitive_fwd(&self, sensitive: bool) {
self.btn_fwd.set_sensitive(sensitive);
}
pub fn offset(&self) -> usize {
*self.offset.borrow()
}
}
impl Deref for Pagination {
type Target = gtk::Box;
fn deref(&self) -> &Self::Target {
&self.controls_container
}
}