From 2ab06f044ec300628d6648852d395889b6752b76 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Fri, 7 Apr 2023 20:22:31 +0100 Subject: [PATCH] refactor(custom): split into enum with separate file per widget --- src/modules/custom.rs | 309 ----------------------------------- src/modules/custom/box.rs | 43 +++++ src/modules/custom/button.rs | 56 +++++++ src/modules/custom/image.rs | 43 +++++ src/modules/custom/label.rs | 42 +++++ src/modules/custom/mod.rs | 187 +++++++++++++++++++++ 6 files changed, 371 insertions(+), 309 deletions(-) delete mode 100644 src/modules/custom.rs create mode 100644 src/modules/custom/box.rs create mode 100644 src/modules/custom/button.rs create mode 100644 src/modules/custom/image.rs create mode 100644 src/modules/custom/label.rs create mode 100644 src/modules/custom/mod.rs diff --git a/src/modules/custom.rs b/src/modules/custom.rs deleted file mode 100644 index 13af4f0..0000000 --- a/src/modules/custom.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::config::CommonConfig; -use crate::dynamic_string::DynamicString; -use crate::image::ImageProvider; -use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::popup::{ButtonGeometry, Popup}; -use crate::script::Script; -use crate::{send_async, try_send}; -use color_eyre::{Report, Result}; -use gtk::prelude::*; -use gtk::{Button, IconTheme, Label, Orientation}; -use serde::Deserialize; -use tokio::spawn; -use tokio::sync::mpsc::{Receiver, Sender}; -use tracing::{debug, error}; - -#[derive(Debug, Deserialize, Clone)] -pub struct CustomModule { - /// Container class name - class: Option, - /// Widgets to add to the bar container - bar: Vec, - /// Widgets to add to the popup container - popup: Option>, - - #[serde(flatten)] - pub common: Option, -} - -/// Attempts to parse an `Orientation` from `String` -fn try_get_orientation(orientation: &str) -> Result { - match orientation.to_lowercase().as_str() { - "horizontal" | "h" => Ok(Orientation::Horizontal), - "vertical" | "v" => Ok(Orientation::Vertical), - _ => Err(Report::msg("Invalid orientation string in config")), - } -} - -/// Widget attributes -#[derive(Debug, Deserialize, Clone)] -pub struct Widget { - /// Type of GTK widget to add - #[serde(rename = "type")] - widget_type: WidgetType, - widgets: Option>, - label: Option, - name: Option, - class: Option, - on_click: Option, - orientation: Option, - src: Option, - size: Option, -} - -/// Supported GTK widget types -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "snake_case")] -pub enum WidgetType { - Box, - Label, - Button, - Image, -} - -impl Widget { - /// Creates this widget and adds it to the parent container - fn add_to( - self, - parent: >k::Box, - tx: Sender, - bar_orientation: Orientation, - icon_theme: &IconTheme, - ) { - match self.widget_type { - WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)), - WidgetType::Label => parent.add(&self.into_label()), - WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)), - WidgetType::Image => parent.add(&self.into_image(icon_theme)), - } - } - - /// Creates a `gtk::Box` from this widget - fn into_box( - self, - tx: &Sender, - bar_orientation: Orientation, - icon_theme: &IconTheme, - ) -> gtk::Box { - let mut builder = gtk::Box::builder(); - - if let Some(name) = self.name { - builder = builder.name(&name); - } - - if let Some(orientation) = self.orientation { - builder = builder - .orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal)); - } - - let container = builder.build(); - - if let Some(class) = self.class { - container.style_context().add_class(&class); - } - - if let Some(widgets) = self.widgets { - for widget in widgets { - widget.add_to(&container, tx.clone(), bar_orientation, icon_theme); - } - } - - container - } - - /// Creates a `gtk::Label` from this widget - fn into_label(self) -> Label { - let mut builder = Label::builder().use_markup(true); - - if let Some(name) = self.name { - builder = builder.name(name); - } - - let label = builder.build(); - - if let Some(class) = self.class { - label.style_context().add_class(&class); - } - - let text = self.label.map_or_else(String::new, |text| text); - - { - let label = label.clone(); - DynamicString::new(&text, move |string| { - label.set_label(&string); - Continue(true) - }); - } - - label - } - - /// Creates a `gtk::Button` from this widget - fn into_button(self, tx: Sender, bar_orientation: Orientation) -> Button { - let mut builder = Button::builder(); - - if let Some(name) = self.name { - builder = builder.name(name); - } - - let button = builder.build(); - - if let Some(text) = self.label { - let label = Label::new(None); - label.set_use_markup(true); - label.set_markup(&text); - button.add(&label); - } - - if let Some(class) = self.class { - button.style_context().add_class(&class); - } - - if let Some(exec) = self.on_click { - button.connect_clicked(move |button| { - try_send!( - tx, - ExecEvent { - cmd: exec.clone(), - geometry: Popup::button_pos(button, bar_orientation), - } - ); - }); - } - - button - } - - fn into_image(self, icon_theme: &IconTheme) -> gtk::Image { - let mut builder = gtk::Image::builder(); - - if let Some(name) = self.name { - builder = builder.name(&name); - } - - let gtk_image = builder.build(); - - if let Some(src) = self.src { - let size = self.size.unwrap_or(32); - if let Err(err) = ImageProvider::parse(&src, icon_theme, size) - .and_then(|image| image.load_into_image(gtk_image.clone())) - { - error!("{err:?}"); - } - } - - if let Some(class) = self.class { - gtk_image.style_context().add_class(&class); - } - - gtk_image - } -} - -#[derive(Debug)] -pub struct ExecEvent { - cmd: String, - geometry: ButtonGeometry, -} - -impl Module for CustomModule { - type SendMessage = (); - type ReceiveMessage = ExecEvent; - - fn name() -> &'static str { - "custom" - } - - fn spawn_controller( - &self, - _info: &ModuleInfo, - tx: Sender>, - mut rx: Receiver, - ) -> Result<()> { - spawn(async move { - while let Some(event) = rx.recv().await { - if event.cmd.starts_with('!') { - let script = Script::from(&event.cmd[1..]); - - debug!("executing command: '{}'", script.cmd); - // TODO: Migrate to use script.run - if let Err(err) = script.get_output().await { - error!("{err:?}"); - } - } else if event.cmd == "popup:toggle" { - send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry)); - } else if event.cmd == "popup:open" { - send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry)); - } else if event.cmd == "popup:close" { - send_async!(tx, ModuleUpdateEvent::ClosePopup); - } else { - error!("Received invalid command: '{}'", event.cmd); - } - } - }); - - Ok(()) - } - - fn into_widget( - self, - context: WidgetContext, - info: &ModuleInfo, - ) -> Result> { - let orientation = info.bar_position.get_orientation(); - let container = gtk::Box::builder().orientation(orientation).build(); - - if let Some(ref class) = self.class { - container.style_context().add_class(class); - } - - self.bar.clone().into_iter().for_each(|widget| { - widget.add_to( - &container, - context.controller_tx.clone(), - orientation, - info.icon_theme, - ); - }); - - let popup = self.into_popup(context.controller_tx, context.popup_rx, info); - - Ok(ModuleWidget { - widget: container, - popup, - }) - } - - fn into_popup( - self, - tx: Sender, - _rx: glib::Receiver, - info: &ModuleInfo, - ) -> Option - where - Self: Sized, - { - let container = gtk::Box::builder().name("popup-custom").build(); - - if let Some(class) = self.class { - container - .style_context() - .add_class(format!("popup-{class}").as_str()); - } - - if let Some(popup) = self.popup { - for widget in popup { - widget.add_to( - &container, - tx.clone(), - Orientation::Horizontal, - info.icon_theme, - ); - } - } - - container.show_all(); - - Some(container) - } -} diff --git a/src/modules/custom/box.rs b/src/modules/custom/box.rs new file mode 100644 index 0000000..6ea0748 --- /dev/null +++ b/src/modules/custom/box.rs @@ -0,0 +1,43 @@ +use super::{try_get_orientation, CustomWidget, CustomWidgetContext, Widget}; +use gtk::prelude::*; +use gtk::Orientation; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct BoxWidget { + name: Option, + class: Option, + orientation: Option, + widgets: Option>, +} + +impl CustomWidget for BoxWidget { + type Widget = gtk::Box; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = gtk::Box::builder(); + + if let Some(name) = self.name { + builder = builder.name(&name); + } + + if let Some(orientation) = self.orientation { + builder = builder + .orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal)); + } + + let container = builder.build(); + + if let Some(class) = self.class { + container.style_context().add_class(&class); + } + + if let Some(widgets) = self.widgets { + for widget in widgets { + widget.add_to(&container, context); + } + } + + container + } +} diff --git a/src/modules/custom/button.rs b/src/modules/custom/button.rs new file mode 100644 index 0000000..e688234 --- /dev/null +++ b/src/modules/custom/button.rs @@ -0,0 +1,56 @@ +use super::{CustomWidget, CustomWidgetContext, ExecEvent}; +use crate::popup::Popup; +use crate::try_send; +use gtk::prelude::*; +use gtk::{Button, Label}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct ButtonWidget { + name: Option, + class: Option, + label: Option, + on_click: Option, +} + +impl CustomWidget for ButtonWidget { + type Widget = Button; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = Button::builder(); + + if let Some(name) = self.name { + builder = builder.name(name); + } + + let button = builder.build(); + + if let Some(text) = self.label { + let label = Label::new(None); + label.set_use_markup(true); + label.set_markup(&text); + button.add(&label); + } + + if let Some(class) = self.class { + button.style_context().add_class(&class); + } + + if let Some(exec) = self.on_click { + let bar_orientation = context.bar_orientation; + let tx = context.tx.clone(); + + button.connect_clicked(move |button| { + try_send!( + tx, + ExecEvent { + cmd: exec.clone(), + geometry: Popup::button_pos(button, bar_orientation), + } + ); + }); + } + + button + } +} diff --git a/src/modules/custom/image.rs b/src/modules/custom/image.rs new file mode 100644 index 0000000..ba56638 --- /dev/null +++ b/src/modules/custom/image.rs @@ -0,0 +1,43 @@ +use super::{CustomWidget, CustomWidgetContext}; +use crate::image::ImageProvider; +use gtk::prelude::*; +use gtk::Image; +use serde::Deserialize; +use tracing::error; + +#[derive(Debug, Deserialize, Clone)] +pub struct ImageWidget { + name: Option, + class: Option, + src: Option, + size: Option, +} + +impl CustomWidget for ImageWidget { + type Widget = Image; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = Image::builder(); + + if let Some(name) = self.name { + builder = builder.name(&name); + } + + let gtk_image = builder.build(); + + if let Some(src) = self.src { + let size = self.size.unwrap_or(32); + if let Err(err) = ImageProvider::parse(&src, context.icon_theme, size) + .and_then(|image| image.load_into_image(gtk_image.clone())) + { + error!("{err:?}"); + } + } + + if let Some(class) = self.class { + gtk_image.style_context().add_class(&class); + } + + gtk_image + } +} diff --git a/src/modules/custom/label.rs b/src/modules/custom/label.rs new file mode 100644 index 0000000..9badc5d --- /dev/null +++ b/src/modules/custom/label.rs @@ -0,0 +1,42 @@ +use super::{CustomWidget, CustomWidgetContext}; +use crate::dynamic_string::DynamicString; +use gtk::prelude::*; +use gtk::Label; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct LabelWidget { + name: Option, + class: Option, + label: Option, +} + +impl CustomWidget for LabelWidget { + type Widget = Label; + + fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget { + let mut builder = Label::builder().use_markup(true); + + if let Some(name) = self.name { + builder = builder.name(name); + } + + let label = builder.build(); + + if let Some(class) = self.class { + label.style_context().add_class(&class); + } + + let text = self.label.map_or_else(String::new, |text| text); + + { + let label = label.clone(); + DynamicString::new(&text, move |string| { + label.set_label(&string); + Continue(true) + }); + } + + label + } +} diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs new file mode 100644 index 0000000..e929d7b --- /dev/null +++ b/src/modules/custom/mod.rs @@ -0,0 +1,187 @@ +mod r#box; +mod button; +mod image; +mod label; + +use self::image::ImageWidget; +use self::label::LabelWidget; +use self::r#box::BoxWidget; +use crate::config::CommonConfig; +use crate::modules::custom::button::ButtonWidget; +use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; +use crate::popup::ButtonGeometry; +use crate::script::Script; +use crate::send_async; +use color_eyre::{Report, Result}; +use gtk::prelude::*; +use gtk::{IconTheme, Orientation}; +use serde::Deserialize; +use tokio::spawn; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{debug, error}; + +#[derive(Debug, Deserialize, Clone)] +pub struct CustomModule { + /// Container class name + class: Option, + /// Widgets to add to the bar container + bar: Vec, + /// Widgets to add to the popup container + popup: Option>, + + #[serde(flatten)] + pub common: Option, +} + +/// Attempts to parse an `Orientation` from `String` +fn try_get_orientation(orientation: &str) -> Result { + match orientation.to_lowercase().as_str() { + "horizontal" | "h" => Ok(Orientation::Horizontal), + "vertical" | "v" => Ok(Orientation::Vertical), + _ => Err(Report::msg("Invalid orientation string in config")), + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Widget { + Box(BoxWidget), + Label(LabelWidget), + Button(ButtonWidget), + Image(ImageWidget) +} + +#[derive(Clone, Copy)] +struct CustomWidgetContext<'a> { + tx: &'a Sender, + bar_orientation: Orientation, + icon_theme: &'a IconTheme, +} + +trait CustomWidget { + type Widget; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget; +} + +impl Widget { + /// Creates this widget and adds it to the parent container + fn add_to(self, parent: >k::Box, context: CustomWidgetContext) { + match self { + Widget::Box(widget) => parent.add(&widget.into_widget(context)), + Widget::Label(widget) => parent.add(&widget.into_widget(context)), + Widget::Button(widget) => parent.add(&widget.into_widget(context)), + Widget::Image(widget) => parent.add(&widget.into_widget(context)), + } + } +} + +#[derive(Debug)] +pub struct ExecEvent { + cmd: String, + geometry: ButtonGeometry, +} + +impl Module for CustomModule { + type SendMessage = (); + type ReceiveMessage = ExecEvent; + + fn name() -> &'static str { + "custom" + } + + fn spawn_controller( + &self, + _info: &ModuleInfo, + tx: Sender>, + mut rx: Receiver, + ) -> Result<()> { + spawn(async move { + while let Some(event) = rx.recv().await { + if event.cmd.starts_with('!') { + let script = Script::from(&event.cmd[1..]); + + debug!("executing command: '{}'", script.cmd); + // TODO: Migrate to use script.run + if let Err(err) = script.get_output().await { + error!("{err:?}"); + } + } else if event.cmd == "popup:toggle" { + send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry)); + } else if event.cmd == "popup:open" { + send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry)); + } else if event.cmd == "popup:close" { + send_async!(tx, ModuleUpdateEvent::ClosePopup); + } else { + error!("Received invalid command: '{}'", event.cmd); + } + } + }); + + Ok(()) + } + + fn into_widget( + self, + context: WidgetContext, + info: &ModuleInfo, + ) -> Result> { + let orientation = info.bar_position.get_orientation(); + let container = gtk::Box::builder().orientation(orientation).build(); + + if let Some(ref class) = self.class { + container.style_context().add_class(class); + } + + let custom_context = CustomWidgetContext { + tx: &context.controller_tx, + bar_orientation: orientation, + icon_theme: info.icon_theme, + }; + + self.bar.clone().into_iter().for_each(|widget| { + widget.add_to(&container, custom_context); + }); + + let popup = self.into_popup(context.controller_tx, context.popup_rx, info); + + Ok(ModuleWidget { + widget: container, + popup, + }) + } + + fn into_popup( + self, + tx: Sender, + _rx: glib::Receiver, + info: &ModuleInfo, + ) -> Option + where + Self: Sized, + { + let container = gtk::Box::builder().name("popup-custom").build(); + + if let Some(class) = self.class { + container + .style_context() + .add_class(format!("popup-{class}").as_str()); + } + + if let Some(popup) = self.popup { + let custom_context = CustomWidgetContext { + tx: &tx, + bar_orientation: info.bar_position.get_orientation(), + icon_theme: info.icon_theme, + }; + + for widget in popup { + widget.add_to(&container, custom_context); + } + } + + container.show_all(); + + Some(container) + } +}