Graph widget (#338)
This commit is contained in:
parent
5e5692742e
commit
106106ade3
4 changed files with 370 additions and 1 deletions
336
crates/eww/src/widgets/graph.rs
Normal file
336
crates/eww/src/widgets/graph.rs
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
use std::{cell::RefCell, collections::VecDeque};
|
||||||
|
// https://www.figuiere.net/technotes/notes/tn002/
|
||||||
|
// https://github.com/gtk-rs/examples/blob/master/src/bin/listbox_model.rs
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use glib::{object_subclass, wrapper};
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
|
use crate::error_handling_ctx;
|
||||||
|
|
||||||
|
// This widget shouldn't be a Bin/Container but I've not been
|
||||||
|
// able to subclass just a gtk::Widget
|
||||||
|
wrapper! {
|
||||||
|
pub struct Graph(ObjectSubclass<GraphPriv>)
|
||||||
|
@extends gtk::Bin, gtk::Container, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphPriv {
|
||||||
|
value: RefCell<f64>,
|
||||||
|
thickness: RefCell<f64>,
|
||||||
|
line_style: RefCell<String>,
|
||||||
|
min: RefCell<f64>,
|
||||||
|
max: RefCell<f64>,
|
||||||
|
dynamic: RefCell<bool>,
|
||||||
|
time_range: RefCell<u64>,
|
||||||
|
history: RefCell<VecDeque<(std::time::Instant, f64)>>,
|
||||||
|
extra_point: RefCell<Option<(std::time::Instant, f64)>>,
|
||||||
|
last_updated_at: RefCell<std::time::Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GraphPriv {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
value: RefCell::new(0.0),
|
||||||
|
thickness: RefCell::new(1.0),
|
||||||
|
line_style: RefCell::new("miter".to_string()),
|
||||||
|
min: RefCell::new(0.0),
|
||||||
|
max: RefCell::new(100.0),
|
||||||
|
dynamic: RefCell::new(true),
|
||||||
|
time_range: RefCell::new(10),
|
||||||
|
history: RefCell::new(VecDeque::new()),
|
||||||
|
extra_point: RefCell::new(None),
|
||||||
|
last_updated_at: RefCell::new(std::time::Instant::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphPriv {
|
||||||
|
// Updates the history, removing points ouside the range
|
||||||
|
fn update_history(&self, v: (std::time::Instant, f64)) {
|
||||||
|
let mut history = self.history.borrow_mut();
|
||||||
|
let mut last_value = self.extra_point.borrow_mut();
|
||||||
|
let mut last_updated_at = self.last_updated_at.borrow_mut();
|
||||||
|
*last_updated_at = std::time::Instant::now();
|
||||||
|
|
||||||
|
while let Some(entry) = history.front() {
|
||||||
|
if last_updated_at.duration_since(entry.0).as_millis() as u64 > *self.time_range.borrow() {
|
||||||
|
*last_value = history.pop_front();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history.push_back(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for GraphPriv {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpec::new_double("value", "Value", "The value", 0f64, 100f64, 0f64, glib::ParamFlags::READWRITE),
|
||||||
|
glib::ParamSpec::new_double(
|
||||||
|
"thickness",
|
||||||
|
"Thickness",
|
||||||
|
"The Thickness",
|
||||||
|
0f64,
|
||||||
|
100f64,
|
||||||
|
1f64,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_double(
|
||||||
|
"max",
|
||||||
|
"Maximum Value",
|
||||||
|
"The Maximum Value",
|
||||||
|
0f64,
|
||||||
|
f64::MAX,
|
||||||
|
100f64,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_double(
|
||||||
|
"min",
|
||||||
|
"Minumum Value",
|
||||||
|
"The Minimum Value",
|
||||||
|
0f64,
|
||||||
|
f64::MAX,
|
||||||
|
0f64,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_boolean("dynamic", "Dynamic", "If it is dynamic", true, glib::ParamFlags::READWRITE),
|
||||||
|
glib::ParamSpec::new_uint64(
|
||||||
|
"time-range",
|
||||||
|
"Time Range",
|
||||||
|
"The Time Range",
|
||||||
|
0u64,
|
||||||
|
u64::MAX,
|
||||||
|
10u64,
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_string(
|
||||||
|
"line-style",
|
||||||
|
"Line Style",
|
||||||
|
"The Line Style",
|
||||||
|
Some("miter"),
|
||||||
|
glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(&self, obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
||||||
|
match pspec.name() {
|
||||||
|
"value" => {
|
||||||
|
let value = value.get().unwrap();
|
||||||
|
self.value.replace(value);
|
||||||
|
self.update_history((std::time::Instant::now(), value));
|
||||||
|
obj.queue_draw();
|
||||||
|
}
|
||||||
|
"thickness" => {
|
||||||
|
self.thickness.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
"max" => {
|
||||||
|
self.max.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
"min" => {
|
||||||
|
self.min.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
"dynamic" => {
|
||||||
|
self.dynamic.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
"time-range" => {
|
||||||
|
self.time_range.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
"line-style" => {
|
||||||
|
self.line_style.replace(value.get().unwrap());
|
||||||
|
}
|
||||||
|
x => panic!("Tried to set inexistant property of Graph: {}", x,),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"value" => self.value.borrow().to_value(),
|
||||||
|
"thickness" => self.thickness.borrow().to_value(),
|
||||||
|
"max" => self.max.borrow().to_value(),
|
||||||
|
"min" => self.min.borrow().to_value(),
|
||||||
|
"dynamic" => self.dynamic.borrow().to_value(),
|
||||||
|
"time-range" => self.time_range.borrow().to_value(),
|
||||||
|
"line-style" => self.line_style.borrow().to_value(),
|
||||||
|
x => panic!("Tried to access inexistant property of Graph: {}", x,),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[object_subclass]
|
||||||
|
impl ObjectSubclass for GraphPriv {
|
||||||
|
type ParentType = gtk::Bin;
|
||||||
|
type Type = Graph;
|
||||||
|
|
||||||
|
const NAME: &'static str = "Graph";
|
||||||
|
|
||||||
|
fn class_init(klass: &mut Self::Class) {
|
||||||
|
klass.set_css_name("graph");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Graph {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
glib::Object::new::<Self>(&[]).expect("Failed to create Graph Widget")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerImpl for GraphPriv {
|
||||||
|
fn add(&self, _container: &Self::Type, _widget: >k::Widget) {
|
||||||
|
error_handling_ctx::print_error(anyhow!("Error, Graph widget shoudln't have any children"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinImpl for GraphPriv {}
|
||||||
|
impl WidgetImpl for GraphPriv {
|
||||||
|
fn preferred_width(&self, _widget: &Self::Type) -> (i32, i32) {
|
||||||
|
let thickness = *self.thickness.borrow() as i32;
|
||||||
|
(thickness, thickness)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preferred_width_for_height(&self, _widget: &Self::Type, height: i32) -> (i32, i32) {
|
||||||
|
(height, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preferred_height(&self, _widget: &Self::Type) -> (i32, i32) {
|
||||||
|
let thickness = *self.thickness.borrow() as i32;
|
||||||
|
(thickness, thickness)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preferred_height_for_width(&self, _widget: &Self::Type, width: i32) -> (i32, i32) {
|
||||||
|
(width, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, widget: &Self::Type, cr: &cairo::Context) -> Inhibit {
|
||||||
|
let res: Result<()> = try {
|
||||||
|
let history = &*self.history.borrow();
|
||||||
|
let extra_point = *self.extra_point.borrow();
|
||||||
|
|
||||||
|
// Calculate the max value
|
||||||
|
let (min, max) = {
|
||||||
|
let mut max = *self.max.borrow();
|
||||||
|
let min = *self.min.borrow();
|
||||||
|
let dynamic = *self.dynamic.borrow() as bool;
|
||||||
|
if dynamic {
|
||||||
|
// Check for points higher than max
|
||||||
|
for (_, value) in history {
|
||||||
|
if *value > max {
|
||||||
|
max = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((_, value)) = extra_point {
|
||||||
|
if value > max {
|
||||||
|
max = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(min, max)
|
||||||
|
};
|
||||||
|
|
||||||
|
let styles = widget.style_context();
|
||||||
|
let (margin_top, margin_right, margin_bottom, margin_left) = {
|
||||||
|
let margin = styles.margin(gtk::StateFlags::NORMAL);
|
||||||
|
(margin.top as f64, margin.right as f64, margin.bottom as f64, margin.left as f64)
|
||||||
|
};
|
||||||
|
let width = widget.allocated_width() as f64 - margin_left - margin_right;
|
||||||
|
let height = widget.allocated_height() as f64 - margin_top - margin_bottom;
|
||||||
|
|
||||||
|
// Calculate graph points once
|
||||||
|
// Separating this into another function would require pasing a
|
||||||
|
// GraphPriv that would hide interior mutability
|
||||||
|
let points = {
|
||||||
|
let value_range = max - min;
|
||||||
|
let time_range = *self.time_range.borrow() as f64;
|
||||||
|
let last_updated_at = self.last_updated_at.borrow();
|
||||||
|
let mut points = history
|
||||||
|
.iter()
|
||||||
|
.map(|(instant, value)| {
|
||||||
|
let t = last_updated_at.duration_since(*instant).as_millis() as f64;
|
||||||
|
let x = width * (1.0 - (t / time_range));
|
||||||
|
let y = height * (1.0 - ((value - min) / value_range));
|
||||||
|
(x, y)
|
||||||
|
})
|
||||||
|
.collect::<VecDeque<(f64, f64)>>();
|
||||||
|
|
||||||
|
// Aad an extra point outside of the graph to extend the line to the left
|
||||||
|
if let Some((instant, value)) = extra_point {
|
||||||
|
let t = last_updated_at.duration_since(instant).as_millis() as f64;
|
||||||
|
let x = -width * ((t - time_range) / time_range);
|
||||||
|
let y = height * (1.0 - ((value - min) / value_range));
|
||||||
|
points.push_front((x, y));
|
||||||
|
}
|
||||||
|
points
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually draw the graph
|
||||||
|
cr.save()?;
|
||||||
|
cr.translate(margin_left, margin_top);
|
||||||
|
cr.rectangle(0.0, 0.0, width, height);
|
||||||
|
cr.clip();
|
||||||
|
|
||||||
|
// Draw Background
|
||||||
|
let bg_color: gdk::RGBA = styles.style_property_for_state("background-color", gtk::StateFlags::NORMAL).get()?;
|
||||||
|
if bg_color.alpha > 0.0 {
|
||||||
|
if let Some(first_point) = points.front() {
|
||||||
|
cr.line_to(first_point.0, height + margin_bottom);
|
||||||
|
}
|
||||||
|
for (x, y) in points.iter() {
|
||||||
|
cr.line_to(*x, *y);
|
||||||
|
}
|
||||||
|
cr.line_to(width, height);
|
||||||
|
|
||||||
|
cr.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
|
||||||
|
cr.fill()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Line
|
||||||
|
let line_color: gdk::RGBA = styles.color(gtk::StateFlags::NORMAL);
|
||||||
|
let thickness = *self.thickness.borrow();
|
||||||
|
if line_color.alpha > 0.0 && thickness > 0.0 {
|
||||||
|
for (x, y) in points.iter() {
|
||||||
|
cr.line_to(*x, *y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_style = &*self.line_style.borrow();
|
||||||
|
apply_line_style(line_style.as_str(), cr)?;
|
||||||
|
cr.set_line_width(thickness);
|
||||||
|
cr.set_source_rgba(line_color.red, line_color.green, line_color.blue, line_color.alpha);
|
||||||
|
cr.stroke()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.reset_clip();
|
||||||
|
cr.restore()?;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = res {
|
||||||
|
error_handling_ctx::print_error(error)
|
||||||
|
};
|
||||||
|
|
||||||
|
gtk::Inhibit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_line_style(style: &str, cr: &cairo::Context) -> Result<()> {
|
||||||
|
match style {
|
||||||
|
"miter" => {
|
||||||
|
cr.set_line_cap(cairo::LineCap::Butt);
|
||||||
|
cr.set_line_join(cairo::LineJoin::Miter);
|
||||||
|
}
|
||||||
|
"bevel" => {
|
||||||
|
cr.set_line_cap(cairo::LineCap::Square);
|
||||||
|
cr.set_line_join(cairo::LineJoin::Bevel);
|
||||||
|
}
|
||||||
|
"round" => {
|
||||||
|
cr.set_line_cap(cairo::LineCap::Round);
|
||||||
|
cr.set_line_join(cairo::LineJoin::Round);
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!("Error, the value: {} for atribute join is not valid", style))?,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ use std::process::Command;
|
||||||
|
|
||||||
pub mod build_widget;
|
pub mod build_widget;
|
||||||
pub mod circular_progressbar;
|
pub mod circular_progressbar;
|
||||||
pub mod widget_definitions;
|
|
||||||
pub mod def_widget_macro;
|
pub mod def_widget_macro;
|
||||||
|
pub mod graph;
|
||||||
|
pub mod widget_definitions;
|
||||||
|
|
||||||
const CMD_STRING_PLACEHODLER: &str = "{}";
|
const CMD_STRING_PLACEHODLER: &str = "{}";
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ pub(super) fn widget_use_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<gtk::W
|
||||||
"centerbox" => build_center_box(bargs)?.upcast(),
|
"centerbox" => build_center_box(bargs)?.upcast(),
|
||||||
"eventbox" => build_gtk_event_box(bargs)?.upcast(),
|
"eventbox" => build_gtk_event_box(bargs)?.upcast(),
|
||||||
"circular-progress" => build_circular_progress_bar(bargs)?.upcast(),
|
"circular-progress" => build_circular_progress_bar(bargs)?.upcast(),
|
||||||
|
"graph" => build_graph(bargs)?.upcast(),
|
||||||
"scale" => build_gtk_scale(bargs)?.upcast(),
|
"scale" => build_gtk_scale(bargs)?.upcast(),
|
||||||
"progress" => build_gtk_progress(bargs)?.upcast(),
|
"progress" => build_gtk_progress(bargs)?.upcast(),
|
||||||
"image" => build_gtk_image(bargs)?.upcast(),
|
"image" => build_gtk_image(bargs)?.upcast(),
|
||||||
|
@ -715,6 +716,37 @@ fn build_circular_progress_bar(bargs: &mut BuilderArgs) -> Result<CircProg> {
|
||||||
Ok(w)
|
Ok(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @widget graph
|
||||||
|
/// @desc A widget that displays a graph showing how a given value changes over time
|
||||||
|
fn build_graph(bargs: &mut BuilderArgs) -> Result<super::graph::Graph> {
|
||||||
|
let w = super::graph::Graph::new();
|
||||||
|
def_widget!(bargs, _g, w, {
|
||||||
|
// @prop value - the value, between 0 - 100
|
||||||
|
prop(value: as_f64) { w.set_property("value", &value)?; },
|
||||||
|
// @prop thickness - the thickness of the line
|
||||||
|
prop(thickness: as_f64) { w.set_property("thickness", &thickness)?; },
|
||||||
|
// @prop time-range - the range of time to show
|
||||||
|
prop(time_range: as_duration) { w.set_property("time-range", &(time_range.as_millis() as u64))?; },
|
||||||
|
// @prop min - the minimum value to show (defaults to 0 if value_max is provided)
|
||||||
|
// @prop max - the maximum value to show
|
||||||
|
prop(min: as_f64 = 0, max: as_f64 = 100) {
|
||||||
|
if min > max {
|
||||||
|
return Err(DiagError::new(gen_diagnostic!(
|
||||||
|
format!("Graph's min ({}) should never be higher than max ({})", min, max)
|
||||||
|
)).into());
|
||||||
|
}
|
||||||
|
w.set_property("min", &min)?;
|
||||||
|
w.set_property("max", &max)?;
|
||||||
|
},
|
||||||
|
// @prop dynamic - whether the y range should dynamically change based on value
|
||||||
|
prop(dynamic: as_bool) { w.set_property("dynamic", &dynamic)?; },
|
||||||
|
// @prop line-style - changes the look of the edges in the graph. Values: "miter" (default), "round",
|
||||||
|
// "bevel"
|
||||||
|
prop(line_style: as_string) { w.set_property("line-style", &line_style)?; },
|
||||||
|
});
|
||||||
|
Ok(w)
|
||||||
|
}
|
||||||
|
|
||||||
/// @var orientation - "vertical", "v", "horizontal", "h"
|
/// @var orientation - "vertical", "v", "horizontal", "h"
|
||||||
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
|
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
|
||||||
enum_parse! { "orientation", o,
|
enum_parse! { "orientation", o,
|
||||||
|
|
BIN
out.gif
Normal file
BIN
out.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Loading…
Add table
Reference in a new issue