Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add circular progress bar widget for Gtk4 #282

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

ARKye03
Copy link

@ARKye03 ARKye03 commented Feb 10, 2025

Introduce a new circular progress bar widget for GTK4

Caution

It's necessary to link the math library to use this widget.

This explains gtk4/src/meson.build, new line:

link_args: ['-lm']

I don't use it directly, but the widget uses it under the hood.

This widget has 4 Widgets which are:

  • Progress Arc, the most important one:
    • double percentage [0..1], tells how much is drawn.
    • int line_width how thick the arc is
    • Gsk.LineCap line_cap the line cap style, Rounded, Square, Butt.
    • Gsk.FillRule fill_rule the fill rule, EvenOdd, Winding.
  • Radius Fill, the radius fill area.
    • bool radius_filled whether the radius is filled.
    • ProgressArc props are also used to draw the radius.
  • Center Fill, the center fill area.
    • bool center_filled whether the center is filled.
  • Child, just a normal Gtk.Widget. It can be anything.

Css nodes

circular-progress
├── progress-arc
├── center-fill
╰── radius-fill

Use the CSS color property to set the color of each node.

Example

With Gtk Blueprint:

$CircularProgressBar {
    percentage: bind template.speaker as <AstalWp.Endpoint>.volume;
    line-width: 5;
    line-cap: round;
    center-filled: false;
    radius-filled: true;

    child: Image {
    icon-name: bind template.speaker as <AstalWp.Endpoint>.volume-icon;
    };
}

cpb

The icon looks unimpressive, but it's not an issue with the CircularProgressBar.

@kotontrion
Copy link
Collaborator

I haven't looked at the widget implementation yet.
Please also add it to the jsx-runtime so you can use it in the gjs bindings using jsx. Note that in gtk4 astal makes use of Gtk.Builder to build the widget tree from jsx, so you need to inherit from Gtk.Buildable and at least implement its add_child method.

This would then also allow to this in blueprint instead of specifying the child prop:

$CircularProgressBar {
    percentage: bind template.speaker as <AstalWp.Endpoint>.volume;
    line-width: 5;
    line-cap: round;
    center-filled: false;
    radius-filled: true;

    Image {
        icon-name: bind template.speaker as <AstalWp.Endpoint>.volume-icon;
    };
}

@ARKye03
Copy link
Author

ARKye03 commented Feb 18, 2025

This would then also allow to this in blueprint instead of specifying the child prop:

It implements Gtk.Buildable, and Gtk.Buildable.add_child:

Astal.CircularProgressBar {
  percentage: bind template.speaker as <AstalWp.Endpoint>.volume;
  line-width: 5;
  line-cap: round;
  center-filled: true;
  radius-filled: true;
  fill-rule: winding;

  Image {
    icon-name: bind template.speaker as <AstalWp.Endpoint>.volume-icon;
  }
}

I have added GJS Binding, which I think they're correct, suggestion/corrections would be great. Valid snippet:

<CircularProgress
    lineCap={Gsk.LineCap.ROUND}
    lineWidth={5}
    percentage={0.5}
    centerFilled={true}
    radiusFilled={true}
    fillRule={Gsk.FillRule.EVEN_ODD}
    widthRequest={30}
><image
    iconName={"audio-volume-high-symbolic"}
/>
</CircularProgress>

…dding support to EventControllers

which is terrible obvious but I didn't noticed it before
Copy link
Collaborator

@kotontrion kotontrion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more things to add:

  • add circularprogress to the jsx runtime in the jsx-runtime.ts file.
  • fill rule and line cap properties require Gsk, so add this to the reexports in the index.ts file, so you can do
//this
import { App, Astal, Gtk, Gdk, Gsk } from "astal/gtk4"
//instead of
import Gsk from "gi://Gsk"

Also I would like to wait for Aylurs opinion on the Css style sheet and the Gizmo changes.

set_css_name("circular-progress");
Gtk.CssProvider css_provider = new Gtk.CssProvider();
css_provider.load_from_string(
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why you want to add some minimal css for this widget, but I don't really like having it in code here. What I did in libkompass is have one scss file containing some minimal definitions for all my widgets, which is loaded as a gresource when the lib is initiated, similar to what libadwaita does.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the minimal CSS; I didn't like it either, so better implement this later for a more generic use case.
Maybe another PR to load a custom CssProvider based on a hypothetical astal.scss.

/**
* Private widget that handles drawing the progress arc.
*/
private class ProgressArc : Gtk.Widget {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gtk does have a private class Gtk.Gizmo, which is basically just a Gtk.Widget, but takes function pointers for snapshot, allocate, measure, etc. Reimplementing this for astal could be quite useful for other custom widgets in the future as well. Such a class would allow you to have lambdas for those functions and would get rid of the duplicated properties in each subwidget and the update_geometry function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Feel free to ignore this comment, as this depends entirely on the Gizmo PR, and this might not be the time to talk about this.

This removes this redundancy, and no longer update_geometry methods exist. Nevertheless, the custom nodes (Progress, Radius, Center) are manually snapshotted, e.g., _progress_arc.snapshot(snapshot) instead of snapshot_child(_progress_arc, snapshot) which doesn't seem to work correctly, but for the _child it works as intended. It could and looks like a bug, but the Gizmo looks fine, and again, it's just the original gtkgizmo.c but in Vala.

Right now is:

if (center_filled) {
    _center_fill.snapshot(snapshot);
}

if (radius_filled) {
    _radius_fill.snapshot(snapshot);
}

_progress_arc.snapshot(snapshot);

if (_child != null) {
    snapshot_child(_child, snapshot);
}

But it could directly be:

if (center_filled) {
    draw_center_fill(snapshot);
}

if (radius_filled) {
    draw_radius_fill(snapshot);
}

draw_progress_arc(snapshot);

if (_child != null) {
    snapshot_child(_child, snapshot);
}

Calling the methods directly will allow the same logic with no extra overhead unless there is something I'm missing.

@ARKye03
Copy link
Author

ARKye03 commented Feb 20, 2025

I will implement all or most of the changes. Nevertheless, I will put this widget on standby, as a hypothetical Astal.Gizmo seems more suitable to create first. Then, I will use it to develop a circular progress widget.

@ARKye03
Copy link
Author

ARKye03 commented Feb 20, 2025

Changes made:

  • Added circularprogress to the JSX runtime in the jsx-runtime.ts file.
  • Added Gsk to the reexports in the index.ts file.
  • Removed cached values(radius, delta, width, height), unnecessary "optimization"
  • Removed private properties, bool _center_filled, bool _radius_filled, Gsk.LineCap _line_cap, Gsk.FillRule _fill_rule, and used public variable directly for most cases.
  • Removed queue_draw() calls on every setter and moved to the constructor, called on each property change.
  • Used base.add_child instead of handling event controllers manually.
  • Changed CSS Nodes to circularprogress, progress, radius, and center to make it consistent and simpler.
  • Removed unused measure() method as the CircularProgress uses BinLayout as LayoutManager
  • Removed LayoutManager to the different nodes of CircularProgress, as these don't and won't have children.
  • Changed and formatted the code style to make it subjectively more readable and consistent with the rest of the code.

Warning

This still doesn't use Astal.Gizmo

@ARKye03
Copy link
Author

ARKye03 commented Feb 20, 2025

Warning

Below will only happen if Astal.Gizmo or similar is merged.

I’ve created a variant of this widget using Astal.Gizmo from #293 , which you can find here. If the Gizmo gets approved, I’ll use that version and force merge the branch into this one.

@kotontrion
Copy link
Collaborator

I totally missed that this implementation is missing a feature which the gtk3 version has, and many people probably want.

The gtk3 version allows to specify a start and end point, so it does not draw a full circle, but only an arc.

You could then also make a function which draws an arc, which gets used by both the radius and progress fill snapshot functions to deduplicate code.

…larProgressBar to remove 'normalized' terminology
@ARKye03
Copy link
Author

ARKye03 commented Feb 22, 2025

I totally missed that this implementation is missing a feature which the gtk3 version has, and many people probably want.

The gtk3 version allows to specify a start and end point, so it does not draw a full circle, but only an arc.

You could then also make a function which draws an arc, which gets used by both the radius and progress fill snapshot functions to deduplicate code.

I have implemented the start_at and end_at properties, both works. Also implemented the inverted property, so now you can draw a progress bar clockwise and counter-clockwise. I have refactored the snapshot of the progressArc, to be more "readable".

Both start_at and end_at can have values from -1 to 1, where:

  • -1 => -2π
  • 0 => 0
  • 1 => 2π

Kinda obvious. Also, there can be a lot of refactoring, like you said having a draw arc for both radius and progress, could be possible, but some issues exist when trying to draw a complete arc, which is easily avoidable by drawing a circle, and cleaner. Using the Astal.Gizmo offers a lot of room for refactoring too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants