diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml
index db1f4e91d11f74cd428c69c71413f7462a2697da..30d9dc452e1bfbdd646ebabcee2d8841ba478626 100644
--- a/data/gnome-shell-theme.gresource.xml
+++ b/data/gnome-shell-theme.gresource.xml
@@ -8,8 +8,6 @@
gnome-shell-high-contrast.css
gnome-shell-start.svg
pad-osd.css
- process-working-light.svg
- process-working-dark.svg
workspace-placeholder.svg
diff --git a/data/theme/process-working-dark.svg b/data/theme/process-working-dark.svg
deleted file mode 100644
index 6c7ad64a6a6bee3d086d4fa7ebad14d546cb0266..0000000000000000000000000000000000000000
--- a/data/theme/process-working-dark.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/data/theme/process-working-light.svg b/data/theme/process-working-light.svg
deleted file mode 100644
index 903edde0fd997a31faa780074db1199eae3af9dc..0000000000000000000000000000000000000000
--- a/data/theme/process-working-light.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/js/ui/animation.js b/js/ui/animation.js
index feffc19ce001f3e2d9f3b3c418e8628357be0133..25b40d4b867fe722839952d6b44d32c03bcb123d 100644
--- a/js/ui/animation.js
+++ b/js/ui/animation.js
@@ -1,7 +1,6 @@
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
-import Gio from 'gi://Gio';
import St from 'gi://St';
import * as Params from '../misc/params.js';
@@ -130,48 +129,29 @@ class AnimatedIcon extends Animation {
});
export const Spinner = GObject.registerClass(
-class Spinner extends AnimatedIcon {
- _init(size, params) {
+class Spinner extends St.Widget {
+ constructor(size, params) {
params = Params.parse(params, {
animate: false,
hideOnStop: false,
});
- this._fileDark = Gio.File.new_for_uri(
- 'resource:///org/gnome/shell/theme/process-working-dark.svg');
- this._fileLight = Gio.File.new_for_uri(
- 'resource:///org/gnome/shell/theme/process-working-light.svg');
- super._init(this._fileDark, size);
-
- this.connect('style-changed', () => {
- const themeNode = this.get_theme_node();
- const textColor = themeNode.get_foreground_color();
- const [, , luminance] = textColor.to_hsl();
- const file = luminance > 0.5
- ? this._fileDark
- : this._fileLight;
- if (file !== this._file) {
- this._file = file;
- this._loadFile();
- }
+ super({
+ width: size,
+ height: size,
+ opacity: 0,
});
- this.opacity = 0;
this._animate = params.animate;
this._hideOnStop = params.hideOnStop;
this.visible = !this._hideOnStop;
}
- _onDestroy() {
- this._animate = false;
- super._onDestroy();
- }
-
play() {
this.remove_all_transitions();
+ this.set_content(new St.SpinnerContent());
this.show();
if (this._animate) {
- super.play();
this.ease({
opacity: 255,
delay: SPINNER_ANIMATION_DELAY,
@@ -180,7 +160,6 @@ class Spinner extends AnimatedIcon {
});
} else {
this.opacity = 255;
- super.play();
}
}
@@ -193,14 +172,14 @@ class Spinner extends AnimatedIcon {
duration: SPINNER_ANIMATION_TIME,
mode: Clutter.AnimationMode.LINEAR,
onComplete: () => {
- super.stop();
+ this.set_content(null);
if (this._hideOnStop)
this.hide();
},
});
} else {
this.opacity = 0;
- super.stop();
+ this.set_content(null);
if (this._hideOnStop)
this.hide();
diff --git a/src/st/meson.build b/src/st/meson.build
index 899fa788cfad3977f191bb63e3502fee0982a50a..4553cdbe2426473c6e98ff5a509cb6b4d909fd72 100644
--- a/src/st/meson.build
+++ b/src/st/meson.build
@@ -22,6 +22,7 @@ st_headers = [
'st-scroll-view-fade.h',
'st-settings.h',
'st-shadow.h',
+ 'st-spinner-content.h',
'st-texture-cache.h',
'st-theme.h',
'st-theme-context.h',
@@ -136,6 +137,7 @@ st_sources = [
'st-scroll-view-fade.c',
'st-settings.c',
'st-shadow.c',
+ 'st-spinner-content.c',
'st-texture-cache.c',
'st-theme.c',
'st-theme-context.c',
diff --git a/src/st/st-spinner-content.c b/src/st/st-spinner-content.c
new file mode 100644
index 0000000000000000000000000000000000000000..76e48b88ff6a2ffc8e0f5c16c9b1e70d6223abb5
--- /dev/null
+++ b/src/st/st-spinner-content.c
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2024 Red Hat
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+#include "st-spinner-content.h"
+
+#include
+
+#define MIN_RADIUS 8
+#define NAT_RADIUS 48
+#define SMALL_WIDTH 2.5
+#define LARGE_WIDTH 12
+#define SPIN_DURATION_MS 1200
+#define START_ANGLE (G_PI * 0.35)
+#define CIRCLE_OPACITY 0.15
+#define MIN_ARC_LENGTH (G_PI * 0.015)
+#define MAX_ARC_LENGTH (G_PI * 0.9)
+#define IDLE_DISTANCE (G_PI * 0.9)
+#define OVERLAP_DISTANCE (G_PI * 0.7)
+#define EXTEND_DISTANCE (G_PI * 1.1)
+#define CONTRACT_DISTANCE (G_PI * 1.35)
+/* How many full cycles it takes for the spinner to loop. Should be:
+ * (IDLE_DISTANCE + EXTEND_DISTANCE + CONTRACT_DISTANCE - OVERLAP_DISTANCE) * k,
+ * where k is an integer */
+#define N_CYCLES 53
+
+/**
+ * StSpinnerContent:
+ *
+ * A [iface@Clutter.Content] showing a loading spinner.
+ *
+ * `StSpinnerContent` size varies depending on the available space, but is
+ * capped at 96×96 pixels.
+ *
+ * It will be animated whenever it is attached to a mapped actor.
+ *
+ * If the attached actor is a [class@Widget], its style information will
+ * be used, similar to symbolic icons.
+ */
+
+struct _StSpinnerContent
+{
+ GObject parent_instance;
+
+ ClutterTimeline *timeline;
+ ClutterActor *actor;
+
+ CoglTexture *texture;
+ gboolean dirty;
+
+ CoglBitmap *buffer;
+};
+
+static void st_spinner_content_iface_init (ClutterContentInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (StSpinnerContent, st_spinner_content, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTENT,
+ st_spinner_content_iface_init))
+
+static void
+st_spinner_content_init (StSpinnerContent *spinner)
+{
+}
+
+static void
+st_spinner_content_class_init (StSpinnerContentClass *klass)
+{
+}
+
+static void
+actor_map_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ ClutterActor *actor = CLUTTER_ACTOR (object);
+ StSpinnerContent *spinner = user_data;
+
+ if (!spinner->timeline)
+ return;
+
+ if (clutter_actor_is_mapped (actor))
+ clutter_timeline_start (spinner->timeline);
+ else
+ clutter_timeline_stop (spinner->timeline);
+}
+
+static void
+new_frame_cb (ClutterTimeline *timeline,
+ int elapsed,
+ gpointer user_data)
+{
+ ClutterContent *content = user_data;
+
+ clutter_content_invalidate (content);
+}
+
+static void
+st_spinner_content_set_actor (StSpinnerContent *spinner,
+ ClutterActor *actor)
+{
+ if (actor == spinner->actor)
+ return;
+
+ if (spinner->actor)
+ {
+ g_clear_object (&spinner->timeline);
+ g_signal_handlers_disconnect_by_func (spinner->actor, actor_map_cb, spinner);
+ }
+
+ g_set_object (&spinner->actor, actor);
+
+ if (spinner->actor)
+ {
+ spinner->timeline = clutter_timeline_new_for_actor (actor,
+ SPIN_DURATION_MS * N_CYCLES);
+ clutter_timeline_set_repeat_count (spinner->timeline, -1);
+ clutter_timeline_set_progress_mode (spinner->timeline, CLUTTER_LINEAR);
+
+ g_signal_connect (spinner->timeline, "new-frame", G_CALLBACK (new_frame_cb), spinner);
+
+ if (clutter_actor_is_mapped (actor))
+ clutter_timeline_start (spinner->timeline);
+
+ g_signal_connect (actor, "notify::mapped", G_CALLBACK (actor_map_cb), spinner);
+ }
+
+ clutter_content_invalidate (CLUTTER_CONTENT (spinner));
+}
+
+static void
+st_spinner_content_attached (ClutterContent *content,
+ ClutterActor *actor)
+{
+ st_spinner_content_set_actor (ST_SPINNER_CONTENT (content), actor);
+}
+
+static void
+st_spinner_content_detached (ClutterContent *content,
+ ClutterActor *actor)
+{
+ st_spinner_content_set_actor (ST_SPINNER_CONTENT (content), NULL);
+}
+
+static void
+st_spinner_content_paint_content (ClutterContent *content,
+ ClutterActor *actor,
+ ClutterPaintNode *root,
+ ClutterPaintContext *paint_context)
+{
+ StSpinnerContent *spinner = ST_SPINNER_CONTENT (content);
+ ClutterPaintNode *node;
+
+ if (spinner->buffer == NULL)
+ return;
+
+ if (spinner->dirty)
+ g_clear_object (&spinner->texture);
+
+ if (spinner->texture == NULL)
+ spinner->texture = COGL_TEXTURE (cogl_texture_2d_new_from_bitmap (spinner->buffer));
+
+ if (spinner->texture == NULL)
+ return;
+
+ node = clutter_actor_create_texture_paint_node (actor, spinner->texture);
+ clutter_paint_node_set_static_name (node, "Spinner Content");
+ clutter_paint_node_add_child (root, node);
+ clutter_paint_node_unref (node);
+
+ spinner->dirty = FALSE;
+}
+
+#define LERP(a, b, t) (a + (b - a) * t)
+#define INVERSE_LERP(a, b, t) ((t - a) / (b - a))
+
+static double
+normalize_angle (double angle)
+{
+ while (angle < 0)
+ angle += G_PI * 2;
+
+ while (angle > G_PI * 2)
+ angle -= G_PI * 2;
+
+ return angle;
+}
+
+static inline double
+ease_in_out_sine (double t)
+{
+ return -0.5 * (cos (M_PI * t) - 1);
+}
+
+static double
+get_arc_start (double angle)
+{
+ double l = IDLE_DISTANCE + EXTEND_DISTANCE + CONTRACT_DISTANCE - OVERLAP_DISTANCE;
+ double t;
+
+ angle = fmod (angle, l);
+
+ if (angle > EXTEND_DISTANCE)
+ t = 1;
+ else
+ t = ease_in_out_sine (angle / EXTEND_DISTANCE);
+
+ return LERP (MIN_ARC_LENGTH, MAX_ARC_LENGTH, t) - angle * MAX_ARC_LENGTH / l;
+}
+
+static double
+get_arc_end (double angle)
+{
+ double l = IDLE_DISTANCE + EXTEND_DISTANCE + CONTRACT_DISTANCE - OVERLAP_DISTANCE;
+ double t;
+
+ angle = fmod (angle, l);
+
+ if (angle < EXTEND_DISTANCE - OVERLAP_DISTANCE)
+ t = 0;
+ else if (angle > l - IDLE_DISTANCE)
+ t = 1;
+ else
+ t = ease_in_out_sine ((angle - EXTEND_DISTANCE + OVERLAP_DISTANCE) / CONTRACT_DISTANCE);
+
+ return LERP (0, MAX_ARC_LENGTH - MIN_ARC_LENGTH, t) - angle * MAX_ARC_LENGTH / l;
+}
+
+static void
+st_spinner_content_draw_spinner (StSpinnerContent *spinner,
+ cairo_t *cr,
+ int width,
+ int height)
+{
+ CoglColor color;
+ double radius, line_width;
+ double progress, start_angle, end_angle;
+
+ g_assert (spinner->actor);
+
+ if (ST_IS_WIDGET (spinner->actor))
+ {
+ StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (spinner->actor));
+ st_theme_node_get_foreground_color (theme_node, &color);
+ }
+ else
+ {
+ cogl_color_init_from_4f (&color, 0., 0., 0., 1.);
+ }
+
+ radius = MIN (floorf (MIN (width, height) / 2), NAT_RADIUS);
+ line_width = LERP (SMALL_WIDTH, LARGE_WIDTH,
+ INVERSE_LERP (MIN_RADIUS, NAT_RADIUS, radius));
+ radius -= roundf (line_width / 2.);
+
+ if (radius < 0)
+ return;
+
+ cairo_translate (cr, roundf (width / 2), roundf(height / 2));
+ cairo_set_line_width (cr, line_width);
+
+ /* Circle */
+
+ cairo_save (cr);
+
+ cairo_set_source_rgba (cr,
+ color.red / 255.,
+ color.green / 255.,
+ color.blue / 255.,
+ color.alpha / 255. * CIRCLE_OPACITY);
+ cairo_arc (cr, 0, 0, radius, 0, 2 * M_PI);
+ cairo_stroke (cr);
+
+ cairo_restore (cr);
+
+ /* Moving part */
+
+ cairo_save (cr);
+
+ if (spinner->timeline)
+ progress = clutter_timeline_get_progress (spinner->timeline) * N_CYCLES * M_PI * 2;
+ else
+ progress = EXTEND_DISTANCE - OVERLAP_DISTANCE / 2;
+
+ start_angle = progress + get_arc_start (progress) + START_ANGLE;
+ end_angle = progress + get_arc_end (progress) + START_ANGLE;
+
+ start_angle = normalize_angle (start_angle);
+ end_angle = normalize_angle (end_angle);
+
+ cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_source_rgba (cr,
+ color.red / 255.,
+ color.green / 255.,
+ color.blue / 255.,
+ color.alpha / 255.);
+
+ cairo_arc (cr, 0, 0, radius, end_angle, start_angle);
+ cairo_stroke (cr);
+
+ cairo_restore (cr);
+}
+
+static void
+st_spinner_content_redraw (StSpinnerContent *spinner)
+{
+ ClutterActorBox allocation;
+ int width, height;
+ int real_width, real_height;
+ float scale_factor;
+ cairo_surface_t *surface;
+ gboolean mapped_buffer;
+ unsigned char *data;
+ CoglBuffer *buffer;
+ cairo_t *cr;
+
+ g_assert (spinner->actor && clutter_actor_is_mapped (spinner->actor));
+
+ spinner->dirty = TRUE;
+
+ clutter_actor_get_allocation_box (spinner->actor, &allocation);
+
+ width = (int)(0.5 + allocation.x2 - allocation.x1);
+ height = (int)(0.5 + allocation.y2 - allocation.y1);
+
+ scale_factor = clutter_actor_get_resource_scale (spinner->actor);
+
+ real_width = ceilf (width * scale_factor);
+ real_height = ceilf (height * scale_factor);
+
+ if (width == 0 || height == 0)
+ return;
+
+ if (spinner->buffer == NULL)
+ {
+ ClutterBackend *backend;
+ CoglContext *ctx;
+
+ backend = clutter_context_get_backend (clutter_actor_get_context (spinner->actor));
+ ctx = clutter_backend_get_cogl_context (backend);
+ spinner->buffer = cogl_bitmap_new_with_size (ctx,
+ real_width,
+ real_height,
+ COGL_PIXEL_FORMAT_CAIRO_ARGB32_COMPAT);
+ }
+
+ buffer = COGL_BUFFER (cogl_bitmap_get_buffer (spinner->buffer));
+ if (buffer == NULL)
+ return;
+ cogl_buffer_set_update_hint (buffer, COGL_BUFFER_UPDATE_HINT_DYNAMIC);
+
+ data = cogl_buffer_map (buffer,
+ COGL_BUFFER_ACCESS_READ_WRITE,
+ COGL_BUFFER_MAP_HINT_DISCARD);
+
+ if (data != NULL)
+ {
+ int bitmap_stride = cogl_bitmap_get_rowstride (spinner->buffer);
+
+ surface = cairo_image_surface_create_for_data (data,
+ CAIRO_FORMAT_ARGB32,
+ real_width,
+ real_height,
+ bitmap_stride);
+ mapped_buffer = TRUE;
+ }
+ else
+ {
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+ real_width,
+ real_height);
+
+ mapped_buffer = FALSE;
+ }
+
+ cairo_surface_set_device_scale (surface,
+ scale_factor,
+ scale_factor);
+
+ cr = cairo_create (surface);
+
+ cairo_save (cr);
+ cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint (cr);
+ cairo_restore (cr);
+
+ st_spinner_content_draw_spinner (spinner, cr, width, height);
+
+ cairo_destroy (cr);
+
+ if (mapped_buffer)
+ cogl_buffer_unmap (buffer);
+ else
+ {
+ int size = cairo_image_surface_get_stride (surface) * height;
+ cogl_buffer_set_data (buffer,
+ 0,
+ cairo_image_surface_get_data (surface),
+ size);
+ }
+
+ cairo_surface_destroy (surface);
+}
+
+static void
+st_spinner_content_invalidate (ClutterContent *content)
+{
+ StSpinnerContent *spinner = ST_SPINNER_CONTENT (content);
+
+ g_clear_object (&spinner->buffer);
+
+ if (spinner->actor && clutter_actor_is_mapped (spinner->actor))
+ st_spinner_content_redraw (spinner);
+}
+
+static void
+st_spinner_content_iface_init (ClutterContentInterface *iface)
+{
+ iface->paint_content = st_spinner_content_paint_content;
+ iface->attached = st_spinner_content_attached;
+ iface->detached = st_spinner_content_detached;
+ iface->invalidate = st_spinner_content_invalidate;
+}
+
+/**
+ * st_spinner_content_new:
+ *
+ * Returns: (transfer full): the newly created #StSpinnerContent content
+ */
+ClutterContent *
+st_spinner_content_new (void)
+{
+ return g_object_new (ST_TYPE_SPINNER_CONTENT, NULL);
+}
diff --git a/src/st/st-spinner-content.h b/src/st/st-spinner-content.h
new file mode 100644
index 0000000000000000000000000000000000000000..63bdcde92a706204f8356ff5c5c2c0ad5fdb6616
--- /dev/null
+++ b/src/st/st-spinner-content.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 Red Hat
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+#define ST_TYPE_SPINNER_CONTENT (st_spinner_content_get_type ())
+G_DECLARE_FINAL_TYPE (StSpinnerContent, st_spinner_content,
+ ST, SPINNER_CONTENT, GObject)
+
+ClutterContent *st_spinner_content_new (void);