1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 23:01:04 +02:00
ironbar/src/modules/launcher/item.rs
Jake Stanger 720ba7bfb0
Major module refactor (#19)
* refactor: major module restructuring

Modules now implement a "controller", which allows for separation of logic from UI code and enforces a tighter structure around how modules should be written. The introduction of this change required major refactoring or even rewriting of all modules.

This also better integrates the popup into modules, making it easier for data to be passed around without fetching the same thing twice

The refactor also improves some client code, switching from `ksway` to the much more stable `swayipc-async`. Partial multi-monitor for the tray module has been added.

BREAKING CHANGE: The `mpd` module config has changed, moving the icons to their own object.
2022-09-25 22:49:00 +01:00

299 lines
8.1 KiB
Rust

use super::open_state::OpenState;
use crate::collection::Collection;
use crate::icon::get_icon;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::sway::node::{get_node_id, is_node_xwayland};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image};
use std::rc::Rc;
use std::sync::RwLock;
use swayipc_async::Node;
use tokio::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct Item {
pub app_id: String,
pub favorite: bool,
pub open_state: OpenState,
pub windows: Collection<i64, Window>,
pub name: Option<String>,
pub is_xwayland: bool,
}
impl Item {
pub const fn new(app_id: String, open_state: OpenState, favorite: bool) -> Self {
Self {
app_id,
favorite,
open_state,
windows: Collection::new(),
name: None,
is_xwayland: false,
}
}
/// Merges the provided node into this launcher item
pub fn merge_node(&mut self, node: Node) -> Window {
let id = node.id;
if self.windows.is_empty() {
self.name = node.name.clone();
}
self.is_xwayland = self.is_xwayland || is_node_xwayland(&node);
let window: Window = node.into();
self.windows.insert(id, window.clone());
self.recalculate_open_state();
window
}
pub fn unmerge_node(&mut self, node: &Node) {
self.windows.remove(&node.id);
self.recalculate_open_state();
}
pub fn get_name(&self) -> &str {
self.name.as_ref().unwrap_or(&self.app_id)
}
pub fn set_window_name(&mut self, window_id: i64, name: Option<String>) {
if let Some(window) = self.windows.get_mut(&window_id) {
if let OpenState::Open { focused: true, .. } = window.open_state {
self.name = name.clone();
}
window.name = name;
}
}
pub fn set_unfocused(&mut self) {
let focused = self
.windows
.iter_mut()
.find(|window| window.open_state.is_focused());
if let Some(focused) = focused {
focused.open_state = OpenState::Open {
focused: false,
urgent: focused.open_state.is_urgent(),
};
self.recalculate_open_state();
}
}
pub fn set_window_focused(&mut self, window_id: i64, focused: bool) {
if let Some(window) = self.windows.get_mut(&window_id) {
window.open_state =
OpenState::merge_states(&[&window.open_state, &OpenState::focused(focused)]);
self.recalculate_open_state();
}
}
pub fn set_window_urgent(&mut self, window_id: i64, urgent: bool) {
if let Some(window) = self.windows.get_mut(&window_id) {
window.open_state =
OpenState::merge_states(&[&window.open_state, &OpenState::urgent(urgent)]);
self.recalculate_open_state();
}
}
/// Sets this item's open state
/// to the merged result of its windows' open states
fn recalculate_open_state(&mut self) {
let new_state = OpenState::merge_states(
&self
.windows
.iter()
.map(|win| &win.open_state)
.collect::<Vec<_>>(),
);
self.open_state = new_state;
}
}
impl From<Node> for Item {
fn from(node: Node) -> Self {
let app_id = get_node_id(&node).to_string();
let open_state = OpenState::from_node(&node);
let name = node.name.clone();
let is_xwayland = is_node_xwayland(&node);
let mut windows = Collection::new();
windows.insert(node.id, node.into());
Self {
app_id,
favorite: false,
open_state,
windows,
name,
is_xwayland,
}
}
}
#[derive(Clone, Debug)]
pub struct Window {
pub id: i64,
pub name: Option<String>,
pub open_state: OpenState,
}
impl From<Node> for Window {
fn from(node: Node) -> Self {
let open_state = OpenState::from_node(&node);
Self {
id: node.id,
name: node.name,
open_state,
}
}
}
pub struct MenuState {
pub num_windows: usize,
}
pub struct ItemButton {
pub button: Button,
pub persistent: bool,
pub show_names: bool,
pub menu_state: Rc<RwLock<MenuState>>,
}
impl ItemButton {
pub fn new(
item: &Item,
show_names: bool,
show_icons: bool,
icon_theme: &IconTheme,
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
controller_tx: &Sender<ItemEvent>,
) -> Self {
let mut button = Button::builder();
if show_names {
button = button.label(item.get_name());
}
if show_icons {
let icon = get_icon(icon_theme, &item.app_id, 32);
if icon.is_some() {
let image = Image::from_pixbuf(icon.as_ref());
button = button.image(&image).always_show_image(true);
}
}
let button = button.build();
let style_context = button.style_context();
style_context.add_class("item");
if item.favorite {
style_context.add_class("favorite");
}
if item.open_state.is_open() {
style_context.add_class("open");
}
if item.open_state.is_focused() {
style_context.add_class("focused");
}
if item.open_state.is_urgent() {
style_context.add_class("urgent");
}
{
let app_id = item.app_id.clone();
let tx = controller_tx.clone();
button.connect_clicked(move |button| {
// lazy check :|
let style_context = button.style_context();
if style_context.has_class("open") {
tx.try_send(ItemEvent::FocusItem(app_id.clone()))
.expect("Failed to send item focus event");
} else {
tx.try_send(ItemEvent::OpenItem(app_id.clone()))
.expect("Failed to send item open event");
}
});
}
let menu_state = Rc::new(RwLock::new(MenuState {
num_windows: item.windows.len(),
}));
{
let app_id = item.app_id.clone();
let tx = tx.clone();
let menu_state = menu_state.clone();
button.connect_enter_notify_event(move |button, _| {
let menu_state = menu_state
.read()
.expect("Failed to get read lock on item menu state");
if menu_state.num_windows > 1 {
tx.try_send(ModuleUpdateEvent::Update(LauncherUpdate::Hover(
app_id.clone(),
)))
.expect("Failed to send item open popup event");
tx.try_send(ModuleUpdateEvent::OpenPopup(Popup::button_pos(button)))
.expect("Failed to send item open popup event");
} else {
tx.try_send(ModuleUpdateEvent::ClosePopup)
.expect("Failed to send item close popup event");
}
Inhibit(false)
});
}
button.show_all();
Self {
button,
persistent: item.favorite,
show_names,
menu_state,
}
}
pub fn set_open(&self, open: bool) {
self.update_class("open", open);
if !open {
self.set_focused(false);
self.set_urgent(false);
}
}
pub fn set_focused(&self, focused: bool) {
self.update_class("focused", focused);
}
pub fn set_urgent(&self, urgent: bool) {
self.update_class("urgent", urgent);
}
/// Adds or removes a class to the button based on `toggle`.
fn update_class(&self, class: &str, toggle: bool) {
let style_context = self.button.style_context();
if toggle {
style_context.add_class(class);
} else {
style_context.remove_class(class);
}
}
}