feat(custom): progress bar widget.

Resolves partially #68.
This commit is contained in:
Jake Stanger 2023-04-10 00:17:52 +01:00
parent 910945306c
commit 72b14b6c4e
No known key found for this signature in database
GPG Key ID: C51FC8F9CB0BEA61
4 changed files with 137 additions and 9 deletions

View File

@ -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. Every widget has the following options available; `type` is mandatory.
| Name | Type | Default | Description | | Name | Type | Default | Description |
|---------|-----------------------------------------------------|---------|-------------------------------| |---------|-------------------------------------------------------------------|---------|-------------------------------|
| `type` | `box` or `label` or `button` or `image` or `slider` | `null` | Type of GTK widget to create. | | `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
| `name` | `string` | `null` | Widget name. | | `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. | | `class` | `string` | `null` | Widget class name. |
#### Box #### Box
@ -86,9 +86,6 @@ If your input program requires an integer, you will need to round it.
| `max` | `float` | `100` | Maximum slider value. | | `max` | `float` | `100` | Maximum slider value. |
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | | `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, 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. 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 ### Label Attributes
> This is different to the `label` widget, although applies to it. > This is different to the `label` widget, although applies to it.

View File

@ -2,6 +2,7 @@ mod r#box;
mod button; mod button;
mod image; mod image;
mod label; mod label;
mod progress;
mod slider; mod slider;
use self::image::ImageWidget; use self::image::ImageWidget;
@ -10,6 +11,7 @@ use self::r#box::BoxWidget;
use self::slider::SliderWidget; use self::slider::SliderWidget;
use crate::config::CommonConfig; use crate::config::CommonConfig;
use crate::modules::custom::button::ButtonWidget; use crate::modules::custom::button::ButtonWidget;
use crate::modules::custom::progress::ProgressWidget;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::WidgetGeometry; use crate::popup::WidgetGeometry;
use crate::script::Script; use crate::script::Script;
@ -52,6 +54,7 @@ pub enum Widget {
Button(ButtonWidget), Button(ButtonWidget),
Image(ImageWidget), Image(ImageWidget),
Slider(SliderWidget), Slider(SliderWidget),
Progress(ProgressWidget),
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -76,6 +79,7 @@ impl Widget {
Widget::Button(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)), Widget::Image(widget) => parent.add(&widget.into_widget(context)),
Widget::Slider(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)),
} }
} }
} }

View File

@ -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<String>,
class: Option<String>,
orientation: Option<String>,
label: Option<String>,
value: Option<ScriptInput>,
#[serde(default = "default_max")]
max: f64,
length: Option<i32>,
}
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::<f64>() {
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
}
}

View File

@ -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::popup::Popup;
use crate::script::{OutputStream, Script, ScriptInput}; use crate::script::{OutputStream, Script, ScriptInput};
use crate::{send, try_send}; use crate::{send, try_send};