Merge pull request #237 from JakeStanger/feat/ipc-popup

feat(ipc): commands for opening/closing popups
This commit is contained in:
Jake Stanger 2023-07-16 20:06:13 +01:00 committed by GitHub
commit 36abe4073e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 747 additions and 345 deletions

View File

@ -138,6 +138,49 @@ Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists,
}
```
### `toggle_popup`
Toggles the open/closed state for a module's popup.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "toggle_popup",
"bar_name": "DP-2-13",
"name": "clock"
}
```
### `open_popup`
Sets a module's popup open, regardless of its current state.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "open_popup",
"bar_name": "DP-2-13",
"name": "clock"
}
```
### `close_popup`
Sets the popup on a bar closed, regardless of which module it is open for.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "toggle_popup",
"bar_name": "DP-2-13"
}
```
## Responses
### `ok`

View File

@ -4,11 +4,13 @@ use crate::modules::{
};
use crate::popup::Popup;
use crate::unique_id::get_unique_usize;
use crate::{arc_rw, Config};
use crate::{arc_rw, Config, GlobalState};
use color_eyre::Result;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, RwLock};
use tracing::{debug, info};
@ -19,6 +21,7 @@ pub fn create_bar(
monitor: &Monitor,
monitor_name: &str,
config: Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> {
let win = ApplicationWindow::builder().application(app).build();
let bar_name = config
@ -62,7 +65,12 @@ pub fn create_bar(
content.set_center_widget(Some(&center));
content.pack_end(&end, false, false, 0);
load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
let load_result = load_modules(&start, &center, &end, app, config, monitor, monitor_name)?;
global_state
.borrow_mut()
.popups_mut()
.insert(bar_name.into(), load_result.popup);
win.add(&content);
win.connect_destroy_event(|_, _| {
@ -143,6 +151,11 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
container
}
#[derive(Debug)]
struct BarLoadResult {
popup: Arc<RwLock<Popup>>,
}
/// Loads the configured modules onto a bar.
fn load_modules(
left: &gtk::Box,
@ -152,7 +165,7 @@ fn load_modules(
config: Config,
monitor: &Monitor,
output_name: &str,
) -> Result<()> {
) -> Result<BarLoadResult> {
let icon_theme = IconTheme::new();
if let Some(ref theme) = config.icon_theme {
icon_theme.set_custom_theme(Some(theme));
@ -190,7 +203,9 @@ fn load_modules(
add_modules(right, modules, &info, &popup)?;
}
Ok(())
let result = BarLoadResult { popup };
Ok(result)
}
/// Adds modules into a provided GTK box,
@ -205,8 +220,14 @@ fn add_modules(
macro_rules! add_module {
($module:expr, $id:expr) => {{
let common = $module.common.take().expect("Common config did not exist");
let widget_parts = create_module(*$module, $id, &info, &Arc::clone(&popup))?;
let common = $module.common.take().expect("common config to exist");
let widget_parts = create_module(
*$module,
$id,
common.name.clone(),
&info,
&Arc::clone(&popup),
)?;
set_widget_identifiers(&widget_parts, &common);
let container = wrap_widget(&widget_parts.widget, common, orientation);

43
src/global_state.rs Normal file
View File

@ -0,0 +1,43 @@
use crate::popup::Popup;
use crate::write_lock;
use std::collections::HashMap;
use std::sync::{Arc, RwLock, RwLockWriteGuard};
/// Global application state shared across all bars.
///
/// Data that needs to be accessed from anywhere
/// that is not otherwise accessible should be placed on here.
#[derive(Debug)]
pub struct GlobalState {
popups: HashMap<Box<str>, Arc<RwLock<Popup>>>,
}
impl GlobalState {
pub(crate) fn new() -> Self {
Self {
popups: HashMap::new(),
}
}
pub fn popups(&self) -> &HashMap<Box<str>, Arc<RwLock<Popup>>> {
&self.popups
}
pub fn popups_mut(&mut self) -> &mut HashMap<Box<str>, Arc<RwLock<Popup>>> {
&mut self.popups
}
pub fn with_popup_mut<F, T>(&self, monitor_name: &str, f: F) -> Option<T>
where
F: FnOnce(RwLockWriteGuard<Popup>) -> T,
{
let popup = self.popups().get(monitor_name);
if let Some(popup) = popup {
let popup = write_lock!(popup);
Some(f(popup))
} else {
None
}
}
}

View File

@ -1,8 +1,77 @@
use glib::IsA;
use gtk::prelude::*;
use gtk::Widget;
use gtk::{Orientation, Widget};
/// Adds a new CSS class to a widget.
pub fn add_class<W: IsA<Widget>>(widget: &W, class: &str) {
widget.style_context().add_class(class);
/// Represents a widget's size
/// and location relative to the bar's start edge.
#[derive(Debug, Copy, Clone)]
pub struct WidgetGeometry {
/// Position of the start edge of the widget
/// from the start edge of the bar.
pub position: i32,
/// The length of the widget.
pub size: i32,
/// The length of the bar.
pub bar_size: i32,
}
pub trait IronbarGtkExt {
/// Adds a new CSS class to the widget.
fn add_class(&self, class: &str);
/// Gets the geometry for the widget
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
/// Gets a data tag on a widget, if it exists.
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V>;
/// Sets a data tag on a widget.
fn set_tag<V: 'static>(&self, key: &str, value: V);
}
impl<W: IsA<Widget>> IronbarGtkExt for W {
fn add_class(&self, class: &str) {
self.style_context().add_class(class);
}
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
let allocation = self.allocation();
let widget_size = if orientation == Orientation::Horizontal {
allocation.width()
} else {
allocation.height()
};
let top_level = self.toplevel().expect("Failed to get top-level widget");
let top_level_allocation = top_level.allocation();
let bar_size = if orientation == Orientation::Horizontal {
top_level_allocation.width()
} else {
top_level_allocation.height()
};
let (widget_x, widget_y) = self
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_pos = if orientation == Orientation::Horizontal {
widget_x
} else {
widget_y
};
WidgetGeometry {
position: widget_pos,
size: widget_size,
bar_size,
}
}
fn get_tag<V: 'static>(&self, key: &str) -> Option<&V> {
unsafe { self.data(key).map(|val| val.as_ref()) }
}
fn set_tag<V: 'static>(&self, key: &str, value: V) {
unsafe { self.set_data(key, value) }
}
}

View File

@ -1,5 +1,5 @@
use super::ImageProvider;
use crate::gtk_helpers::add_class;
use crate::gtk_helpers::IronbarGtkExt;
use gtk::prelude::*;
use gtk::{Button, IconTheme, Image, Label, Orientation};
@ -9,8 +9,8 @@ pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
add_class(&image, "image");
add_class(&image, "icon");
image.add_class("image");
image.add_class("icon");
match ImageProvider::parse(input, icon_theme, size)
.map(|provider| provider.load_into_image(image.clone()))
@ -36,8 +36,8 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
if ImageProvider::is_definitely_image_input(input) {
let image = Image::new();
add_class(&image, "icon");
add_class(&image, "image");
image.add_class("icon");
image.add_class("image");
container.add(&image);
@ -45,8 +45,8 @@ pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Bo
.map(|provider| provider.load_into_image(image));
} else {
let label = Label::new(Some(input));
add_class(&label, "icon");
add_class(&label, "text-icon");
label.add_class("icon");
label.add_class("text-icon");
container.add(&label);
}

View File

@ -1,6 +1,7 @@
use std::path::PathBuf;
use clap::Subcommand;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
@ -52,4 +53,27 @@ pub enum Command {
/// Bar name to target.
bar_name: String,
},
/// Toggle a popup open/closed.
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
TogglePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Open a popup, regardless of current state.
OpenPopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Close a popup, regardless of current state.
ClosePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
},
}

View File

@ -3,21 +3,25 @@ pub mod commands;
pub mod responses;
mod server;
use std::path::PathBuf;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use tracing::warn;
use crate::GlobalState;
pub use commands::Command;
pub use responses::Response;
#[derive(Debug)]
pub struct Ipc {
path: PathBuf,
global_state: Rc<RefCell<GlobalState>>,
}
impl Ipc {
/// Creates a new IPC instance.
/// This can be used as both a server and client.
pub fn new() -> Self {
pub fn new(global_state: Rc<RefCell<GlobalState>>) -> Self {
let ipc_socket_file = std::env::var("XDG_RUNTIME_DIR")
.map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from)
.join("ironbar-ipc.sock");
@ -28,6 +32,11 @@ impl Ipc {
Self {
path: ipc_socket_file,
global_state,
}
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
}

View File

@ -1,20 +1,27 @@
use super::Ipc;
use crate::bridge_channel::BridgeChannel;
use crate::ipc::{Command, Response};
use crate::ironvar::get_variable_manager;
use crate::style::load_css;
use crate::{read_lock, send_async, try_send, write_lock};
use std::cell::RefCell;
use std::fs;
use std::path::Path;
use std::rc::Rc;
use color_eyre::{Report, Result};
use glib::Continue;
use gtk::prelude::*;
use gtk::Application;
use std::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::spawn;
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{debug, error, info, warn};
use crate::bridge_channel::BridgeChannel;
use crate::ipc::{Command, Response};
use crate::ironvar::get_variable_manager;
use crate::modules::PopupButton;
use crate::style::load_css;
use crate::{read_lock, send_async, try_send, write_lock, GlobalState};
use super::Ipc;
impl Ipc {
/// Starts the IPC server on its socket.
///
@ -29,7 +36,7 @@ impl Ipc {
if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address");
self.shutdown();
Self::shutdown(&path);
}
spawn(async move {
@ -63,8 +70,9 @@ impl Ipc {
});
let application = application.clone();
let global_state = self.global_state.clone();
bridge.recv(move |command| {
let res = Self::handle_command(command, &application);
let res = Self::handle_command(command, &application, &global_state);
try_send!(res_tx, res);
Continue(true)
});
@ -104,7 +112,11 @@ impl Ipc {
/// Takes an input command, runs it and returns with the appropriate response.
///
/// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(command: Command, application: &Application) -> Response {
fn handle_command(
command: Command,
application: &Application,
global_state: &Rc<RefCell<GlobalState>>,
) -> Response {
match command {
Command::Inspect => {
gtk::Window::set_interactive_debugging(true);
@ -117,7 +129,7 @@ impl Ipc {
window.close();
}
crate::load_interface(application);
crate::load_interface(application, global_state);
Response::Ok
}
@ -145,6 +157,71 @@ impl Ipc {
Response::error("File not found")
}
}
Command::TogglePopup { bar_name, name } => {
let global_state = global_state.borrow();
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
let current_widget = popup.current_widget();
popup.hide();
let data = popup
.cache
.iter()
.find(|(_, (module_name, _))| module_name == &name)
.map(|module| (module, module.1 .1.buttons.first()));
match data {
Some(((&id, _), Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
Some(_) => Response::Ok,
None => Response::error("Invalid module name"),
}
});
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
}
Command::OpenPopup { bar_name, name } => {
let global_state = global_state.borrow();
let response = global_state.with_popup_mut(&bar_name, |mut popup| {
// only one popup per bar, so hide if open for another widget
popup.hide();
let data = popup
.cache
.iter()
.find(|(_, (module_name, _))| module_name == &name)
.map(|module| (module, module.1 .1.buttons.first()));
match data {
Some(((&id, _), Some(button))) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
None => Response::error("Invalid module name"),
}
});
response.unwrap_or_else(|| Response::error("Invalid monitor name"))
}
Command::ClosePopup { bar_name } => {
let global_state = global_state.borrow();
let popup_found = global_state
.with_popup_mut(&bar_name, |mut popup| popup.hide())
.is_some();
if popup_found {
Response::Ok
} else {
Response::error("Invalid monitor name")
}
}
Command::Ping => Response::Ok,
Command::SetVisible { bar_name, visible } => {
let windows = application.windows();
@ -178,7 +255,9 @@ impl Ipc {
/// Shuts down the IPC server,
/// removing the socket file in the process.
pub fn shutdown(&self) {
fs::remove_file(&self.path).ok();
///
/// Note this is static as the `Ipc` struct is not `Send`.
pub fn shutdown<P: AsRef<Path>>(path: P) {
fs::remove_file(&path).ok();
}
}

View File

@ -1,5 +1,35 @@
#![doc = include_str!("../README.md")]
use std::cell::{Cell, RefCell};
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use tokio::runtime::Handle;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader;
use clients::wayland;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::error::ExitCode;
use crate::global_state::GlobalState;
use crate::style::load_css;
mod bar;
mod bridge_channel;
#[cfg(feature = "cli")]
@ -9,6 +39,7 @@ mod config;
mod desktop_file;
mod dynamic_value;
mod error;
mod global_state;
mod gtk_helpers;
mod image;
#[cfg(feature = "ipc")]
@ -23,33 +54,6 @@ mod script;
mod style;
mod unique_id;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
use crate::style::load_css;
use cfg_if::cfg_if;
#[cfg(feature = "cli")]
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::Report;
use dirs::config_dir;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::Application;
use std::cell::Cell;
use std::env;
use std::future::Future;
use std::path::PathBuf;
use std::process::exit;
use std::rc::Rc;
use std::sync::mpsc;
use tokio::runtime::Handle;
use tokio::task::{block_in_place, spawn_blocking};
use crate::error::ExitCode;
use clients::wayland;
use tracing::{debug, error, info, warn};
use universal_config::ConfigLoader;
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION");
@ -57,32 +61,34 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
async fn main() {
let _guard = logging::install_logging();
let global_state = Rc::new(RefCell::new(GlobalState::new()));
cfg_if! {
if #[cfg(feature = "cli")] {
run_with_args().await;
run_with_args(global_state).await;
} else {
start_ironbar();
start_ironbar(global_state);
}
}
}
#[cfg(feature = "cli")]
async fn run_with_args() {
async fn run_with_args(global_state: Rc<RefCell<GlobalState>>) {
let args = cli::Args::parse();
match args.command {
Some(command) => {
let ipc = ipc::Ipc::new();
let ipc = ipc::Ipc::new(global_state);
match ipc.send(command).await {
Ok(res) => cli::handle_response(res),
Err(err) => error!("{err:?}"),
};
}
None => start_ironbar(),
None => start_ironbar(global_state),
}
}
fn start_ironbar() {
fn start_ironbar(global_state: Rc<RefCell<GlobalState>>) {
info!("Ironbar version {}", VERSION);
info!("Starting application");
@ -101,12 +107,12 @@ fn start_ironbar() {
cfg_if! {
if #[cfg(feature = "ipc")] {
let ipc = ipc::Ipc::new();
let ipc = ipc::Ipc::new(global_state.clone());
ipc.start(app);
}
}
load_interface(app);
load_interface(app, &global_state);
let style_path = env::var("IRONBAR_CSS").ok().map_or_else(
|| {
@ -128,13 +134,15 @@ fn start_ironbar() {
let (tx, rx) = mpsc::channel();
#[cfg(feature = "ipc")]
let ipc_path = ipc.path().to_path_buf();
spawn_blocking(move || {
rx.recv().expect("to receive from channel");
info!("Shutting down");
#[cfg(feature = "ipc")]
ipc.shutdown();
ipc::Ipc::shutdown(ipc_path);
exit(0);
});
@ -149,7 +157,7 @@ fn start_ironbar() {
}
/// Loads the Ironbar config and interface.
pub fn load_interface(app: &Application) {
pub fn load_interface(app: &Application, global_state: &Rc<RefCell<GlobalState>>) {
let display = Display::default().map_or_else(
|| {
let report = Report::msg("Failed to get default GTK display");
@ -180,12 +188,12 @@ pub fn load_interface(app: &Application) {
let variable_manager = ironvar::get_variable_manager();
for (k, v) in ironvars {
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
tracing::warn!("Ignoring invalid ironvar: '{k}'");
warn!("Ignoring invalid ironvar: '{k}'");
}
}
}
if let Err(err) = create_bars(app, &display, &config) {
if let Err(err) = create_bars(app, &display, &config, global_state) {
error!("{:?}", err);
exit(ExitCode::CreateBars as i32);
}
@ -194,7 +202,12 @@ pub fn load_interface(app: &Application) {
}
/// Creates each of the bars across each of the (configured) outputs.
fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<()> {
fn create_bars(
app: &Application,
display: &Display,
config: &Config,
global_state: &Rc<RefCell<GlobalState>>,
) -> Result<()> {
let wl = wayland::get_client();
let outputs = lock!(wl).get_outputs();
@ -216,19 +229,19 @@ fn create_bars(app: &Application, display: &Display, config: &Config) -> Result<
config.monitors.as_ref().map_or_else(
|| {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())
create_bar(app, &monitor, monitor_name, config.clone(), global_state)
},
|config| {
let config = config.get(monitor_name);
match &config {
Some(MonitorConfig::Single(config)) => {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())
create_bar(app, &monitor, monitor_name, config.clone(), global_state)
}
Some(MonitorConfig::Multiple(configs)) => {
for config in configs {
info!("Creating bar on '{}'", monitor_name);
create_bar(app, &monitor, monitor_name, config.clone())?;
create_bar(app, &monitor, monitor_name, config.clone(), global_state)?;
}
Ok(())

View File

@ -2,8 +2,9 @@ use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
@ -124,25 +125,26 @@ impl Module<Button> for ClipboardModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
) -> color_eyre::Result<ModuleParts<Button>> {
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
button.style_context().add_class("btn");
button.connect_clicked(move |button| {
let pos = Popup::widget_geometry(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
// we need to bind to the receiver as the channel does not open
// until the popup is first opened.
context.widget_rx.attach(None, |_| Continue(true));
Ok(ModuleWidget {
widget: button,
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
}
fn into_popup(

View File

@ -1,19 +1,22 @@
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use std::env;
use chrono::{DateTime, Local, Locale};
use color_eyre::Result;
use glib::Continue;
use gtk::prelude::*;
use gtk::{Align, Button, Calendar, Label, Orientation};
use serde::Deserialize;
use std::env;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{send_async, try_send};
#[derive(Debug, Deserialize, Clone)]
pub struct ClockModule {
/// Date/time format string.
@ -96,17 +99,16 @@ impl Module<Button> for ClockModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let label = Label::new(None);
label.set_angle(info.bar_position.get_angle());
button.add(&label);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
@ -119,12 +121,11 @@ impl Module<Button> for ClockModule {
Continue(true)
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleWidget {
widget: button,
popup,
})
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@ -136,12 +137,12 @@ impl Module<Button> for ClockModule {
let container = gtk::Box::new(Orientation::Vertical, 0);
let clock = Label::builder().halign(Align::Center).build();
add_class(&clock, "calendar-clock");
clock.add_class("calendar-clock");
container.add(&clock);
let calendar = Calendar::new();
add_class(&calendar, "calendar");
calendar.add_class("calendar");
container.add(&calendar);
let format = self.format_popup;

View File

@ -27,7 +27,7 @@ impl CustomWidget for BoxWidget {
if let Some(widgets) = self.widgets {
for widget in widgets {
widget.widget.add_to(&container, context, widget.common);
widget.widget.add_to(&container, &context, widget.common);
}
}

View File

@ -1,11 +1,13 @@
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
use crate::dynamic_value::dynamic_string;
use crate::popup::Popup;
use crate::{build, try_send};
use gtk::prelude::*;
use gtk::{Button, Label};
use serde::Deserialize;
use crate::dynamic_value::dynamic_string;
use crate::modules::PopupButton;
use crate::{build, try_send};
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct ButtonWidget {
name: Option<String>,
@ -19,6 +21,7 @@ impl CustomWidget for ButtonWidget {
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
let button = build!(self, Self::Widget);
context.popup_buttons.borrow_mut().push(button.clone());
if let Some(text) = self.label {
let label = Label::new(None);
@ -32,7 +35,6 @@ impl CustomWidget for ButtonWidget {
}
if let Some(exec) = self.on_click {
let bar_orientation = context.bar_orientation;
let tx = context.tx.clone();
button.connect_clicked(move |button| {
@ -41,7 +43,7 @@ impl CustomWidget for ButtonWidget {
ExecEvent {
cmd: exec.clone(),
args: None,
geometry: Popup::widget_geometry(button, bar_orientation),
id: button.popup_id(),
}
);
});

View File

@ -1,11 +1,13 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_value::dynamic_string;
use crate::image::ImageProvider;
use gtk::prelude::*;
use gtk::Image;
use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use crate::image::ImageProvider;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ImageWidget {
name: Option<String>,

View File

@ -1,10 +1,12 @@
use super::{CustomWidget, CustomWidgetContext};
use crate::build;
use crate::dynamic_value::dynamic_string;
use gtk::prelude::*;
use gtk::Label;
use serde::Deserialize;
use crate::build;
use crate::dynamic_value::dynamic_string;
use super::{CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct LabelWidget {
name: Option<String>,

View File

@ -13,15 +13,16 @@ use crate::config::CommonConfig;
use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{
wrap_widget, Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext,
wrap_widget, Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::popup::WidgetGeometry;
use crate::script::Script;
use crate::send_async;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use gtk::{Button, IconTheme, Orientation};
use serde::Deserialize;
use std::cell::RefCell;
use std::rc::Rc;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
@ -56,11 +57,12 @@ pub enum Widget {
Progress(ProgressWidget),
}
#[derive(Clone, Copy)]
#[derive(Clone)]
struct CustomWidgetContext<'a> {
tx: &'a Sender<ExecEvent>,
bar_orientation: Orientation,
icon_theme: &'a IconTheme,
popup_buttons: Rc<RefCell<Vec<Button>>>,
}
trait CustomWidget {
@ -115,11 +117,11 @@ fn try_get_orientation(orientation: &str) -> Result<Orientation> {
impl Widget {
/// Creates this widget and adds it to the parent container
fn add_to(self, parent: &gtk::Box, context: CustomWidgetContext, common: CommonConfig) {
fn add_to(self, parent: &gtk::Box, context: &CustomWidgetContext, common: CommonConfig) {
macro_rules! create {
($widget:expr) => {
wrap_widget(
&$widget.into_widget(context),
&$widget.into_widget(context.clone()),
common,
context.bar_orientation,
)
@ -143,7 +145,7 @@ impl Widget {
pub struct ExecEvent {
cmd: String,
args: Option<Vec<String>>,
geometry: WidgetGeometry,
id: usize,
}
impl Module<gtk::Box> for CustomModule {
@ -173,9 +175,9 @@ impl Module<gtk::Box> for CustomModule {
error!("{err:?}");
}
} else if event.cmd == "popup:toggle" {
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry));
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
} else if event.cmd == "popup:open" {
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry));
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
} else if event.cmd == "popup:close" {
send_async!(tx, ModuleUpdateEvent::ClosePopup);
} else {
@ -191,25 +193,30 @@ impl Module<gtk::Box> for CustomModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.get_orientation();
let container = gtk::Box::builder().orientation(orientation).build();
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
let custom_context = CustomWidgetContext {
tx: &context.controller_tx,
bar_orientation: orientation,
icon_theme: info.icon_theme,
popup_buttons: popup_buttons.clone(),
};
self.bar.clone().into_iter().for_each(|widget| {
widget
.widget
.add_to(&container, custom_context, widget.common);
.add_to(&container, &custom_context, widget.common);
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts_owned(popup_buttons.take());
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup,
})
@ -231,12 +238,13 @@ impl Module<gtk::Box> for CustomModule {
tx: &tx,
bar_orientation: info.bar_position.get_orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
};
for widget in popup {
widget
.widget
.add_to(&container, custom_context, widget.common);
.add_to(&container, &custom_context, widget.common);
}
}

View File

@ -1,14 +1,16 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use gtk::prelude::*;
use gtk::ProgressBar;
use serde::Deserialize;
use tokio::spawn;
use tracing::error;
use crate::dynamic_value::dynamic_string;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
#[derive(Debug, Deserialize, Clone)]
pub struct ProgressWidget {
name: Option<String>,

View File

@ -1,16 +1,18 @@
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
use crate::modules::custom::set_length;
use crate::popup::Popup;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use std::cell::Cell;
use std::ops::Neg;
use gtk::prelude::*;
use gtk::Scale;
use serde::Deserialize;
use std::cell::Cell;
use std::ops::Neg;
use tokio::spawn;
use tracing::error;
use crate::modules::custom::set_length;
use crate::script::{OutputStream, Script, ScriptInput};
use crate::{build, send, try_send};
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
#[derive(Debug, Deserialize, Clone)]
pub struct SliderWidget {
name: Option<String>,
@ -78,7 +80,7 @@ impl CustomWidget for SliderWidget {
Inhibit(false)
});
scale.connect_change_value(move |scale, _, val| {
scale.connect_change_value(move |_, _, val| {
// GTK will send values outside min/max range
let val = val.clamp(min, max);
@ -88,7 +90,7 @@ impl CustomWidget for SliderWidget {
ExecEvent {
cmd: on_change.clone(),
args: Some(vec![val.to_string()]),
geometry: Popup::widget_geometry(scale, context.bar_orientation),
id: usize::MAX // ignored
}
);

View File

@ -1,8 +1,8 @@
use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::{CommonConfig, TruncateMode};
use crate::gtk_helpers::add_class;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{lock, send_async, try_send};
use color_eyre::Result;
use glib::Continue;
@ -104,19 +104,19 @@ impl Module<gtk::Box> for FocusedModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let icon = gtk::Image::new();
if self.show_icon {
add_class(&icon, "icon");
icon.add_class("icon");
container.add(&icon);
}
let label = Label::new(None);
add_class(&label, "label");
label.add_class("label");
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
@ -144,7 +144,7 @@ impl Module<gtk::Box> for FocusedModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@ -1,6 +1,6 @@
use crate::config::CommonConfig;
use crate::dynamic_value::dynamic_string;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::try_send;
use color_eyre::Result;
use glib::Continue;
@ -52,7 +52,7 @@ impl Module<Label> for LabelModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleWidget<Label>> {
) -> Result<ModuleParts<Label>> {
let label = Label::new(None);
label.set_use_markup(true);
@ -64,7 +64,7 @@ impl Module<Label> for LabelModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: label,
popup: None,
})

View File

@ -1,9 +1,9 @@
use super::open_state::OpenState;
use crate::clients::wayland::ToplevelHandle;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
use crate::modules::ModuleUpdateEvent;
use crate::popup::Popup;
use crate::{read_lock, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
@ -249,7 +249,7 @@ impl ItemButton {
try_send!(
tx,
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::OpenPopupAt(button.geometry(orientation))
);
} else {
try_send!(tx, ModuleUpdateEvent::ClosePopup);

View File

@ -7,7 +7,9 @@ use crate::clients::wayland::{self, ToplevelEvent};
use crate::config::CommonConfig;
use crate::desktop_file::find_desktop_file;
use crate::modules::launcher::item::AppearanceOptions;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{arc_mut, lock, send_async, try_send, write_lock};
use color_eyre::{Help, Report};
use glib::Continue;
@ -313,7 +315,7 @@ impl Module<gtk::Box> for LauncherModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> crate::Result<ModuleWidget<gtk::Box>> {
) -> crate::Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
@ -408,8 +410,11 @@ impl Module<gtk::Box> for LauncherModule {
});
}
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
Ok(ModuleWidget {
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
Ok(ModuleParts {
widget: container,
popup,
})

View File

@ -1,3 +1,19 @@
use std::sync::{Arc, RwLock};
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
use tokio::sync::mpsc;
use tracing::debug;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{send, write_lock};
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
@ -24,19 +40,6 @@ pub mod upower;
#[cfg(feature = "workspaces")]
pub mod workspaces;
use crate::bridge_channel::BridgeChannel;
use crate::config::{BarPosition, CommonConfig, TransitionType};
use crate::popup::{Popup, WidgetGeometry};
use crate::{read_lock, send, write_lock};
use color_eyre::Result;
use glib::IsA;
use gtk::gdk::{EventMask, Monitor};
use gtk::prelude::*;
use gtk::{Application, EventBox, IconTheme, Orientation, Revealer, Widget};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
use tracing::debug;
#[derive(Clone)]
pub enum ModuleLocation {
Left,
@ -54,13 +57,15 @@ pub struct ModuleInfo<'a> {
#[derive(Debug)]
pub enum ModuleUpdateEvent<T> {
/// Sends an update to the module UI
/// Sends an update to the module UI.
Update(T),
/// Toggles the open state of the popup.
TogglePopup(WidgetGeometry),
/// Takes the button ID.
TogglePopup(usize),
/// Force sets the popup open.
/// Takes the button X position and width.
OpenPopup(WidgetGeometry),
/// Takes the button ID.
OpenPopup(usize),
OpenPopupAt(WidgetGeometry),
/// Force sets the popup closed.
ClosePopup,
}
@ -73,9 +78,55 @@ pub struct WidgetContext<TSend, TReceive> {
pub popup_rx: glib::Receiver<TSend>,
}
pub struct ModuleWidget<W: IsA<Widget>> {
pub struct ModuleParts<W: IsA<Widget>> {
pub widget: W,
pub popup: Option<gtk::Box>,
pub popup: Option<ModulePopupParts>,
}
impl<W: IsA<Widget>> ModuleParts<W> {
fn new(widget: W, popup: Option<ModulePopupParts>) -> Self {
Self { widget, popup }
}
}
#[derive(Debug, Clone)]
pub struct ModulePopupParts {
/// The popup container, with all its contents
pub container: gtk::Box,
/// An array of buttons which can be used for opening the popup.
/// For most modules, this will only be a single button.
/// For some advanced modules, such as `Launcher`, this is all item buttons.
pub buttons: Vec<Button>,
}
pub trait ModulePopup {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts>;
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts>;
}
impl ModulePopup for Option<gtk::Box> {
fn into_popup_parts(self, buttons: Vec<&Button>) -> Option<ModulePopupParts> {
self.into_popup_parts_owned(buttons.into_iter().cloned().collect())
}
fn into_popup_parts_owned(self, buttons: Vec<Button>) -> Option<ModulePopupParts> {
self.map(|container| ModulePopupParts { container, buttons })
}
}
pub trait PopupButton {
fn popup_id(&self) -> usize;
}
impl PopupButton for Button {
/// Gets the popup ID associated with this button.
/// This should only be called on buttons which are known to be associated with popups.
///
/// # Panics
/// Will panic if an ID has not been set.
fn popup_id(&self) -> usize {
*self.get_tag("popup-id").expect("data to exist")
}
}
pub trait Module<W>
@ -98,7 +149,7 @@ where
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<W>>;
) -> Result<ModuleParts<W>>;
fn into_popup(
self,
@ -118,9 +169,10 @@ where
pub fn create_module<TModule, TWidget, TSend, TRec>(
module: TModule,
id: usize,
name: Option<String>,
info: &ModuleInfo,
popup: &Arc<RwLock<Popup>>,
) -> Result<ModuleWidget<TWidget>>
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
TWidget: IsA<Widget>,
@ -142,29 +194,45 @@ where
controller_tx: ui_tx,
};
let name = TModule::name();
let module_name = TModule::name();
let instance_name = name.unwrap_or_else(|| module_name.to_string());
let module_parts = module.into_widget(context, info)?;
module_parts.widget.style_context().add_class(name);
module_parts.widget.style_context().add_class(module_name);
let mut has_popup = false;
if let Some(popup_content) = module_parts.popup.clone() {
let has_popup = if let Some(popup_content) = module_parts.popup.clone() {
popup_content
.container
.style_context()
.add_class(&format!("popup-{name}"));
.add_class(&format!("popup-{module_name}"));
register_popup_content(popup, id, popup_content);
has_popup = true;
}
register_popup_content(popup, id, instance_name, popup_content);
true
} else {
false
};
setup_receiver(channel, w_tx, p_tx, popup.clone(), name, id, has_popup);
setup_receiver(
channel,
w_tx,
p_tx,
popup.clone(),
module_name,
id,
has_popup,
);
Ok(module_parts)
}
/// Registers the popup content with the popup.
fn register_popup_content(popup: &Arc<RwLock<Popup>>, id: usize, popup_content: gtk::Box) {
write_lock!(popup).register_content(id, popup_content);
fn register_popup_content(
popup: &Arc<RwLock<Popup>>,
id: usize,
name: String,
popup_content: ModulePopupParts,
) {
write_lock!(popup).register_content(id, name, popup_content);
}
/// Sets up the bridge channel receiver
@ -196,40 +264,51 @@ fn setup_receiver<TSend>(
send!(w_tx, update);
}
ModuleUpdateEvent::TogglePopup(geometry) => {
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = write_lock!(popup);
if popup.is_visible() {
popup.hide();
} else {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
has_popup_opened = true;
}
}
}
ModuleUpdateEvent::OpenPopup(geometry) => {
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = write_lock!(popup);
popup.hide();
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_content(id);
popup.show(geometry);
popup.show(id, button_id);
has_popup_opened = true;
}
}
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = write_lock!(popup);
popup.hide();
popup.show_at(id, geometry);
// force re-render on initial open to try and fix size issue
if !has_popup_opened {
popup.show_at(id, geometry);
has_popup_opened = true;
}
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
let popup = read_lock!(popup);
let mut popup = write_lock!(popup);
popup.hide();
}
}
@ -239,14 +318,14 @@ fn setup_receiver<TSend>(
}
pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
widget_parts: &ModuleWidget<TWidget>,
widget_parts: &ModuleParts<TWidget>,
common: &CommonConfig,
) {
if let Some(ref name) = common.name {
widget_parts.widget.set_widget_name(name);
if let Some(ref popup) = widget_parts.popup {
popup.set_widget_name(&format!("popup-{name}"));
popup.container.set_widget_name(&format!("popup-{name}"));
}
}
@ -258,7 +337,10 @@ pub fn set_widget_identifiers<TWidget: IsA<Widget>>(
if let Some(ref popup) = widget_parts.popup {
for part in class.split(' ') {
popup.style_context().add_class(&format!("popup-{part}"));
popup
.container
.style_context()
.add_class(&format!("popup-{part}"));
}
}
}

View File

@ -1,29 +1,33 @@
mod config;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::gtk_helpers::add_class;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{send_async, try_send};
use color_eyre::Result;
use glib::{Continue, PropertySet};
use gtk::prelude::*;
use gtk::{Button, IconTheme, Label, Orientation, Scale};
use regex::Regex;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::error;
use crate::clients::music::{
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
};
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{send_async, try_send};
pub use self::config::MusicModule;
use self::config::PlayerType;
mod config;
#[derive(Debug)]
pub enum PlayerCommand {
Previous,
@ -178,10 +182,10 @@ impl Module<Button> for MusicModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
) -> Result<ModuleParts<Button>> {
let button = Button::new();
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&button_contents, "contents");
button_contents.add_class("contents");
button.add(&button_contents);
@ -199,16 +203,11 @@ impl Module<Button> for MusicModule {
button_contents.add(&icon_play);
button_contents.add(&label);
let orientation = info.bar_position.get_orientation();
{
let tx = context.tx.clone();
button.connect_clicked(move |button| {
try_send!(
tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
);
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
}
@ -252,12 +251,11 @@ impl Module<Button> for MusicModule {
});
};
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleWidget {
widget: button,
popup,
})
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@ -275,7 +273,7 @@ impl Module<Button> for MusicModule {
.width_request(128)
.height_request(128)
.build();
add_class(&album_image, "album-art");
album_image.add_class("album-art");
let icons = self.icons;
@ -284,28 +282,28 @@ impl Module<Button> for MusicModule {
let album_label = IconLabel::new(&icons.album, None, icon_theme);
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
add_class(&title_label.container, "title");
add_class(&album_label.container, "album");
add_class(&artist_label.container, "artist");
title_label.container.add_class("title");
album_label.container.add_class("album");
artist_label.container.add_class("artist");
info_box.add(&title_label.container);
info_box.add(&album_label.container);
info_box.add(&artist_label.container);
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
add_class(&controls_box, "controls");
controls_box.add_class("controls");
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
add_class(&btn_prev, "btn-prev");
btn_prev.add_class("btn-prev");
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
add_class(&btn_play, "btn-play");
btn_play.add_class("btn-play");
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
add_class(&btn_pause, "btn-pause");
btn_pause.add_class("btn-pause");
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
add_class(&btn_next, "btn-next");
btn_next.add_class("btn-next");
controls_box.add(&btn_prev);
controls_box.add(&btn_play);
@ -315,14 +313,14 @@ impl Module<Button> for MusicModule {
info_box.add(&controls_box);
let volume_box = gtk::Box::new(Orientation::Vertical, 5);
add_class(&volume_box, "volume");
volume_box.add_class("volume");
let volume_slider = Scale::with_range(Orientation::Vertical, 0.0, 100.0, 5.0);
volume_slider.set_inverted(true);
add_class(&volume_slider, "slider");
volume_slider.add_class("slider");
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
add_class(&volume_icon, "icon");
volume_icon.add_class("icon");
volume_box.pack_start(&volume_slider, true, true, 0);
volume_box.pack_end(&volume_icon, false, false, 0);
@ -359,17 +357,17 @@ impl Module<Button> for MusicModule {
});
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&progress_box, "progress");
progress_box.add_class("progress");
let progress_label = Label::new(None);
add_class(&progress_label, "label");
progress_label.add_class("label");
let progress = Scale::builder()
.orientation(Orientation::Horizontal)
.draw_value(false)
.hexpand(true)
.build();
add_class(&progress, "slider");
progress.add_class("slider");
progress_box.add(&progress);
progress_box.add(&progress_label);
@ -546,8 +544,8 @@ impl IconLabel {
let icon = new_icon_label(icon_input, icon_theme, 24);
let label = Label::new(label);
add_class(&icon, "icon-box");
add_class(&label, "label");
icon.add_class("icon-box");
label.add_class("label");
container.add(&icon);
container.add(&label);

View File

@ -1,5 +1,5 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::script::{OutputStream, Script, ScriptMode};
use crate::try_send;
use color_eyre::{Help, Report, Result};
@ -83,7 +83,7 @@ impl Module<Label> for ScriptModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Label>> {
) -> Result<ModuleParts<Label>> {
let label = Label::builder().use_markup(true).build();
label.set_angle(info.bar_position.get_angle());
@ -95,7 +95,7 @@ impl Module<Label> for ScriptModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: label,
popup: None,
})

View File

@ -1,6 +1,6 @@
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::send_async;
use color_eyre::Result;
use gtk::prelude::*;
@ -186,7 +186,7 @@ impl Module<gtk::Box> for SysInfoModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
@ -196,7 +196,7 @@ impl Module<gtk::Box> for SysInfoModule {
for format in &self.format {
let label = Label::builder().label(format).use_markup(true).build();
add_class(&label, "item");
label.add_class("item");
label.set_angle(info.bar_position.get_angle());
container.add(&label);
@ -220,7 +220,7 @@ impl Module<gtk::Box> for SysInfoModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@ -1,6 +1,6 @@
use crate::clients::system_tray::get_tray_event_client;
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{await_sync, try_send};
use color_eyre::Result;
use gtk::gdk_pixbuf::{Colorspace, InterpType};
@ -172,7 +172,7 @@ impl Module<MenuBar> for TrayModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> Result<ModuleWidget<MenuBar>> {
) -> Result<ModuleParts<MenuBar>> {
let container = MenuBar::new();
{
@ -238,7 +238,7 @@ impl Module<MenuBar> for TrayModule {
});
};
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@ -1,10 +1,3 @@
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::add_class;
use crate::image::ImageProvider;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::{await_sync, error, send_async, try_send};
use color_eyre::Result;
use futures_lite::stream::StreamExt;
use gtk::{prelude::*, Button};
@ -15,6 +8,16 @@ use tokio::sync::mpsc::{Receiver, Sender};
use upower_dbus::BatteryState;
use zbus;
use crate::clients::upower::get_display_proxy;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::ImageProvider;
use crate::modules::PopupButton;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
};
use crate::{await_sync, error, send_async, try_send};
const DAY: i64 = 24 * 60 * 60;
const HOUR: i64 = 60 * 60;
const MINUTE: i64 = 60;
@ -150,32 +153,31 @@ impl Module<gtk::Button> for UpowerModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<Button>> {
) -> Result<ModuleParts<Button>> {
let icon_theme = info.icon_theme.clone();
let icon = gtk::Image::new();
add_class(&icon, "icon");
icon.add_class("icon");
let label = Label::builder()
.label(&self.format)
.use_markup(true)
.build();
add_class(&label, "label");
label.add_class("label");
let container = gtk::Box::new(Orientation::Horizontal, 5);
add_class(&container, "contents");
container.add_class("contents");
let button = Button::new();
add_class(&button, "button");
button.add_class("button");
container.add(&icon);
container.add(&label);
button.add(&container);
let orientation = info.bar_position.get_orientation();
button.connect_clicked(move |button| {
try_send!(
context.tx,
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
ModuleUpdateEvent::TogglePopup(button.popup_id())
);
});
@ -193,12 +195,11 @@ impl Module<gtk::Button> for UpowerModule {
Continue(true)
});
let popup = self.into_popup(context.controller_tx, context.popup_rx, info);
let popup = self
.into_popup(context.controller_tx, context.popup_rx, info)
.into_popup_parts(vec![&button]);
Ok(ModuleWidget {
widget: button,
popup,
})
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
@ -215,7 +216,7 @@ impl Module<gtk::Button> for UpowerModule {
.build();
let label = Label::new(None);
add_class(&label, "upower-details");
label.add_class("upower-details");
container.add(&label);
rx.attach(None, move |properties| {

View File

@ -1,7 +1,7 @@
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::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{send_async, try_send};
use color_eyre::{Report, Result};
use gtk::prelude::*;
@ -154,7 +154,7 @@ impl Module<gtk::Box> for WorkspacesModule {
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleWidget<gtk::Box>> {
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let name_map = self.name_map.unwrap_or_default();
@ -277,7 +277,7 @@ impl Module<gtk::Box> for WorkspacesModule {
});
}
Ok(ModuleWidget {
Ok(ModuleParts {
widget: container,
popup: None,
})

View File

@ -1,18 +1,22 @@
use std::collections::HashMap;
use crate::config::BarPosition;
use crate::modules::ModuleInfo;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation};
use tracing::debug;
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
use crate::unique_id::get_unique_usize;
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: HashMap<usize, gtk::Box>,
pub cache: HashMap<usize, (String, ModulePopupParts)>,
monitor: Monitor,
pos: BarPosition,
current_widget: Option<usize>,
}
impl Popup {
@ -28,6 +32,7 @@ impl Popup {
.build();
gtk_layer_shell::init_for_window(&win);
gtk_layer_shell::set_monitor(&win, module_info.monitor);
gtk_layer_shell::set_layer(&win, gtk_layer_shell::Layer::Overlay);
gtk_layer_shell::set_namespace(&win, env!("CARGO_PKG_NAME"));
@ -108,20 +113,54 @@ impl Popup {
cache: HashMap::new(),
monitor: module_info.monitor.clone(),
pos,
current_widget: None,
}
}
pub fn register_content(&mut self, key: usize, content: gtk::Box) {
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
debug!("Registered popup content for #{}", key);
self.cache.insert(key, content);
for button in &content.buttons {
let id = get_unique_usize();
button.set_tag("popup-id", id);
}
self.cache.insert(key, (name, content));
}
pub fn show_content(&self, key: usize) {
pub fn show(&mut self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(content) = self.cache.get(&key) {
content.style_context().add_class("popup");
self.window.add(content);
if let Some((_name, content)) = self.cache.get(&widget_id) {
self.current_widget = Some(widget_id);
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some((_name, content)) = self.cache.get(&widget_id) {
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
self.set_pos(geometry);
}
}
@ -132,14 +171,9 @@ impl Popup {
}
}
/// Shows the popup
pub fn show(&self, geometry: WidgetGeometry) {
self.window.show();
self.set_pos(geometry);
}
/// Hides the popover
pub fn hide(&self) {
pub fn hide(&mut self) {
self.current_widget = None;
self.window.hide();
}
@ -148,6 +182,10 @@ impl Popup {
self.window.is_visible()
}
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
}
/// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation).
fn set_pos(&self, geometry: WidgetGeometry) {
@ -187,48 +225,4 @@ impl Popup {
gtk_layer_shell::set_margin(&self.window, edge, offset as i32);
}
/// Gets the absolute X position of the button
/// and its width / height (depending on orientation).
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
where
W: IsA<gtk::Widget>,
{
let widget_size = if orientation == Orientation::Horizontal {
widget.allocation().width()
} else {
widget.allocation().height()
};
let top_level = widget.toplevel().expect("Failed to get top-level widget");
let bar_size = if orientation == Orientation::Horizontal {
top_level.allocation().width()
} else {
top_level.allocation().height()
};
let (widget_x, widget_y) = widget
.translate_coordinates(&top_level, 0, 0)
.unwrap_or((0, 0));
let widget_pos = if orientation == Orientation::Horizontal {
widget_x
} else {
widget_y
};
WidgetGeometry {
position: widget_pos,
size: widget_size,
bar_size,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct WidgetGeometry {
position: i32,
size: i32,
bar_size: i32,
}