st/spinner-content: Add ClutterContent for a loading indicator
Last cycle, libadwaita added a new Spinner widget whose visuals we should adapt. It loops extremely slowly though (a framerate of 60fps would require 3180(!) frames), which means our current asset-based implementation isn't well-suited for the job. Instead, reimplement the underlying GdkPaintable as ClutterContent. This does not only ensure consistency with libadwaita, but also gives us more flexibility by picking up style information from the widget the content is attached to. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3565>
This commit is contained in:
parent
2508325ad7
commit
9783f72d74
@ -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',
|
||||
|
446
src/st/st-spinner-content.c
Normal file
446
src/st/st-spinner-content.c
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "st-spinner-content.h"
|
||||
|
||||
#include <st-widget.h>
|
||||
|
||||
#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);
|
||||
}
|
27
src/st/st-spinner-content.h
Normal file
27
src/st/st-spinner-content.h
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <glib-object.h>
|
||||
#include <clutter/clutter.h>
|
||||
|
||||
#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);
|
Loading…
x
Reference in New Issue
Block a user