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

Introduce CircularProgressbar #2274

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/Widgets/CircularProgressbar.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// TODO: Copyright

public class Gala.CircularProgressbar : Clutter.Actor {
public uint radius { get; set; default = 0; }
public uint duration { get; set; default = 0; }
public float monitor_scaling_factor { get; set; default = 1.0f; }
public double angle { get; set; default = START_ANGLE; }

private const double BACKGROUND_OPACITY = 0.7;
private const int BORDER_WIDTH_PX = 1;
private const double START_ANGLE = 3 * Math.PI_2;

private Cogl.Pipeline pipeline;
private Clutter.PropertyTransition transition;
private Cairo.ImageSurface? surface;
private uint diameter { get { return radius * 2; }}

construct {
visible = false;
reactive = false;

#if HAS_MUTTER47
unowned var backend = context.get_backend ();
#else
unowned var backend = Clutter.get_default_backend ();
#endif
pipeline = new Cogl.Pipeline (backend.get_cogl_context ());

transition = new Clutter.PropertyTransition ("angle");
transition.set_progress_mode (Clutter.AnimationMode.LINEAR);
transition.set_animatable (this);
transition.set_from_value (START_ANGLE);
transition.set_to_value (START_ANGLE + 2 * Math.PI);

transition.new_frame.connect (() => {
queue_redraw ();
});
}

public void start () {
visible = true;
width = diameter;
height = diameter;

if (surface == null || surface.get_width () != diameter || surface.get_height () != diameter) {
surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, (int) diameter, (int) diameter);
}

transition.duration = duration;
transition.start ();
}

public void reset () {
visible = false;
transition.stop ();
}

public override void paint (Clutter.PaintContext context) {
if (angle == START_ANGLE) {
return;
}

var rgba = Drawing.StyleManager.get_instance ().theme_accent_color;

/* Don't use alpha from the stylesheet to ensure contrast */
var stroke_color = new Cairo.Pattern.rgb (rgba.red, rgba.green, rgba.blue);
var fill_color = new Cairo.Pattern.rgba (rgba.red, rgba.green, rgba.blue, BACKGROUND_OPACITY);

var border_width = InternalUtils.scale_to_int (BORDER_WIDTH_PX, monitor_scaling_factor);

var cr = new Cairo.Context (surface);

// Clear the surface
cr.save ();
cr.set_source_rgba (0, 0, 0, 0);
cr.set_operator (Cairo.Operator.SOURCE);
cr.paint ();
cr.restore ();

cr.set_line_cap (Cairo.LineCap.ROUND);
cr.set_line_join (Cairo.LineJoin.ROUND);
cr.translate (radius, radius);

cr.move_to (0, 0);
cr.arc (0, 0, radius - border_width, START_ANGLE, angle);
cr.line_to (0, 0);
cr.close_path ();

cr.set_line_width (0);
cr.set_source (fill_color);
cr.fill_preserve ();

cr.set_line_width (border_width);
cr.set_source (stroke_color);
cr.stroke ();

var cogl_context = context.get_framebuffer ().get_context ();

try {
var texture = new Cogl.Texture2D.from_data (
cogl_context,
(int) diameter, (int) diameter,
Cogl.PixelFormat.BGRA_8888_PRE,
surface.get_stride (), surface.get_data ()
);

pipeline.set_layer_texture (0, texture);

context.get_framebuffer ().draw_rectangle (pipeline, 0, 0, diameter, diameter);
} catch (Error e) {
warning ("CircularProgressbar: Couldn't create new texture: %s", e.message);
}

base.paint (context);
}
}
136 changes: 17 additions & 119 deletions src/Widgets/DwellClickTimer.vala
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,15 @@
* SPDX-FileCopyrightText: 2020, 2025 elementary, Inc. (https://elementary.io)
*/

public class Gala.DwellClickTimer : Clutter.Actor, Clutter.Animatable {
private const double BACKGROUND_OPACITY = 0.7;
private const int BORDER_WIDTH_PX = 1;

private const double START_ANGLE = 3 * Math.PI / 2;

/**
* Delay, in milliseconds, before showing the animation.
* libinput uses a timeout of 180ms when tapping is enabled. Use that value plus a safety
* margin so the animation is never displayed when tapping.
*/
private const double DELAY_TIMEOUT = 185;

public class Gala.DwellClickTimer : Clutter.Actor {
private float scaling_factor = 1.0f;
private int cursor_size = 24;
private uint cursor_size = 24;

private Cogl.Pipeline pipeline;
private Clutter.PropertyTransition transition;
private Cairo.Pattern stroke_color;
private Cairo.Pattern fill_color;
private CircularProgressbar progressbar;
private GLib.Settings interface_settings;
private Cairo.ImageSurface surface;

public Meta.Display display { get; construct; }

public double angle { get; set; }

public DwellClickTimer (Meta.Display display) {
Object (display: display);
}
Expand All @@ -38,32 +20,21 @@ public class Gala.DwellClickTimer : Clutter.Actor, Clutter.Animatable {
visible = false;
reactive = false;

interface_settings = new GLib.Settings ("org.gnome.desktop.interface");

progressbar = new CircularProgressbar ();
add_child (progressbar);

#if HAS_MUTTER47
unowned var backend = context.get_backend ();
#else
unowned var backend = Clutter.get_default_backend ();
#endif

pipeline = new Cogl.Pipeline (backend.get_cogl_context ());

transition = new Clutter.PropertyTransition ("angle");
transition.set_progress_mode (Clutter.AnimationMode.EASE_OUT_QUAD);
transition.set_animatable (this);
transition.set_from_value (START_ANGLE);
transition.set_to_value (START_ANGLE + (2 * Math.PI));

transition.new_frame.connect (() => {
queue_redraw ();
});

interface_settings = new GLib.Settings ("org.gnome.desktop.interface");

var seat = backend.get_default_seat ();
seat.set_pointer_a11y_dwell_click_type (Clutter.PointerA11yDwellClickType.PRIMARY);

seat.ptr_a11y_timeout_started.connect ((device, type, timeout) => {
var scale = display.get_monitor_scale (display.get_current_monitor ());
update_cursor_size (scale);
update_cursor_size ();

unowned var tracker = display.get_cursor_tracker ();
Graphene.Point coords = {};
Expand All @@ -72,97 +43,24 @@ public class Gala.DwellClickTimer : Clutter.Actor, Clutter.Animatable {
x = coords.x - (width / 2);
y = coords.y - (width / 2);

transition.set_duration (timeout);
visible = true;
transition.start ();
progressbar.duration = timeout;
progressbar.start ();
});

seat.ptr_a11y_timeout_stopped.connect ((device, type, clicked) => {
transition.stop ();
visible = false;
progressbar.reset ();
});
}

private void update_cursor_size (float scale) {
scaling_factor = scale;
private void update_cursor_size () {
scaling_factor = display.get_monitor_scale (display.get_current_monitor ());

cursor_size = (int) (interface_settings.get_int ("cursor-size") * scaling_factor * 1.25);

if (surface == null || surface.get_width () != cursor_size || surface.get_height () != cursor_size) {
surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, cursor_size, cursor_size);
}
cursor_size = (uint) (interface_settings.get_int ("cursor-size") * scaling_factor * 1.25);
var radius = cursor_size / 2;
progressbar.radius = radius;

set_size (cursor_size, cursor_size);
}

public override void paint (Clutter.PaintContext context) {
if (angle == 0) {
return;
}

var rgba = Drawing.StyleManager.get_instance ().theme_accent_color;

/* Don't use alpha from the stylesheet to ensure contrast */
stroke_color = new Cairo.Pattern.rgb (rgba.red, rgba.green, rgba.blue);
fill_color = new Cairo.Pattern.rgba (rgba.red, rgba.green, rgba.blue, BACKGROUND_OPACITY);

var radius = int.min (cursor_size / 2, cursor_size / 2);
var end_angle = START_ANGLE + angle;
var border_width = InternalUtils.scale_to_int (BORDER_WIDTH_PX, scaling_factor);

var cr = new Cairo.Context (surface);

// Clear the surface
cr.save ();
cr.set_source_rgba (0, 0, 0, 0);
cr.set_operator (Cairo.Operator.SOURCE);
cr.paint ();
cr.restore ();

cr.set_line_cap (Cairo.LineCap.ROUND);
cr.set_line_join (Cairo.LineJoin.ROUND);
cr.translate (cursor_size / 2, cursor_size / 2);

cr.move_to (0, 0);
cr.arc (0, 0, radius - border_width, START_ANGLE, end_angle);
cr.line_to (0, 0);
cr.close_path ();

cr.set_line_width (0);
cr.set_source (fill_color);
cr.fill_preserve ();

cr.set_line_width (border_width);
cr.set_source (stroke_color);
cr.stroke ();

var cogl_context = context.get_framebuffer ().get_context ();

try {
var texture = new Cogl.Texture2D.from_data (cogl_context, cursor_size, cursor_size, Cogl.PixelFormat.BGRA_8888_PRE,
surface.get_stride (), surface.get_data ());

pipeline.set_layer_texture (0, texture);

context.get_framebuffer ().draw_rectangle (pipeline, 0, 0, cursor_size, cursor_size);
} catch (Error e) {}

base.paint (context);
}

public bool interpolate_value (string property_name, Clutter.Interval interval, double progress, out Value @value) {
if (property_name == "angle") {
@value = 0;

var elapsed_time = transition.get_elapsed_time ();
if (elapsed_time > DELAY_TIMEOUT) {
double delayed_progress = (elapsed_time - DELAY_TIMEOUT) / (transition.duration - DELAY_TIMEOUT);
@value = (delayed_progress * 2 * Math.PI);
}

return true;
}

return base.interpolate_value (property_name, interval, progress, out @value);
}
}
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ gala_bin_sources = files(
'ShellClients/PositionedWindow.vala',
'ShellClients/ShellClientsManager.vala',
'ShellClients/ShellWindow.vala',
'Widgets/CircularProgressbar.vala',
'Widgets/DwellClickTimer.vala',
'Widgets/IconGroup.vala',
'Widgets/IconGroupContainer.vala',
Expand Down