From 72b14b6c4ed3dccfe7b4b23b220ab0a87ec79aa2 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Mon, 10 Apr 2023 00:17:52 +0100 Subject: [PATCH] feat(custom): progress bar widget. Resolves partially #68. --- docs/modules/Custom.md | 47 ++++++++++++++--- src/modules/custom/mod.rs | 4 ++ src/modules/custom/progress.rs | 93 ++++++++++++++++++++++++++++++++++ src/modules/custom/slider.rs | 2 +- 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 src/modules/custom/progress.rs diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md index fc4332d..6596b16 100644 --- a/docs/modules/Custom.md +++ b/docs/modules/Custom.md @@ -17,11 +17,11 @@ You can think of these like HTML elements and their attributes. Every widget has the following options available; `type` is mandatory. -| Name | Type | Default | Description | -|---------|-----------------------------------------------------|---------|-------------------------------| -| `type` | `box` or `label` or `button` or `image` or `slider` | `null` | Type of GTK widget to create. | -| `name` | `string` | `null` | Widget name. | -| `class` | `string` | `null` | Widget class name. | +| Name | Type | Default | Description | +|---------|-------------------------------------------------------------------|---------|-------------------------------| +| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. | +| `name` | `string` | `null` | Widget name. | +| `class` | `string` | `null` | Widget class name. | #### Box @@ -86,9 +86,6 @@ If your input program requires an integer, you will need to round it. | `max` | `float` | `100` | Maximum slider value. | | `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | -Note that `on_change` will provide the **floating point** value as an argument. -If your input program requires an integer, you will need to round it. - The example slider widget below shows a volume control for MPC, which updates the server when changed, and polls the server for volume changes to keep the slider in sync. @@ -107,6 +104,40 @@ $slider = { } ``` +#### Progress + +A progress bar. + +> Type: `progress` + +Note that `value` expects a numeric value **between 0-`max`** as output. + +| Name | Type | Default | Description | +|---------------|----------------------------------------------------|--------------|---------------------------------------------------------------------------------| +| `src` | `image` | `null` | Image source. See [here](images) for information on images. | +| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | +| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. | +| `value` | `Script` | `null` | Script to run to get the progress bar value. Output must be a valid percentage. | +| `max` | `float` | `100` | Maximum progress bar value. | +| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | + +The example below shows progress for the current playing song in MPD, +and displays the elapsed/length timestamps as a label above: + +```corn +$progress = { + type = "custom" + bar = [ + { + type = "progress" + value = "500:mpc | sed -n 2p | awk '{ print $4 }' | grep -Eo '[0-9]+'" + label = "{{500:mpc | sed -n 2p | awk '{ print $3 }'}} elapsed" + length = 200 + } + ] +} +``` + ### Label Attributes > ℹ This is different to the `label` widget, although applies to it. diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs index d2831d5..8bc6fa4 100644 --- a/src/modules/custom/mod.rs +++ b/src/modules/custom/mod.rs @@ -2,6 +2,7 @@ mod r#box; mod button; mod image; mod label; +mod progress; mod slider; use self::image::ImageWidget; @@ -10,6 +11,7 @@ use self::r#box::BoxWidget; use self::slider::SliderWidget; use crate::config::CommonConfig; use crate::modules::custom::button::ButtonWidget; +use crate::modules::custom::progress::ProgressWidget; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::WidgetGeometry; use crate::script::Script; @@ -52,6 +54,7 @@ pub enum Widget { Button(ButtonWidget), Image(ImageWidget), Slider(SliderWidget), + Progress(ProgressWidget), } #[derive(Clone, Copy)] @@ -76,6 +79,7 @@ impl Widget { Widget::Button(widget) => parent.add(&widget.into_widget(context)), Widget::Image(widget) => parent.add(&widget.into_widget(context)), Widget::Slider(widget) => parent.add(&widget.into_widget(context)), + Widget::Progress(widget) => parent.add(&widget.into_widget(context)), } } } diff --git a/src/modules/custom/progress.rs b/src/modules/custom/progress.rs new file mode 100644 index 0000000..42e07d4 --- /dev/null +++ b/src/modules/custom/progress.rs @@ -0,0 +1,93 @@ +use super::{try_get_orientation, CustomWidget, CustomWidgetContext}; +use crate::dynamic_string::DynamicString; +use crate::script::{OutputStream, Script, ScriptInput}; +use crate::send; +use gtk::prelude::*; +use gtk::{Orientation, ProgressBar}; +use serde::Deserialize; +use tokio::spawn; +use tracing::error; + +#[derive(Debug, Deserialize, Clone)] +pub struct ProgressWidget { + name: Option, + class: Option, + orientation: Option, + label: Option, + value: Option, + #[serde(default = "default_max")] + max: f64, + length: Option, +} + +const fn default_max() -> f64 { + 100.0 +} + +// TODO: Reduce duplication with slider, other widgets. +impl CustomWidget for ProgressWidget { + type Widget = ProgressBar; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = ProgressBar::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(context.bar_orientation)); + } + + if let Some(length) = self.length { + builder = match context.bar_orientation { + Orientation::Horizontal => builder.width_request(length), + Orientation::Vertical => builder.height_request(length), + _ => builder, + } + } + + let progress = builder.build(); + + if let Some(class) = self.class { + progress.style_context().add_class(&class); + } + + if let Some(value) = self.value { + let script = Script::from(value); + let progress = progress.clone(); + + let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + spawn(async move { + script + .run(None, move |stream, _success| match stream { + OutputStream::Stdout(out) => match out.parse::() { + Ok(value) => send!(tx, value), + Err(err) => error!("{err:?}"), + }, + OutputStream::Stderr(err) => error!("{err:?}"), + }) + .await; + }); + + rx.attach(None, move |value| { + progress.set_fraction(value / self.max); + Continue(true) + }); + } + + if let Some(text) = self.label { + let progress = progress.clone(); + progress.set_show_text(true); + + DynamicString::new(&text, move |string| { + progress.set_text(Some(&string)); + Continue(true) + }); + } + + progress + } +} diff --git a/src/modules/custom/slider.rs b/src/modules/custom/slider.rs index c91e539..5f1aa95 100644 --- a/src/modules/custom/slider.rs +++ b/src/modules/custom/slider.rs @@ -1,4 +1,4 @@ -use crate::modules::custom::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent}; +use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent}; use crate::popup::Popup; use crate::script::{OutputStream, Script, ScriptInput}; use crate::{send, try_send};