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);