Circular progress widget (#328)

* Basic implementation of circle: works

* Attempts to draw child widget

* Drawing a ring with a child widget inside

* Rotation

* thickness for circular progressbar

* upgraded glib-gtk to 0.14

* Clockwise and counterclockwise cicular progress widget

* Allow specifying a background color

* queue draw on value update

* ring background and foreground

* Update circular_progressbar.rs

* implament default values for CircProgPriv

* Remove useless comment

* Renamed circle-progress to circular-progress

* Actually handle multiple children widgets

* Clipping the childrens, allowing for background colors

* Clean comments of despair

* renamed c to center

* Removed commented code

* fix overflowing children allocating space outside the circle

* Removed unused import

* Fix resizing issues and implement margin via cairo

* Cleanup

* make margins work with empty widgets

* Set css name for the custom widget

Co-authored-by: elkowar <5300871+elkowar@users.noreply.github.com>
This commit is contained in:
Pedro Burgos 2021-11-10 00:20:01 +01:00 committed by GitHub
parent 68e6d7b00d
commit 89d4dfda27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 282 additions and 12 deletions

3
Cargo.lock generated
View file

@ -414,6 +414,7 @@ dependencies = [
"anyhow",
"base64",
"bincode",
"cairo-rs",
"cairo-sys-rs",
"codespan-reporting",
"debug_stub_derive",
@ -427,7 +428,7 @@ dependencies = [
"gdk-pixbuf 0.9.0",
"gdkx11",
"gio 0.9.1",
"glib 0.10.3",
"glib 0.14.8",
"grass",
"gtk",
"gtk-layer-shell",

View file

@ -20,7 +20,9 @@ version = "0.14.0"
gtk = { version = "0.14", features = [ "v3_22" ] }
gdk = { version = "*", features = ["v3_22"] }
gio = { version = "*", features = ["v2_44"] }
glib = { version = "*", features = ["v2_44"] }
glib = { version = "0.14.8"}
cairo-rs = "0.14.0"
gdk-pixbuf = "0.9"

View file

@ -0,0 +1,248 @@
use anyhow::{anyhow, Result};
use glib::{object_subclass, wrapper};
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell;
use crate::error_handling_ctx;
wrapper! {
pub struct CircProg(ObjectSubclass<CircProgPriv>)
@extends gtk::Bin, gtk::Container, gtk::Widget;
}
pub struct CircProgPriv {
start_at: RefCell<f64>,
value: RefCell<f64>,
thickness: RefCell<f64>,
clockwise: RefCell<bool>,
content: RefCell<Option<gtk::Widget>>,
}
// This should match the default values from the ParamSpecs
impl Default for CircProgPriv {
fn default() -> Self {
CircProgPriv {
start_at: RefCell::new(0.0),
value: RefCell::new(0.0),
thickness: RefCell::new(1.0),
clockwise: RefCell::new(true),
content: RefCell::new(None),
}
}
}
impl ObjectImpl for CircProgPriv {
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",
"Thickness",
0f64,
100f64,
1f64,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_double(
"start-at",
"Starting at",
"Starting at",
0f64,
100f64,
0f64,
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_boolean("clockwise", "Clockwise", "Clockwise", true, 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" => {
self.value.replace(value.get().unwrap());
obj.queue_draw(); // Queue a draw call with the updated value
}
"thickness" => {
self.thickness.replace(value.get().unwrap());
}
"start-at" => {
self.start_at.replace(value.get().unwrap());
}
"clockwise" => {
self.clockwise.replace(value.get().unwrap());
}
x => panic!("Tried to set inexistant property of CircProg: {}", x,),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"value" => self.value.borrow().to_value(),
"start-at" => self.start_at.borrow().to_value(),
"thickness" => self.thickness.borrow().to_value(),
"clockwise" => self.clockwise.borrow().to_value(),
x => panic!("Tried to access inexistant property of CircProg: {}", x,),
}
}
}
#[object_subclass]
impl ObjectSubclass for CircProgPriv {
type ParentType = gtk::Bin;
type Type = CircProg;
const NAME: &'static str = "CircProg";
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("circular-progress");
}
}
impl CircProg {
pub fn new() -> Self {
glib::Object::new::<Self>(&[]).expect("Failed to create CircularProgress Widget")
}
}
impl ContainerImpl for CircProgPriv {
fn add(&self, container: &Self::Type, widget: &gtk::Widget) {
if let Some(content) = &*self.content.borrow() {
// TODO: Handle this error when populating children widgets instead
error_handling_ctx::print_error(anyhow!("Error, trying to add multiple children to a circular-progress widget"));
self.parent_remove(container, content);
}
self.parent_add(container, widget);
self.content.replace(Some(widget.clone()));
}
}
fn calc_widget_lowest_preferred_dimension(widget: &gtk::Widget) -> (i32, i32) {
let preferred_width = widget.preferred_width();
let preferred_height = widget.preferred_height();
let min_lowest = i32::min(preferred_width.0, preferred_height.0);
let natural_lowest = i32::min(preferred_width.1, preferred_height.1);
(min_lowest, natural_lowest)
}
impl BinImpl for CircProgPriv {}
impl WidgetImpl for CircProgPriv {
// We overwrite preferred_* so that overflowing content from the children gets cropped
// We return min(child_width, child_height)
fn preferred_width(&self, widget: &Self::Type) -> (i32, i32) {
let styles = widget.style_context();
let margin = styles.margin(gtk::StateFlags::NORMAL);
if let Some(child) = &*self.content.borrow() {
let (min_child, natural_child) = calc_widget_lowest_preferred_dimension(child);
(min_child + margin.right as i32 + margin.left as i32, natural_child + margin.right as i32 + margin.left as i32)
} else {
let empty_width = (2 * *self.thickness.borrow() as i32) + margin.right as i32 + margin.left as i32;
(empty_width, empty_width)
}
}
fn preferred_width_for_height(&self, widget: &Self::Type, _height: i32) -> (i32, i32) {
self.preferred_width(widget)
}
fn preferred_height(&self, widget: &Self::Type) -> (i32, i32) {
let styles = widget.style_context();
let margin = styles.margin(gtk::StateFlags::NORMAL);
if let Some(child) = &*self.content.borrow() {
let (min_child, natural_child) = calc_widget_lowest_preferred_dimension(child);
(min_child + margin.bottom as i32 + margin.top as i32, natural_child + margin.bottom as i32 + margin.top as i32)
} else {
let empty_height = (2 * *self.thickness.borrow() as i32) + margin.right as i32 + margin.left as i32;
(empty_height, empty_height)
}
}
fn preferred_height_for_width(&self, widget: &Self::Type, _width: i32) -> (i32, i32) {
self.preferred_height(widget)
}
fn draw(&self, widget: &Self::Type, cr: &cairo::Context) -> Inhibit {
let res: Result<()> = try {
let value = *self.value.borrow();
let start_at = *self.start_at.borrow() as f64;
let thickness = *self.thickness.borrow() as f64;
let clockwise = *self.clockwise.borrow() as bool;
let styles = widget.style_context();
let margin = styles.margin(gtk::StateFlags::NORMAL);
// Padding is not supported yet
let fg_color: gdk::RGBA = styles.color(gtk::StateFlags::NORMAL);
let bg_color: gdk::RGBA = styles.style_property_for_state("background-color", gtk::StateFlags::NORMAL).get()?;
let (start_angle, end_angle) =
if clockwise { (0.0, perc_to_rad(value as f64)) } else { (perc_to_rad(100.0 - value as f64), 0.0) };
let total_width = widget.allocated_width() as f64;
let total_height = widget.allocated_height() as f64;
let center = (total_width / 2.0, total_height / 2.0);
let circle_width = total_width - margin.left as f64 - margin.right as f64;
let circle_height = total_height as f64 - margin.top as f64 - margin.bottom as f64;
let outer_ring = f64::min(circle_width, circle_height) / 2.0;
let inner_ring = (f64::min(circle_width, circle_height) / 2.0) - thickness;
cr.save()?;
// Centering
cr.translate(center.0, center.1);
cr.rotate(perc_to_rad(start_at));
cr.translate(-center.0, -center.1);
// Background Ring
cr.move_to(center.0, center.1);
cr.arc(center.0, center.1, outer_ring, 0.0, perc_to_rad(100.0));
cr.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
cr.move_to(center.0, center.1);
cr.arc(center.0, center.1, inner_ring, 0.0, perc_to_rad(100.0));
cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other
cr.fill()?;
// Foreground Ring
cr.move_to(center.0, center.1);
cr.arc(center.0, center.1, outer_ring, start_angle, end_angle);
cr.set_source_rgba(fg_color.red, fg_color.green, fg_color.blue, fg_color.alpha);
cr.move_to(center.0, center.1);
cr.arc(center.0, center.1, inner_ring, start_angle, end_angle);
cr.set_fill_rule(cairo::FillRule::EvenOdd); // Substract one circle from the other
cr.fill()?;
cr.restore()?;
// Draw the children widget, clipping it to the inside
if let Some(child) = &*self.content.borrow() {
cr.save()?;
// Center circular clip
cr.arc(center.0, center.1, inner_ring + 1.0, 0.0, perc_to_rad(100.0));
cr.set_source_rgba(bg_color.red, 0.0, 0.0, bg_color.alpha);
cr.clip();
// Children widget
widget.propagate_draw(child, &cr);
cr.reset_clip();
cr.restore()?;
}
};
if let Err(error) = res {
error_handling_ctx::print_error(error)
};
gtk::Inhibit(false)
}
}
fn perc_to_rad(n: f64) -> f64 {
(n / 100f64) * 2f64 * std::f64::consts::PI
}

View file

@ -10,6 +10,7 @@ use yuck::{config::widget_definition::WidgetDefinition, gen_diagnostic};
use std::process::Command;
use widget_definitions::*;
pub mod circular_progressbar;
pub mod widget_definitions;
pub mod widget_node;
@ -131,13 +132,16 @@ macro_rules! resolve_block {
$args.eww_state.resolve(
$args.window_name,
attr_map,
::glib::clone!(@strong $gtk_widget => move |attrs| {
$(
let $attr_name = attrs.get( ::std::stringify!($attr_name) ).context("something went terribly wrong....")?.$typecast_func()?;
)*
$code
Ok(())
})
{
let $gtk_widget = $gtk_widget.clone();
move |attrs| {
$(
let $attr_name = attrs.get( ::std::stringify!($attr_name) ).context("something went terribly wrong....")?.$typecast_func()?;
)*
$code
Ok(())
}
}
);
}
})+

View file

@ -1,13 +1,12 @@
#![allow(clippy::option_map_unit_fn)]
use super::{run_command, BuilderArgs};
use super::{circular_progressbar::*, run_command, BuilderArgs};
use crate::{
enum_parse, error::DiagError, error_handling_ctx, eww_state, resolve_block, util::list_difference, widgets::widget_node,
};
use anyhow::*;
use codespan_reporting::diagnostic::Severity;
use gdk::NotifyType;
use glib;
use gtk::{self, prelude::*};
use gtk::{self, glib, prelude::*};
use itertools::Itertools;
use once_cell::sync::Lazy;
use std::{
@ -36,6 +35,7 @@ pub(super) fn widget_to_gtk_widget(bargs: &mut BuilderArgs) -> Result<gtk::Widge
"box" => build_gtk_box(bargs)?.upcast(),
"centerbox" => build_center_box(bargs)?.upcast(),
"eventbox" => build_gtk_event_box(bargs)?.upcast(),
"circular-progress" => build_circular_progress_bar(bargs)?.upcast(),
"scale" => build_gtk_scale(bargs)?.upcast(),
"progress" => build_gtk_progress(bargs)?.upcast(),
"image" => build_gtk_image(bargs)?.upcast(),
@ -751,6 +751,21 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result<gtk::Calendar> {
Ok(gtk_widget)
}
fn build_circular_progress_bar(bargs: &mut BuilderArgs) -> Result<CircProg> {
let w = CircProg::new();
resolve_block!(bargs, w, {
// @prop value - the value, between 0 - 100
prop(value: as_f64) { w.set_property("value", value)?; },
// @prop start-angle - the angle that the circle should start at
prop(start_at: as_f64) { w.set_property("start-at", start_at)?; },
// @prop thickness - the thickness of the circle
prop(thickness: as_f64) { w.set_property("thickness", thickness)?; },
// @prop clockwise - wether the progress bar spins clockwise or counter clockwise
prop(clockwise: as_bool) { w.set_property("clockwise", &clockwise)?; },
});
Ok(w)
}
/// @var orientation - "vertical", "v", "horizontal", "h"
fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
enum_parse! { "orientation", o,