/*
* Copyright © 2025 Red Hat , Inc
*
* SPDX - License - Identifier : LGPL - 2 . 1 - or - later
*
* This library 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 library is distributed in the hope that 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 library . If not , see < http : //www.gnu.org/licenses/>.
*
* Authors : Matthias Clasen < mclasen @ redhat . com >
*/
#include "config.h"
#include "gtksvgprivate.h"
#include "gtksvgrendererprivate.h"
#include "gtksvgserializeprivate.h"
#include "gtksvgparserprivate.h"
#include "gtkenums.h"
#include "gtksymbolicpaintable.h"
#include "gtktypebuiltins.h"
#include "gtkmarshalers.h"
#include "gtk/css/gtkcssparserprivate.h"
#include "gtk/css/gtkcssdataurlprivate.h"
#include "gtksnapshotprivate.h"
#include "gsk/gskarithmeticnodeprivate.h"
#include "gsk/gskblendnodeprivate.h"
#include "gsk/gskcolormatrixnodeprivate.h"
#include "gsk/gskcolornodeprivate.h"
#include "gsk/gskcomponenttransfernodeprivate.h"
#include "gsk/gskdisplacementnodeprivate.h"
#include "gsk/gskisolationnodeprivate.h"
#include "gsk/gskrepeatnodeprivate.h"
#include "gsk/gskpathprivate.h"
#include "gsk/gskcontourprivate.h"
#include "gsk/gskrectprivate.h"
#include "gsk/gskroundedrectprivate.h"
#include "gtk/gtkcssselectorprivate.h"
#include "gtksvgenumtypes.h"
#include "gtksvgutilsprivate.h"
#include "gtksvgstringutilsprivate.h"
#include "gtksvgvalueprivate.h"
#include "gtksvgkeywordprivate.h"
#include "gtksvgnumberprivate.h"
#include "gtksvgnumbersprivate.h"
#include "gtksvgstringprivate.h"
#include "gtksvgstringlistprivate.h"
#include "gtksvgenumprivate.h"
#include "gtksvgfilterrefprivate.h"
#include "gtksvgtransformprivate.h"
#include "gtksvgcolorprivate.h"
#include "gtksvgpaintprivate.h"
#include "gtksvgfilterfunctionsprivate.h"
#include "gtksvgdasharrayprivate.h"
#include "gtksvgpathprivate.h"
#include "gtksvgpathdataprivate.h"
#include "gtksvgclipprivate.h"
#include "gtksvgmaskprivate.h"
#include "gtksvgviewboxprivate.h"
#include "gtksvgcontentfitprivate.h"
#include "gtksvgorientprivate.h"
#include "gtksvglanguageprivate.h"
#include "gtksvghrefprivate.h"
#include "gtksvgtextdecorationprivate.h"
#include "gtksvgpathutilsprivate.h"
#include "gtksvgcolorutilsprivate.h"
#include "gtksvgpangoutilsprivate.h"
#include "gtksvglocationprivate.h"
#include "gtksvgerrorprivate.h"
#include "gtksvgelementtypeprivate.h"
#include "gtksvgpropertyprivate.h"
#include "gtksvgfiltertypeprivate.h"
#include "gtksvgfilterprivate.h"
#include "gtksvgcolorstopprivate.h"
#include "gtksvgelementinternal.h"
#include "gtksvganimationprivate.h"
#include "gtksvgtimespecprivate.h"
#include "gtksvggpaprivate.h"
#include "gtksvgmediaqueryprivate.h"
#include <glib/gstdio.h>
#include <stdint.h>
#ifdef HAVE_PANGOFT
#include <pango/pangofc-fontmap.h>
#endif
#include "gdk/gdkrgbaprivate.h"
#ifdef DEBUG
extern int64_t time_base;
#endif
/**
* GtkSvg :
*
* A paintable implementation that renders SVG , with animations .
*
* ` GtkSvg ` objects are created by parsing a subset of SVG ,
* including SVG animations .
*
* ` GtkSvg ` fills or strokes paths with symbolic or fixed colors .
* It can have multiple states , and paths can be included in a subset
* of the states . States can have animations , and the transition
* between different states can also be animated .
*
* To show a static SVG image , it is enough to load the
* the SVG and use it like any other paintable .
*
* To play an SVG animation , use [ method @ Gtk . Svg . set_frame_clock ]
* to connect the paintable to a frame clock , and call
* [ method @ Gtk . Svg . play ] after loading the SVG . The animation can
* be paused using [ method @ Gtk . Svg . pause ] .
*
* To set the current state , use [ method @ Gtk . Svg . set_state ] .
*
*
* # # Error handling
*
* Loading an SVG into ` GtkSvg ` will always produce a ( possibly empty )
* paintable . GTK will drop things that it can ' t handle and try to make
* sense of the rest .
*
* To track errors during parsing or rendering , connect to the
* [ signal @ Gtk . Svg : : error ] signal .
*
* For parsing errors in the ` GTK_SVG_ERROR ` domain , the functions
* [ func @ Gtk . SvgError . get_start ] , [ func @ Gtk . SvgError . get_end ] ,
* [ func @ Gtk . SvgError . get_element ] and [ func @ Gtk . SvgError . get_attribute ]
* can be used to obtain information about where the error occurred .
*
*
* # # The supported subset of SVG
*
* The paintable supports much of [ SVG 2 ] ( https : //svgwg.org/svg2-draft/),
* including [ animations ] ( https : //svgwg.org/specs/animations/), with some
* exceptions .
*
* Among the graphical elements , ` < textPath > ` and ` < foreignObject > ` are
* not supported .
*
* In the ` < filter > ` element , the following primitives are not supported :
* feConvolveMatrix , feDiffuseLighting , feMorphology , feSpecularLighting .
*
* Support for the ` mask ` attribute is limited to just a url referring to
* the ` < mask > ` element by ID .
*
* In animation elements , the parsing of ` begin ` and ` end ` attributes is
* limited , and the ` min ` and ` max ` attributes are not supported .
*
* The interactive aspects of SVG are supported by [ class @ Gtk . SvgWidget ] .
*
*
* # # SVG Extensions
*
* The paintable supports a number of [ custom attributes ] ( icon - format . html )
* that offer a convenient way to define states , transitions and animations .
* For example ,
*
* < circle cx = ' 5 ' cy = ' 5 ' r = ' 5 '
* gpa : states = ' 0 1 '
* gpa : animation - type = ' automatic '
* gpa : animation - direction = ' segment '
* gpa : animation - duration = ' 600 ms ' / >
*
* defines the circle to be shown in states 0 and 1 , and animates a segment
* of the circle .
*
*
* < image src = " svg - renderer1 . svg " >
*
* Note that the generated animations are implemented using standard
* SVG attributes ( ` visibility ` , ` stroke - dasharray , ` stroke - dashoffset ` ,
* ` pathLength ` and ` filter ` ) . Setting these attributes in your SVG
* is therefore going to interfere with generated animations .
*
* To connect general SVG animations to the states of the paintable ,
* use the custom ` StateChange ( . . . ) ` condition in the ` begin ` and ` end `
* attributes of SVG animation elements . For example ,
*
* < animate href = ' path1 '
* attributeName = ' fill '
* begin = ' StateChange ( 1 2 3 , 0 ) '
* dur = ' 300 ms '
* fill = ' freeze '
* from = ' black '
* to = ' magenta ' / >
*
* will make the fill color of path1 transition from black to
* magenta when the renderer enters state 0 from states 1 , 2 , or 3 .
*
* < image src = " svg - renderer2 . svg " >
*
* The ` StateChange ( . . . ) ` condition triggers for upcoming state changes
* as well , to support fade - out transitions . For example ,
*
* < animate href = ' path1 '
* attributeName = ' opacity '
* begin = ' StateChange ( 0 , 1 2 3 ) - 300 ms '
* dur = ' 300 ms '
* fill = ' freeze '
* from = ' 1 '
* to = ' 0 ' / >
*
* will start a fade - out of path1 300 ms before a transition from state
* 0 to 1 , 2 or 3 .
*
* States can be specified numerically , or by name . It is also possible
* to say ` not STATE ` instead of explictly listing all states that are
* different from ` STATE ` .
*
* In addition to the ` gpa : fill ` and ` gpa : stroke ` attributes , symbolic
* colors can also be specified as a custom paint server reference ,
* like this : ` url ( # gpa : warning ) ` . This works in ` fill ` and ` stroke `
* attributes , but also when specifying colors in SVG animation
* attributes like ` to ` or ` values ` .
*
* Note that the SVG syntax allows for a fallback RGB color to be
* specified after the url , for compatibility with other SVG consumers :
*
* fill = ' url ( # gpa : warning ) orange '
*
* GtkSvg also allows to refer to symbolic colors like system colors
* in CSS , with names like SymbolicForeground , SymbolicSuccess , etc .
* These can be used whenever a color is required .
*
* In contrast to SVG 1 . 1 and 2 . 0 , we allow the ` transform ` attribute
* to be animated with ` < animate > ` .
*
*
* Since : 4 . 22
*/
/* {{{ Errors */
static unsigned int error_signal;
void
gtk_svg_emit_error (GtkSvg *svg,
const GError *error)
{
g_signal_emit (svg, error_signal, 0 , error);
}
void
gtk_svg_invalid_attribute (GtkSvg *self,
GMarkupParseContext *context,
const char **attr_names,
const char *attr_name,
const char *format,
...)
{
GError *error = NULL;
GtkSvgLocation start, end;
if (format)
{
va_list args;
va_start (args, format);
error = g_error_new_valist (GTK_SVG_ERROR,
GTK_SVG_ERROR_INVALID_ATTRIBUTE,
format, args);
va_end (args);
}
else
{
error = g_error_new (GTK_SVG_ERROR,
GTK_SVG_ERROR_INVALID_ATTRIBUTE,
"Invalid attribute: %s" , attr_name);
}
gtk_svg_error_set_input (error, "svg" );
gtk_svg_error_set_element (error, g_markup_parse_context_get_element (context));
gtk_svg_error_set_attribute (error, attr_name);
gtk_svg_location_init_tag_range (&start, &end, context);
if (attr_names && attr_name)
{
for (unsigned int i = 0 ; attr_names[i]; i++)
{
if (strcmp (attr_names[i], attr_name) == 0 )
{
gtk_svg_location_init_attr_range (&start, &end, context, i);
break ;
}
}
}
gtk_svg_error_set_location (error, &start, &end);
gtk_svg_emit_error (self, error);
g_error_free (error);
}
void
gtk_svg_invalid_reference (GtkSvg *self,
const char *format,
...)
{
va_list args;
GError *error;
va_start (args, format);
error = g_error_new_valist (GTK_SVG_ERROR,
GTK_SVG_ERROR_INVALID_REFERENCE,
format, args);
va_end (args);
gtk_svg_emit_error (self, error);
g_error_free (error);
}
void
gtk_svg_update_error (GtkSvg *self,
const char *format,
...)
{
va_list args;
GError *error;
va_start (args, format);
error = g_error_new_valist (GTK_SVG_ERROR,
GTK_SVG_ERROR_FAILED_UPDATE,
format, args);
va_end (args);
gtk_svg_emit_error (self, error);
g_error_free (error);
}
void
gtk_svg_rendering_error (GtkSvg *self,
const char *format,
...)
{
va_list args;
GError *error;
va_start (args, format);
error = g_error_new_valist (GTK_SVG_ERROR,
GTK_SVG_ERROR_FAILED_RENDERING,
format, args);
va_end (args);
gtk_svg_emit_error (self, error);
g_error_free (error);
}
/* }}} */
/* {{{ Helpers */
static float
ease (double *params,
double progress)
{
static const double epsilon = 0 .00001 ;
double tmin, t, tmax;
double x1, y1, x2, y2;
x1 = params[0 ];
y1 = params[1 ];
x2 = params[2 ];
y2 = params[3 ];
if (progress <= 0 )
return 0 ;
if (progress >= 1 )
return 1 ;
tmin = 0 .0 ;
tmax = 1 .0 ;
t = progress;
while (tmin < tmax)
{
double sample;
sample = (((1 + 3 * x1 - 3 * x2) * t
+ -6 * x1 + 3 * x2) * t
+ 3 * x1 ) * t;
if (fabs (sample - progress) < epsilon)
break ;
if (progress > sample)
tmin = t;
else
tmax = t;
t = (tmax + tmin) * .5 ;
}
return (((1 + 3 * y1 - 3 * y2) * t
+ -6 * y1 + 3 * y2) * t
+ 3 * y1 ) * t;
}
static gboolean
strv_unique (GStrv strv)
{
if (strv)
for (unsigned int i = 0 ; strv[i]; i++)
for (unsigned int j = i + 1 ; strv[j]; j++)
if (strcmp (strv[i], strv[j]) == 0 )
return FALSE ;
return TRUE ;
}
static int64_t
get_current_time (GtkSvg *self)
{
if (self->clock && self->playing && self->load_time != INDEFINITE)
return MAX (self->current_time, gdk_frame_clock_get_frame_time (self->clock));
return self->current_time;
}
/* }}} */
/* {{{ Image loading */
static GdkTexture *
load_texture (const char *string,
gboolean allow_external,
gboolean *cache_this,
GError **error)
{
GdkTexture *texture = NULL;
*cache_this = TRUE ;
if (g_str_has_prefix (string, "data:" ))
{
GBytes *bytes;
bytes = gtk_css_data_url_parse (string, NULL, error);
if (bytes == NULL)
return NULL;
*cache_this = FALSE ;
texture = gdk_texture_new_from_bytes (bytes, error);
g_bytes_unref (bytes);
}
else if (g_str_has_prefix (string, "resource:" ))
{
texture = gdk_texture_new_from_resource (string + strlen ("resource:" ));
}
else if (allow_external && g_str_has_prefix (string, "file:" ))
{
GFile *file = g_file_new_for_uri (string);
texture = gdk_texture_new_from_file (file, error);
g_object_unref (file);
}
else if (allow_external)
{
texture = gdk_texture_new_from_filename (string, error);
}
else
{
g_set_error (error, GTK_SVG_ERROR, GTK_SVG_ERROR_FEATURE_DISABLED,
"Not loading texture from %s: External resources disabled" , string);
}
return texture;
}
GdkTexture *
get_texture (GtkSvg *svg,
const char *string,
GError **error)
{
GdkTexture *texture;
texture = g_hash_table_lookup (svg->images, string);
if (!texture)
{
gboolean cache_this;
texture = load_texture (string,
(svg->features & GTK_SVG_EXTERNAL_RESOURCES) != 0 ,
&cache_this,
error);
if (texture && cache_this)
g_hash_table_insert (svg->images, g_strdup (string), g_object_ref (texture));
}
return texture;
}
/* }}} */
/* {{{ Font handling */
static void
delete_file (gpointer data)
{
char *path = data;
g_remove (path);
g_free (path);
}
void
ensure_fontmap (GtkSvg *svg)
{
if (svg->fontmap)
return ;
svg->fontmap = pango_cairo_font_map_new ();
if ((svg->features & GTK_SVG_SYSTEM_RESOURCES) == 0 )
{
#ifdef HAVE_PANGOFT
/* This isolates us from the system fonts */
FcConfig *config;
config = FcConfigCreate ();
pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (svg->fontmap), config);
FcConfigDestroy (config);
#endif
}
svg->font_files = g_ptr_array_new_with_free_func (delete_file);
}
static gboolean
add_font_from_file (GtkSvg *svg,
const char *path,
GError **error)
{
ensure_fontmap (svg);
if (!pango_font_map_add_font_file (svg->fontmap, path, error))
return FALSE ;
g_ptr_array_add (svg->font_files, g_strdup (path));
return TRUE ;
}
static gboolean
add_font_from_bytes (GtkSvg *svg,
GBytes *bytes,
GError **error)
{
GFile *file;
GIOStream *iostream;
GOutputStream *ostream;
file = g_file_new_tmp ("gtk4-font-XXXXXX.ttf" , (GFileIOStream **) &iostream, error);
if (!file)
return FALSE ;
ostream = g_io_stream_get_output_stream (iostream);
if (g_output_stream_write_bytes (ostream, bytes, NULL, error) == -1 )
{
g_object_unref (file);
g_object_unref (iostream);
return FALSE ;
}
g_io_stream_close (iostream, NULL, NULL);
g_object_unref (iostream);
if (!add_font_from_file (svg, g_file_peek_path (file), error))
{
g_file_delete (file, NULL, NULL);
g_object_unref (file);
return FALSE ;
}
g_object_unref (file);
return TRUE ;
}
gboolean
add_font_from_url (GtkSvg *svg,
GMarkupParseContext *context,
const char **attr_names,
const char *attr_name,
const char *url)
{
char *scheme;
GBytes *bytes;
char *mimetype = NULL;
GError *error = NULL;
scheme = g_uri_parse_scheme (url);
if (!scheme || g_ascii_strcasecmp (scheme, "data" ) != 0 )
{
char *start = g_utf8_make_valid (url, 20 );
gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
"Unsupported uri scheme for font: %s…" , start);
g_free (start);
return FALSE ;
}
g_free (scheme);
bytes = gtk_css_data_url_parse (url, &mimetype, &error);
if (!bytes)
{
gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
"Can't parse font data: %s" , error->message);
g_error_free (error);
g_free (mimetype);
return FALSE ;
}
if (strcmp (mimetype, "font/ttf" ) != 0 )
{
gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
"Unsupported mime type for font data: %s" , mimetype);
g_bytes_unref (bytes);
g_free (mimetype);
return FALSE ;
}
g_free (mimetype);
if (!add_font_from_bytes (svg, bytes, &error))
{
g_bytes_unref (bytes);
gtk_svg_invalid_attribute (svg, context, attr_names, attr_name,
"Failed to add font: %s" , error->message);
g_error_free (error);
return FALSE ;
}
g_bytes_unref (bytes);
return TRUE ;
}
/* }}} */
/* {{{ Animation */
static int64_t
get_last_start (SvgAnimation *a)
{
if (a->status == ANIMATION_STATUS_DONE)
return a->previous.begin;
else
return a->current.begin;
}
static int
compare_anim (gconstpointer a,
gconstpointer b)
{
SvgAnimation *a1 = (SvgAnimation *) a;
SvgAnimation *a2 = (SvgAnimation *) b;
int64_t start1, start2;
/* It doesn't matter, but we sort by attributes to get
* a predictable order
*/
if (a1->attr < a2->attr)
return -1 ;
else if (a1->attr > a2->attr)
return 1 ;
/* The situation with animateTransform vs. animateMotion
* is special : they don ' t add to each other , and
* the accumulate animateMotion transform is always
* applied after the accumulate animateTransform .
*/
if (a1->attr == SVG_PROPERTY_TRANSFORM)
{
if (a1->type == ANIMATION_TYPE_MOTION && a2->type != ANIMATION_TYPE_MOTION)
return 1 ;
else if (a1->type != ANIMATION_TYPE_MOTION && a2->type == ANIMATION_TYPE_MOTION)
return -1 ;
}
/* SvgAnimation priorities:
* - started later > started earlier
* - later in document > earlier in document
*
* Sort higher priority animations to the end
* so their effect overrides lower priority ones .
*/
start1 = get_last_start (a1);
start2 = get_last_start (a2);
if (start1 < start2)
return -1 ;
else if (start1 > start2)
return 1 ;
if (a1->line < a2->line)
return -1 ;
else if (a1->line > a2->line)
return 1 ;
return 0 ;
}
static void
collect_next_update_for_animation (SvgAnimation *a,
int64_t current_time,
GtkSvgRunMode *run_mode,
int64_t *next_update)
{
svg_animation_update_run_mode (a, current_time);
#ifdef DEBUG
if (a->run_mode > *run_mode)
{
const char *mode_name[] = { "STOPPED" , "DISCRETE" , "CONTINUOUS" };
dbg_print ("run" , "%s updates run mode to %s" , a->id, mode_name[a->run_mode]);
}
if (a->next_invalidate < *next_update)
{
dbg_print ("run" , "%s updates next update to %s" , a->id, format_time (a->next_invalidate));
}
#endif
*run_mode = MAX (*run_mode, a->run_mode);
*next_update = MIN (*next_update, a->next_invalidate);
if (a->state_changed)
{
*next_update = current_time;
a->state_changed = FALSE ;
}
}
static void
collect_next_update_for_shape (SvgElement *shape,
int64_t current_time,
GtkSvgRunMode *run_mode,
int64_t *next_update)
{
if (shape->animations)
{
for (unsigned int i = 0 ; i < shape->animations->len; i++)
{
SvgAnimation *a = g_ptr_array_index (shape->animations, i);
collect_next_update_for_animation (a, current_time, run_mode, next_update);
}
}
if (shape->shapes)
{
for (unsigned int i = 0 ; i < shape->shapes->len; i++)
{
SvgElement *s = g_ptr_array_index (shape->shapes, i);
collect_next_update_for_shape (s, current_time, run_mode, next_update);
}
}
}
static void
collect_next_update (GtkSvg *self)
{
GtkSvgRunMode run_mode = GTK_SVG_RUN_MODE_STOPPED;
int64_t next_update = INDEFINITE;
collect_next_update_for_shape (self->content, self->current_time, &run_mode, &next_update);
#ifdef DEBUG
if (strstr (g_getenv ("SVG_DEBUG" ) ?: "" , "continuous" ))
run_mode = GTK_SVG_RUN_MODE_CONTINUOUS;
#endif
self->run_mode = run_mode;
self->next_update = next_update;
#ifdef DEBUG
const char *mode_name[] = { "STOPPED" , "DISCRETE" , "CONTINUOUS" };
dbg_print ("run" , "run mode %s" , mode_name[self->run_mode]);
dbg_print ("run" , "next update %s" , format_time (self->next_update));
#endif
}
static void
invalidate_for_next_update (GtkSvg *self)
{
if (self->next_update <= self->current_time ||
self->run_mode == GTK_SVG_RUN_MODE_CONTINUOUS)
{
dbg_print ("run" , "invalidating for update" );
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
#ifdef DEBUG
else
{
GString *s = g_string_new ("not invalidating (" );
g_string_append_printf (s, "%s" , format_time (self->next_update));
g_string_append_printf (s, " > %s)" , format_time (self->current_time));
dbg_print ("run" , "%s" , s->str);
g_string_free (s, TRUE );
}
#endif
}
static void
shape_update_animation_state (SvgElement *shape,
int64_t current_time)
{
if (shape->animations)
{
gboolean any_changed = FALSE ;
for (unsigned int i = 0 ; i < shape->animations->len; i++)
{
SvgAnimation *a = g_ptr_array_index (shape->animations, i);
svg_animation_update_state (a, current_time);
any_changed |= a->state_changed;
}
if (any_changed)
g_ptr_array_sort_values (shape->animations, compare_anim);
}
if (shape->shapes)
{
for (unsigned int i = 0 ; i < shape->shapes->len; i++)
{
SvgElement *s = g_ptr_array_index (shape->shapes, i);
shape_update_animation_state (s, current_time);
}
}
}
static void schedule_next_update (GtkSvg *self);
static void
update_animation_state (GtkSvg *self)
{
shape_update_animation_state (self->content, self->current_time);
collect_next_update (self);
invalidate_for_next_update (self);
schedule_next_update (self);
}
static void
animations_update_for_pause (SvgElement *shape,
int64_t duration)
{
if (shape->animations)
{
for (unsigned int i = 0 ; i < shape->animations->len; i++)
{
SvgAnimation *a = g_ptr_array_index (shape->animations, i);
svg_animation_update_for_pause (a, duration);
}
}
if (svg_element_type_is_path (svg_element_get_type (shape)))
{
for (unsigned int i = 0 ; i < shape->shapes->len; i++)
{
SvgElement *sh = g_ptr_array_index (shape->shapes, i);
animations_update_for_pause (sh, duration);
}
}
}
/* }}} */
/* {{{ Animated attributes */
/* SvgAnimation works by
* - updating the current time
* - walking the shape tree and applying the relevant
* animations to produce current values
* - rendering the next frame based on these values
*
* When walking the shape tree , we need to do so in an order
* that ensures values are computed before they are used by
* other animations ( e . g . a motion animation needs to use the
* updated path of the shape it uses to compute the next position ) .
* This update order is determined at parse time .
*
* It is sometimes necessary to compute new values during
* rendering ( e . g . for < use > , where the < use > element replaces
* the parent of the referred to shape for inheritance purposes ) .
*
* To drive the animation , we compute the next update time
* based on the characteristics of the animations that we have .
* E . g . < set > animations only need to update at the start and
* at the end . Animating discrete values such as enums also
* only update when the value changes , while continuous
* interpolation requires an update for every frame .
*/
static SvgValue *
shape_get_current_value (SvgElement *shape,
unsigned int attr,
unsigned int idx)
{
if (idx == 0 )
{
return svg_element_get_current_value (shape, attr);
}
else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
{
SvgColorStop *stop;
g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));
g_assert (idx <= shape->color_stops->len);
stop = g_ptr_array_index (shape->color_stops, idx - 1 );
return svg_color_stop_get_current_value (stop, attr);
}
else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
{
SvgFilter *f;
g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));
g_assert (idx <= shape->filters->len);
f = g_ptr_array_index (shape->filters, idx - 1 );
return svg_filter_get_current_value (f, attr);
}
else
g_assert_not_reached ();
}
/* We pass parent separately instead of relying
* on svg_element_get_parent ( shape ) because < use > overrides parent
*/
static SvgValue *
shape_ref_base_value (SvgElement *shape,
SvgElement *parent,
SvgProperty attr,
unsigned int idx)
{
if (idx == 0 )
{
SvgValue *value;
value = svg_element_get_base_value (shape, attr);
if (svg_value_is_unset (value))
{
if (svg_element_get_type (shape) == SVG_ELEMENT_RADIAL_GRADIENT)
{
if (attr == SVG_PROPERTY_FX)
return shape_ref_base_value (shape, parent, SVG_PROPERTY_CX, idx);
else if (attr == SVG_PROPERTY_FY)
return shape_ref_base_value (shape, parent, SVG_PROPERTY_CY, idx);
}
if (parent && svg_property_inherited (attr))
return svg_value_ref (parent->current[attr]);
else
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else if (svg_value_is_inherit (value))
{
if (parent)
return svg_value_ref (svg_element_get_current_value (parent, attr));
else
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else if (svg_value_is_initial (value))
{
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else
{
return svg_value_ref (value);
}
}
else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
{
SvgColorStop *stop;
SvgValue *value;
g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));
g_assert (idx <= shape->color_stops->len);
stop = g_ptr_array_index (shape->color_stops, idx - 1 );
value = svg_color_stop_get_base_value (stop, attr);
if (svg_value_is_unset (value))
{
if (svg_property_inherited (attr))
return svg_value_ref (svg_element_get_current_value (shape, attr));
else
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else if (svg_value_is_inherit (value))
{
return svg_value_ref (svg_element_get_current_value (shape, attr));
}
else if (svg_value_is_initial (value))
{
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else
{
return svg_value_ref (value);
}
}
else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
{
SvgFilter *f;
SvgValue *value;
g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));
g_assert (idx <= shape->filters->len);
f = g_ptr_array_index (shape->filters, idx - 1 );
value = svg_filter_get_base_value (f, attr);
if (svg_value_is_unset (value))
{
if (svg_property_inherited (attr))
return svg_value_ref (shape_get_current_value (shape, attr, 0 ));
else
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else if (svg_value_is_inherit (value))
{
return svg_value_ref (shape_get_current_value (shape, attr, 0 ));
}
else if (svg_value_is_initial (value))
{
return svg_property_ref_initial_value (attr, svg_element_get_type (shape), parent != NULL);
}
else
{
return svg_value_ref (value);
}
}
else
g_assert_not_reached ();
}
static void
shape_set_current_value (SvgElement *shape,
SvgProperty attr,
unsigned int idx,
SvgValue *value)
{
if (idx == 0 )
{
svg_element_set_current_value (shape, attr, value);
}
else if (FIRST_STOP_PROPERTY <= attr && attr <= LAST_STOP_PROPERTY)
{
SvgColorStop *stop;
g_assert (svg_element_type_is_gradient (svg_element_get_type (shape)));
stop = g_ptr_array_index (shape->color_stops, idx - 1 );
svg_color_stop_set_current_value (stop, attr, value);
}
else if (FIRST_FILTER_PROPERTY <= attr && attr <= LAST_FILTER_PROPERTY)
{
SvgFilter *f;
g_assert (svg_element_type_is_filter (svg_element_get_type (shape)));
f = g_ptr_array_index (shape->filters, idx - 1 );
svg_filter_set_current_value (f, attr, value);
}
else
g_assert_not_reached ();
}
/* }}} */
/* {{{ Update computation */
static void frame_clock_connect (GtkSvg *self);
static void frame_clock_disconnect (GtkSvg *self);
static void
advance_later (gpointer data)
{
GtkSvg *self = data;
int64_t current_time;
self->pending_advance = 0 ;
current_time = self->current_time + 1 ;
if (self->clock)
current_time = MAX (current_time, gdk_frame_clock_get_frame_time (self->clock));
gtk_svg_advance (self, current_time);
}
void
gtk_svg_advance_after_snapshot (GtkSvg *self)
{
if (self->advance_after_snapshot)
{
self->advance_after_snapshot = FALSE ;
g_clear_handle_id (&self->pending_advance, g_source_remove);
self->pending_advance = g_idle_add_once (advance_later, self);
}
}
static void
schedule_next_update (GtkSvg *self)
{
if (!self->playing)
return ;
g_clear_handle_id (&self->pending_advance, g_source_remove);
if (self->run_mode == GTK_SVG_RUN_MODE_CONTINUOUS)
{
frame_clock_connect (self);
return ;
}
else
{
frame_clock_disconnect (self);
}
if (self->next_update <= self->current_time)
{
dbg_print ("times" , "next update NOW (%s)" , format_time (self->current_time));
self->advance_after_snapshot = TRUE ;
return ;
}
if (self->next_update < INDEFINITE)
{
int64_t interval = (self->next_update - self->current_time) / (double ) G_TIME_SPAN_MILLISECOND;
dbg_print ("times" , "next update in %" G_GINT64_FORMAT "ms" , interval);
self->pending_advance = g_timeout_add_once (interval, advance_later, self);
}
else
{
dbg_print ("times" , "next update NEVER" );
}
}
/* }}} */
/* {{{ Frame-clock driven updates */
static void
frame_clock_update (GdkFrameClock *clock,
GtkSvg *self)
{
int64_t time = gdk_frame_clock_get_frame_time (self->clock);
dbg_print ("clock" , "clock update, advancing to %s" , format_time (time));
gtk_svg_advance (self, time);
}
static void
frame_clock_connect (GtkSvg *self)
{
if (self->clock && self->clock_update_id == 0 )
{
self->clock_update_id = g_signal_connect (self->clock, "update" ,
G_CALLBACK (frame_clock_update), self);
gdk_frame_clock_begin_updating (self->clock);
}
}
static void
frame_clock_disconnect (GtkSvg *self)
{
if (self->clock && self->clock_update_id != 0 )
{
gdk_frame_clock_end_updating (self->clock);
g_clear_signal_handler (&self->clock_update_id, self->clock);
}
}
/* }}} */
/* {{{ Value updates */
static void
get_transform_data_for_motion (GskPathMeasure *measure,
double offset,
AnimationRotate rotate,
double *angle,
graphene_point_t *pos)
{
GskPath *path;
GskPathPoint point;
path = gsk_path_measure_get_path (measure);
if (offset == 0 )
{
gsk_path_get_start_point (path, &point);
}
else if (offset == 1 )
{
gsk_path_get_end_point (path, &point);
}
else
{
double length = gsk_path_measure_get_length (measure);
gsk_path_measure_get_point (measure, length * offset, &point);
}
gsk_path_point_get_position (&point, path, pos);
switch (rotate)
{
case ROTATE_FIXED:
break ;
case ROTATE_AUTO:
*angle = gsk_path_point_get_rotation (&point, path, GSK_PATH_TO_END);
break ;
case ROTATE_AUTO_REVERSE:
*angle = gsk_path_point_get_rotation (&point, path, GSK_PATH_TO_START);
break ;
default :
g_assert_not_reached ();
}
}
static SvgValue *
resolve_value (SvgElement *shape,
SvgComputeContext *context,
SvgProperty attr,
unsigned int idx,
SvgValue *value)
{
if (svg_value_is_initial (value))
{
SvgValue *v, *ret;
if (idx > 0 && svg_element_get_type (shape) == SVG_ELEMENT_FILTER)
v = svg_filter_ref_initial_value (g_ptr_array_index (shape->filters, idx - 1 ), attr);
else
v = svg_property_ref_initial_value (attr, svg_element_get_type (shape), context->parent != NULL);
ret = svg_value_resolve (v, attr, idx, shape, context);
svg_value_unref (v);
return ret;
}
else if (svg_value_is_inherit (value))
{
if (idx > 0 )
return svg_value_ref (svg_element_get_current_value (shape, attr));
else if (context->parent && svg_property_applies_to (attr, svg_element_get_type (context->parent)))
return svg_value_ref (svg_element_get_current_value (context->parent, attr));
else
{
SvgValue *v, *ret;
v = svg_property_ref_initial_value (attr, svg_element_get_type (shape), context->parent != NULL);
ret = svg_value_resolve (v, attr, idx, shape, context);
svg_value_unref (v);
return ret;
}
}
else if (svg_value_is_current (value))
{
if (idx > 0 )
{
if (svg_element_get_type (shape) == SVG_ELEMENT_FILTER)
{
SvgFilter *f = g_ptr_array_index (shape->filters, idx - 1 );
return svg_value_ref (svg_filter_get_current_value (f, attr));
}
else
{
SvgColorStop *stop = g_ptr_array_index (shape->color_stops, idx - 1 );
return svg_value_ref (svg_color_stop_get_current_value (stop, attr));
}
}
else
return svg_value_ref (svg_element_get_current_value (shape, attr));
}
else if (svg_value_is_auto (value))
{
if (attr == SVG_PROPERTY_WIDTH || attr == SVG_PROPERTY_HEIGHT)
{
if (svg_element_get_type (shape) == SVG_ELEMENT_SVG)
{
return svg_value_resolve (svg_percentage_new (100 ), attr, idx, shape, context);
}
else if (svg_element_get_type (shape) == SVG_ELEMENT_IMAGE)
{
GdkTexture *texture = svg_href_get_texture (svg_element_get_current_value (shape, SVG_PROPERTY_HREF));
if (!texture)
return svg_number_new (0 );
else if (attr == SVG_PROPERTY_WIDTH)
return svg_number_new (gdk_texture_get_width (texture));
else
return svg_number_new (gdk_texture_get_height (texture));
}
else
{
return svg_number_new (0 );
}
}
else if (attr == SVG_PROPERTY_RX || attr == SVG_PROPERTY_RY)
{
return svg_value_ref (value); /* handled in shape_get_current_path */
}
else
{
g_assert_not_reached ();
}
}
else
{
return svg_value_resolve (value, attr, idx, shape, context);
}
}
static SvgValue *
compute_animation_motion_value (SvgAnimation *a,
unsigned int rep,
unsigned int frame,
double frame_t,
SvgComputeContext *context)
{
double angle;
graphene_point_t orig_pos, final_pos;
SvgValue *value;
GskPathMeasure *measure;
graphene_point_init (&orig_pos, 0 , 0 );
/* svg does not have an origin for this,
* but we want to have one for gpa
*/
if (a->id && g_str_has_prefix (a->id, "gpa:" ))
{
measure = svg_element_get_current_measure (a->shape, context->viewport);
get_transform_data_for_motion (measure, a->gpa.origin, ROTATE_FIXED, &angle, &orig_pos);
gsk_path_measure_unref (measure);
}
angle = a->motion.angle;
measure = svg_animation_motion_get_current_measure (a, context->viewport);
if (measure)
{
double offset;
if (frame + 1 < a->n_frames)
offset = lerp (frame_t, a->frames[frame].point, a->frames[frame + 1 ].point);
else
offset = a->frames[frame].point;
if (offset < 0 || offset > 1 )
{
offset = fmod (offset, 1 );
if (offset < 9 )
offset += 1 ;
}
get_transform_data_for_motion (measure, offset, a->motion.rotate, &angle, &final_pos);
value = svg_transform_new_rotate_and_shift (angle, &orig_pos, &final_pos);
}
else
{
if (frame + 1 == a->n_frames)
{
value = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
}
else if (a->frames[0 ].value)
{
SvgValue *v1 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
SvgValue *v2 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame + 1 ].value);
value = svg_value_interpolate (v1, v2, context, frame_t);
svg_value_unref (v1);
svg_value_unref (v2);
}
else
{
value = svg_transform_new_none ();
}
}
if (a->accumulate == ANIMATION_ACCUMULATE_SUM && rep > 0 )
{
SvgValue *end_val, *acc;
if (measure)
{
get_transform_data_for_motion (measure, 1 , a->motion.rotate, &angle, &final_pos);
end_val = svg_transform_new_rotate_and_shift (angle, &orig_pos, &final_pos);
}
else if (a->frames[0 ].value)
{
end_val = svg_value_ref (a->frames[a->n_frames - 1 ].value);
}
else
{
end_val = svg_transform_new_none ();
}
acc = svg_value_accumulate (value, end_val, context, rep);
svg_value_unref (end_val);
svg_value_unref (value);
value = acc;
}
g_clear_pointer (&measure, gsk_path_measure_unref);
return value;
}
static SvgValue *
compute_value_at_time (SvgAnimation *a,
SvgComputeContext *context,
int64_t time)
{
int rep;
unsigned int frame;
double frame_t;
int64_t frame_start, frame_end;
if (a->type == ANIMATION_TYPE_SET)
return resolve_value (a->shape, context, a->attr, a->idx, a->frames[0 ].value);
if (!svg_animation_get_progress (a, time,
&rep, &frame, &frame_t, &frame_start, &frame_end))
gtk_svg_update_error (context->svg, "Not enough data to advance animation %s" , a->id);
if (a->calc_mode == CALC_MODE_DISCRETE || svg_property_discrete (a->attr))
{
if (a->calc_mode != CALC_MODE_DISCRETE && frame_t > 0 .5 )
frame = frame + 1 ;
if (a->attr != SVG_PROPERTY_TRANSFORM || a->type != ANIMATION_TYPE_MOTION)
return resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
else
return compute_animation_motion_value (a, rep, frame, 0 , context);
}
/* Now we are doing non-discrete linear or spline interpolation */
if (a->calc_mode == CALC_MODE_SPLINE)
frame_t = ease (a->frames[frame].params, frame_t);
if (a->attr != SVG_PROPERTY_TRANSFORM || a->type != ANIMATION_TYPE_MOTION)
{
SvgValue *ival;
if (frame + 1 == a->n_frames)
ival = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
else
{
SvgValue *v1 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
SvgValue *v2 = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame + 1 ].value);
ival = svg_value_interpolate (v1, v2, context, frame_t);
svg_value_unref (v1);
svg_value_unref (v2);
}
if (ival == NULL)
{
gtk_svg_update_error (context->svg,
"Failed to interpolate %s value (animation %s)" ,
svg_property_get_presentation (a->attr, svg_element_get_type (a->shape)),
a->id);
ival = resolve_value (a->shape, context, a->attr, a->idx, a->frames[frame].value);
}
if (a->accumulate == ANIMATION_ACCUMULATE_SUM && rep > 0 )
{
SvgValue *aval;
SvgValue *v = resolve_value (a->shape, context, a->attr, a->idx, a->frames[a->n_frames - 1 ].value);
aval = svg_value_accumulate (ival, v, context, rep);
svg_value_unref (v);
if (aval == NULL)
{
gtk_svg_update_error (context->svg,
"Failed to accumulate %s value (animation %s)" ,
svg_property_get_presentation (a->attr, svg_element_get_type (a->shape)),
a->id);
aval = svg_value_ref (ival);
}
svg_value_unref (ival);
return aval;
}
else
{
return ival;
}
}
else
{
return compute_animation_motion_value (a, rep, frame, frame_t, context);
}
}
static SvgValue *
compute_value_for_animation (SvgAnimation *a,
SvgComputeContext *context)
{
GdkColorState *previous = context->interpolation;
SvgValue *value = NULL;
if (a->color_interpolation == COLOR_INTERPOLATION_LINEAR)
context->interpolation = GDK_COLOR_STATE_SRGB_LINEAR;
else
context->interpolation = GDK_COLOR_STATE_SRGB;
if (a->status == ANIMATION_STATUS_INACTIVE)
{
/* keep the base value */
dbg_print ("values" , "%s: too early" , a->id);
}
else if (a->status == ANIMATION_STATUS_RUNNING)
{
/* animation is active */
dbg_print ("values" , "%s: updating value" , a->id);
value = compute_value_at_time (a, context, context->current_time);
}
else if (a->fill == ANIMATION_FILL_FREEZE)
{
/* keep the last value */
if (a->repeat_count == 1 )
{
if (a->attr == SVG_PROPERTY_TRANSFORM && a->type == ANIMATION_TYPE_MOTION)
{
dbg_print ("values" , "%s: frozen (motion)" , a->id);
value = compute_animation_motion_value (a, 1 , a->n_frames - 1 , 0 , context);
}
else
{
dbg_print ("values" , "%s: frozen (fast)" , a->id);
value = resolve_value (a->shape, context, a->attr, a->idx, a->frames[a->n_frames - 1 ].value);
}
}
else
{
dbg_print ("values" , "%s: frozen" , a->id);
value = compute_value_at_time (a, context, a->previous.end);
}
}
else
{
/* Back to base value */
dbg_print ("values" , "%s: back to base" , a->id);
}
context->interpolation = previous;
return value;
}
static void
shape_init_current_values (SvgElement *shape,
SvgComputeContext *context)
{
for (SvgProperty attr = FIRST_SVG_PROPERTY; attr <= LAST_SVG_PROPERTY; attr++)
{
if (svg_property_applies_to (attr, svg_element_get_type (shape)))
{
SvgValue *base;
SvgValue *value;
base = shape_ref_base_value (shape, context->parent, attr, 0 );
value = resolve_value (shape, context, attr, 0 , base);
svg_element_set_current_value (shape, attr, value);
svg_value_unref (value);
svg_value_unref (base);
}
}
if (svg_element_type_is_gradient (svg_element_get_type (shape)))
{
for (unsigned int idx = 0 ; idx < shape->color_stops->len; idx++)
{
SvgColorStop *stop = g_ptr_array_index (shape->color_stops, idx);
for (SvgProperty attr = FIRST_STOP_PROPERTY; attr <= LAST_STOP_PROPERTY; attr++)
{
SvgValue *base;
SvgValue *value;
base = shape_ref_base_value (shape, context->parent, attr, idx + 1 );
value = resolve_value (shape, context, attr, idx + 1 , base);
svg_color_stop_set_current_value (stop, attr, value);
svg_value_unref (value);
svg_value_unref (base);
}
}
}
if (svg_element_type_is_filter (svg_element_get_type (shape)))
{
for (unsigned int idx = 0 ; idx < shape->filters->len; idx++)
{
SvgFilter *f = g_ptr_array_index (shape->filters, idx);
SvgFilterType type = svg_filter_get_type (f);
for (unsigned int i = 0 ; i < svg_filter_type_get_n_attrs (type); i++)
{
SvgValue *base;
SvgValue *value;
SvgProperty attr;
attr = svg_filter_type_get_property (type, i);
base = shape_ref_base_value (shape, context->parent, attr, idx + 1 );
value = resolve_value (shape, context, attr, idx + 1 , base);
svg_filter_set_current_value (f, attr, value);
svg_value_unref (value);
svg_value_unref (base);
}
}
}
}
void
apply_view (SvgElement *content,
SvgElement *view)
{
SvgValue *vb;
SvgValue *cf;
if (!view)
view = content;
vb = svg_element_get_specified_value (view, SVG_PROPERTY_VIEW_BOX);
cf = svg_element_get_specified_value (view, SVG_PROPERTY_CONTENT_FIT);
if (!vb)
vb = svg_element_get_specified_value (content, SVG_PROPERTY_VIEW_BOX);
if (!cf)
cf = svg_element_get_specified_value (content, SVG_PROPERTY_CONTENT_FIT);
svg_element_set_base_value (content, SVG_PROPERTY_VIEW_BOX, vb, FALSE );
svg_element_set_base_value (content, SVG_PROPERTY_CONTENT_FIT, cf, FALSE );
}
void
compute_current_values_for_shape (SvgElement *shape,
SvgComputeContext *context)
{
const graphene_rect_t *old_viewport = context->viewport;
graphene_rect_t viewport;
shape_init_current_values (shape, context);
if (svg_element_get_type (shape) == SVG_ELEMENT_SVG || svg_element_get_type (shape) == SVG_ELEMENT_SYMBOL)
{
SvgValue *vb = svg_element_get_current_value (shape, SVG_PROPERTY_VIEW_BOX);
double width, height;
if (svg_element_get_parent (shape) == NULL)
{
width = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_WIDTH), context->svg->current_width);
height = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_HEIGHT), context->svg->current_height);
}
else
{
// FIXME use parent override for symbol
g_assert (context->viewport != NULL);
width = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_WIDTH), context->viewport->size.width);
height = svg_number_get (svg_element_get_current_value (shape, SVG_PROPERTY_HEIGHT), context->viewport->size.height);
}
if (!svg_view_box_get (vb, &viewport))
graphene_rect_init (&viewport, 0 , 0 , width, height);
context->viewport = &viewport;
}
if ((context->svg->features & GTK_SVG_ANIMATIONS) == 0 &&
!svg_element_is_important (shape, SVG_PROPERTY_VISIBILITY))
{
Visibility visibility;
if (svg_element_get_states (shape) & context->svg->state)
visibility = VISIBILITY_VISIBLE;
else
visibility = VISIBILITY_HIDDEN;
SvgValue *value = svg_visibility_new (visibility);
shape_set_current_value (shape, SVG_PROPERTY_VISIBILITY, 0 , value);
svg_value_unref (value);
}
if (shape->animations)
{
SvgValue *identity, *motion;
identity = svg_transform_new_none ();
motion = svg_value_ref (identity);
for (unsigned int i = 0 ; i < shape->animations->len; i++)
{
SvgAnimation *a = g_ptr_array_index (shape->animations, i);
SvgValue *val;
if (svg_element_is_important (shape, a->attr))
continue ;
val = compute_value_for_animation (a, context);
if (val)
{
if (a->additive == ANIMATION_ADDITIVE_SUM)
{
SvgValue *end_val;
if (a->type == ANIMATION_TYPE_MOTION)
{
end_val = svg_value_accumulate (val, motion, context, 1 );
if (end_val)
{
svg_value_unref (motion);
motion = end_val;
}
}
else
{
end_val = svg_value_accumulate (val, shape_get_current_value (shape, a->attr, a->idx), context, 1 );
if (end_val)
{
shape_set_current_value (shape, a->attr, a->idx, end_val);
svg_value_unref (end_val);
}
}
}
else
{
if (a->type == ANIMATION_TYPE_MOTION)
{
svg_value_unref (motion);
motion = svg_value_ref (val);
}
else
{
shape_set_current_value (shape, a->attr, a->idx, val);
}
}
svg_value_unref (val);
}
}
if (!svg_element_is_important (shape, SVG_PROPERTY_TRANSFORM))
{
if (!svg_value_equal (motion, identity))
{
SvgValue *combined;
combined = svg_value_accumulate (shape_get_current_value (shape, SVG_PROPERTY_TRANSFORM, 0 ), motion, context, 1 );
if (combined)
{
svg_element_set_current_value (shape, SVG_PROPERTY_TRANSFORM, combined);
svg_value_unref (combined);
}
}
}
svg_value_unref (identity);
svg_value_unref (motion);
}
if (svg_element_get_type (shape) == SVG_ELEMENT_USE)
svg_element_ensure_shadow_tree (shape, context->svg);
if (shape->shapes)
{
SvgElement *parent = context->parent;
context->parent = shape;
for (SvgElement *sh = shape->first; sh; sh = sh->next)
compute_current_values_for_shape (sh, context);
context->parent = parent;
}
context->viewport = old_viewport;
}
/* }}} */
/* {{{ GtkSymbolicPaintable implementation */
static void
gtk_svg_snapshot_with_weight (GtkSymbolicPaintable *paintable,
GtkSnapshot *snapshot,
double width,
double height,
const GdkRGBA *colors,
size_t n_colors,
double weight)
{
GtkSvg *self = GTK_SVG (paintable);
gtk_svg_snapshot_full (self, snapshot, width, height, colors, n_colors, weight);
}
static void
gtk_svg_snapshot_symbolic (GtkSymbolicPaintable *paintable,
GtkSnapshot *snapshot,
double width,
double height,
const GdkRGBA *colors,
size_t n_colors)
{
GtkSvg *self = GTK_SVG (paintable);
gtk_svg_snapshot_full (self, snapshot, width, height, colors, n_colors, 400 );
}
static void
gtk_svg_init_symbolic_paintable_interface (GtkSymbolicPaintableInterface *iface)
{
iface->snapshot_symbolic = gtk_svg_snapshot_symbolic;
iface->snapshot_with_weight = gtk_svg_snapshot_with_weight;
}
/* }}} */
/* {{{ GdkPaintable implementation */
static void
gtk_svg_snapshot (GdkPaintable *paintable,
GtkSnapshot *snapshot,
double width,
double height)
{
GtkSvg *self = GTK_SVG (paintable);
gtk_svg_snapshot_full (self, snapshot, width, height, NULL, 0 , 400 );
}
static int
gtk_svg_get_intrinsic_width (GdkPaintable *paintable)
{
GtkSvg *self = GTK_SVG (paintable);
return ceil (self->width);
}
static int
gtk_svg_get_intrinsic_height (GdkPaintable *paintable)
{
GtkSvg *self = GTK_SVG (paintable);
return ceil (self->height);
}
static double
gtk_svg_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
GtkSvg *self = GTK_SVG (paintable);
return self->aspect_ratio;
}
static void
gtk_svg_init_paintable_interface (GdkPaintableInterface *iface)
{
iface->snapshot = gtk_svg_snapshot;
iface->get_intrinsic_width = gtk_svg_get_intrinsic_width;
iface->get_intrinsic_height = gtk_svg_get_intrinsic_height;
iface->get_intrinsic_aspect_ratio = gtk_svg_get_intrinsic_aspect_ratio;
}
/* }}} */
/* {{{ GObject boilerplate */
struct _GtkSvgClass
{
GObjectClass parent_class;
};
enum
{
PROP_RESOURCE = 1 ,
PROP_FEATURES,
PROP_PLAYING,
PROP_WEIGHT,
PROP_STATE,
PROP_OVERFLOW,
PROP_STYLESHEET,
NUM_PROPERTIES,
};
static GParamSpec *properties[NUM_PROPERTIES];
G_DEFINE_TYPE_WITH_CODE (GtkSvg, gtk_svg, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
gtk_svg_init_paintable_interface)
G_IMPLEMENT_INTERFACE (GTK_TYPE_SYMBOLIC_PAINTABLE,
gtk_svg_init_symbolic_paintable_interface))
static void
gtk_svg_init (GtkSvg *self)
{
self->weight = -1 ;
self->overflow = GTK_OVERFLOW_HIDDEN;
self->initial_state = 0 ;
self->state = 0 ;
self->load_time = INDEFINITE;
self->state_change_delay = 0 ;
self->next_update = INDEFINITE;
self->playing = FALSE ;
self->run_mode = GTK_SVG_RUN_MODE_STOPPED;
self->features = GTK_SVG_DEFAULT_FEATURES;
self->content = svg_element_new (NULL, SVG_ELEMENT_SVG);
self->timeline = timeline_new ();
self->images = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
self->user_styles = array_new_with_clear_func (sizeof (SvgCssRuleset), (GDestroyNotify) svg_css_ruleset_clear);
self->author_styles = array_new_with_clear_func (sizeof (SvgCssRuleset), (GDestroyNotify) svg_css_ruleset_clear);
}
static void
gtk_svg_dispose (GObject *object)
{
GtkSvg *self = GTK_SVG (object);
gtk_svg_clear_content (self);
/* clear_content preserves the stylesheet */
g_clear_pointer (&self->stylesheet, g_bytes_unref);
/* clear_content recreates these */
g_clear_pointer (&self->content, svg_element_free);
g_clear_pointer (&self->timeline, timeline_free);
g_clear_pointer (&self->images, g_hash_table_unref);
g_clear_pointer (&self->user_styles, g_array_unref);
g_clear_pointer (&self->author_styles, g_array_unref);
frame_clock_disconnect (self);
g_clear_handle_id (&self->pending_advance, g_source_remove);
g_clear_object (&self->clock);
G_OBJECT_CLASS (gtk_svg_parent_class)->dispose (object);
}
static void
gtk_svg_get_property (GObject *object,
unsigned int property_id,
GValue *value,
GParamSpec *pspec)
{
GtkSvg *self = GTK_SVG (object);
switch (property_id)
{
case PROP_RESOURCE:
g_value_set_string (value, self->resource);
break ;
case PROP_FEATURES:
g_value_set_flags (value, self->features);
break ;
case PROP_PLAYING:
g_value_set_boolean (value, self->playing);
break ;
case PROP_WEIGHT:
g_value_set_double (value, self->weight);
break ;
case PROP_STATE:
g_value_set_uint (value, self->state);
break ;
case PROP_OVERFLOW:
g_value_set_enum (value, self->overflow);
break ;
case PROP_STYLESHEET:
g_value_set_boxed (value, self->stylesheet);
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break ;
}
}
static void
gtk_svg_set_property (GObject *object,
unsigned int property_id,
const GValue *value,
GParamSpec *pspec)
{
GtkSvg *self = GTK_SVG (object);
switch (property_id)
{
case PROP_RESOURCE:
gtk_svg_load_from_resource (self, g_value_get_string (value));
break ;
case PROP_FEATURES:
gtk_svg_set_features (self, g_value_get_flags (value));
break ;
case PROP_PLAYING:
gtk_svg_set_playing (self, g_value_get_boolean (value));
break ;
case PROP_STATE:
gtk_svg_set_state (self, g_value_get_uint (value));
break ;
case PROP_WEIGHT:
gtk_svg_set_weight (self, g_value_get_double (value));
break ;
case PROP_OVERFLOW:
gtk_svg_set_overflow (self, g_value_get_enum (value));
break ;
case PROP_STYLESHEET:
gtk_svg_set_stylesheet (self, g_value_get_boxed (value));
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break ;
}
}
static void
gtk_svg_class_init (GtkSvgClass *class )
{
GObjectClass *object_class = G_OBJECT_CLASS (class );
svg_element_type_init ();
svg_filter_type_init ();
svg_properties_init ();
object_class->dispose = gtk_svg_dispose;
object_class->get_property = gtk_svg_get_property;
object_class->set_property = gtk_svg_set_property;
/**
* GtkSvg : resource :
*
* Resource to load SVG data from .
*
* This property is meant to create a paintable
* from a resource in ui files .
*
* Since : 4 . 22
*/
properties[PROP_RESOURCE] =
g_param_spec_string ("resource" , NULL, NULL,
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : features :
*
* Enabled features for this paintable .
*
* Note that features have to be set before
* loading SVG data to take effect .
*
* Since : 4 . 22
*/
properties[PROP_FEATURES] =
g_param_spec_flags ("features" , NULL, NULL,
GTK_TYPE_SVG_FEATURES, GTK_SVG_DEFAULT_FEATURES,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : playing :
*
* Whether the paintable is currently animating its content .
*
* To set this property , use the [ method @ Gtk . Svg . play ] and
* [ method @ Gtk . Svg . pause ] functions .
*
* Since : 4 . 22
*/
properties[PROP_PLAYING] =
g_param_spec_boolean ("playing" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : weight :
*
* If not set to - 1 , this value overrides the weight used
* when rendering the paintable .
*
* Since : 4 . 22
*/
properties[PROP_WEIGHT] =
g_param_spec_double ("weight" , NULL, NULL,
-1 , 1000 , -1 ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : state :
*
* The current state of the renderer .
*
* This can be a number between 0 and 63 .
*
* Since : 4 . 22
*/
properties[PROP_STATE] =
g_param_spec_uint ("state" , NULL, NULL,
0 , G_MAXUINT, 0 ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : overflow :
*
* Whether the rendering will be clipped to the bounds .
*
* Since : 4 . 24
*/
properties[PROP_OVERFLOW] =
g_param_spec_enum ("overflow" , NULL, NULL,
GTK_TYPE_OVERFLOW, GTK_OVERFLOW_HIDDEN,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkSvg : stylesheet :
*
* A CSS stylesheet to apply to the SVG .
*
* Since : 4 . 24
*/
properties[PROP_STYLESHEET] =
g_param_spec_boxed ("stylesheet" , NULL, NULL,
G_TYPE_BYTES,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
/**
* GtkSvg : : error :
* @ svg : the SVG paintable
* @ error : the error
*
* Signals that an error occurred .
*
* Errors can occur both during parsing and during rendering .
*
* The expected error values are in the [ error @ Gtk . SvgError ] enumeration ,
* context information about the location of parsing errors can
* be obtained with the various ` gtk_svg_error ` functions .
*
* Parsing errors are never fatal , so the parsing will resume after
* the error . Errors may however cause parts of the given data or
* even all of it to not be parsed at all . So it is a useful idea
* to check that the parsing succeeds by connecting to this signal .
*
* : : : note
* This signal is emitted in the middle of parsing or rendering ,
* and if you handle it , you must be careful . Logging the errors
* you receive is fine , but modifying the widget hierarchy or
* changing the paintable state definitively isn ' t .
*
* If in doubt , defer to an idle .
*
* Since : 4 . 22
*/
error_signal =
g_signal_new ("error" ,
G_TYPE_FROM_CLASS (object_class),
G_SIGNAL_RUN_LAST,
0 ,
NULL, NULL,
g_cclosure_marshal_VOID__BOXED,
G_TYPE_NONE, 1 , G_TYPE_ERROR);
g_signal_set_va_marshaller (error_signal,
G_TYPE_FROM_CLASS (object_class),
g_cclosure_marshal_VOID__BOXEDv);
}
/* }}} */
/* {{{ Debug tools */
#ifdef DEBUG
static void
shape_dump_animation_state (SvgElement *shape, GString *string)
{
if (shape->animations)
{
for (unsigned int i = 0 ; i < shape->animations->len; i++)
{
SvgAnimation *a = g_ptr_array_index (shape->animations, i);
if (a->status == ANIMATION_STATUS_RUNNING)
g_string_append_printf (string, " %s" , a->id);
}
}
if (shape->shapes)
{
for (unsigned int i = 0 ; i < shape->shapes->len; i++)
{
SvgElement *s = g_ptr_array_index (shape->shapes, i);
shape_dump_animation_state (s, string);
}
}
}
static void
animation_state_dump (GtkSvg *self)
{
GString *s = g_string_new ("running" );
shape_dump_animation_state (self->content, s);
dbg_print ("times" , "%s" , s->str);
g_string_free (s, TRUE );
}
static void
timeline_dump (Timeline *timeline)
{
GString *s = g_string_new ("Timeline:\n" );
for (unsigned int i = 0 ; i < timeline->times->len; i++)
{
TimeSpec *spec = g_ptr_array_index (timeline->times, i);
g_string_append (s, " " );
time_spec_print (spec, NULL, s);
g_string_append (s, "\n" );
}
g_print ("%s" , s->str);
g_string_free (s, TRUE );
}
#endif
/* }}} */
/* {{{ Private API */
GtkSvg *
gtk_svg_copy (GtkSvg *orig)
{
GBytes *bytes = NULL;
GtkSvg *svg;
bytes = gtk_svg_serialize_full (orig, NULL, 0 , GTK_SVG_SERIALIZE_DEFAULT);
svg = gtk_svg_new_from_bytes (bytes);
g_bytes_unref (bytes);
gtk_svg_set_weight (svg, gtk_svg_get_weight (orig));
gtk_svg_set_state (svg, gtk_svg_get_state (orig));
return svg;
}
gboolean
gtk_svg_equal (GtkSvg *svg1,
GtkSvg *svg2)
{
if (svg1 == svg2)
return TRUE ;
if (svg1->gpa_version != svg2->gpa_version ||
svg1->n_state_names != svg2->n_state_names ||
svg1->weight != svg2->weight ||
svg1->initial_state != svg2->initial_state ||
svg1->features != svg2->features ||
g_strcmp0 (svg1->author, svg2->author) != 0 ||
g_strcmp0 (svg1->license, svg2->license) != 0 ||
g_strcmp0 (svg1->description, svg2->description) != 0 ||
g_strcmp0 (svg1->keywords, svg2->keywords) != 0 )
return FALSE ;
for (unsigned int i = 0 ; i < svg1->n_state_names; i++)
{
if (g_strcmp0 (svg1->state_names[i], svg2->state_names[i]) != 0 )
return FALSE ;
}
return svg_element_equal (svg1->content, svg2->content);
}
void
gtk_svg_set_activate_callback (GtkSvg *svg,
SvgElementCallback callback,
gpointer data)
{
svg->activate_callback = callback;
svg->activate_data = data;
}
void
gtk_svg_set_hover_callback (GtkSvg *svg,
SvgElementCallback callback,
gpointer data)
{
svg->hover_callback = callback;
svg->hover_data = data;
}
/* {{{ Animation */
static void update_for_state (GtkSvg *self,
unsigned int previous_state);
/*< private>
* gtk_svg_set_load_time :
* @ self : an SVG paintable
* @ load_time : the load time
*
* Sets the load time of the SVG , which marks the
* ' beginning of time ' for any animations defined
* in it .
*
* The load time will be set as a side - effect when
* the animation starts runnning due to ` playing `
* being set to ` TRUE ` .
*
* @ load_time must be in microseconds and in the same
* timescale as the times returned by [ class @ Gdk . FrameClock ] .
*
* Since : 4 . 22
*/
void
gtk_svg_set_load_time (GtkSvg *self,
int64_t load_time)
{
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (self->load_time == INDEFINITE);
self->load_time = load_time;
self->current_time = load_time;
if (!self->playing)
self->pause_time = load_time;
#ifdef DEBUG
time_base = self->load_time;
if (g_getenv ("SVG_DEBUG" ))
timeline_dump (self->timeline);
#endif
timeline_set_load_time (self->timeline, load_time);
update_for_state (self, self->initial_state);
update_animation_state (self);
}
/*< private >
* gtk_svg_advance :
* @ self : an SVG paintable
* @ current_time : the time to advance to
*
* Advances the animation to the given value , which must
* be in microseconds and in the same timescale as the
* times returned by [ class @ Gdk . FrameClock ] .
*
* Note that this function is only useful when * not *
* running the animations automatically with a frame
* clock , via [ method @ Gtk . Svg . play ] .
*
* Also note that moving time backwards is not
* supported and may lead to unexpected results .
*
* Since : 4 . 22
*/
void
gtk_svg_advance (GtkSvg *self,
int64_t current_time)
{
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (self->load_time < INDEFINITE);
g_return_if_fail (self->current_time <= current_time);
dbg_print ("run" , "advancing current time to %s" , format_time (current_time));
self->current_time = current_time;
update_animation_state (self);
#ifdef DEBUG
animation_state_dump (self);
#endif
}
/*< private >
* gtk_svg_get_run_mode :
* @ self : an SVG paintable
*
* Returns the current ' run mode ' of the animation .
*
* A ' continuous ' run mode indicates that animations
* are expected to provide different content for every
* frame , in which case it is most practical to advance
* the animation in a tick callback .
*
* A ' discrete ' run mode indicates that it makes sense
* to advance the animations in larger steps , using a
* timeout . See [ method @ Gtk . Svg . get_next_update ] for
* determining how long the timeout should be .
*
* Note that this function is only useful when * not *
* running the animations automatically via [ method @ Gtk . Svg . play ] .
*
* Since : 4 . 22
*/
GtkSvgRunMode
gtk_svg_get_run_mode (GtkSvg *self)
{
return self->run_mode;
}
/*< private >
* gtk_svg_get_next_update :
* @ self : an SVG paintable
*
* Returns the next time at which animations are
* expected to provide different content .
*
* An economic way of handling the update is to schedule
* a timeout for that time , and advance the animation then .
*
* Since : 4 . 22
*/
int64_t
gtk_svg_get_next_update (GtkSvg *self)
{
return self->next_update;
}
/* }}} */
/*< private>
* gtk_svg_set_playing :
* @ self : an SVG paintable
* @ playing : the new state
*
* Sets whether the paintable is animating its content .
*
* If ` playing ` is set to true , and the paintable has
* a frame clock , then it will automatically advance
* the animation .
*/
void
gtk_svg_set_playing (GtkSvg *self,
gboolean playing)
{
int64_t current_time;
if (self->playing == playing)
return ;
self->playing = playing;
if (self->clock)
current_time = gdk_frame_clock_get_frame_time (self->clock);
else
current_time = 0 ;
current_time = MAX (self->current_time, current_time);
if (self->playing)
{
if (self->load_time != INDEFINITE)
{
int64_t duration = current_time - self->pause_time;
/* A simple-minded implementation of pausing:
* Forward all definite times by the duration of
* the pause , to make it as if that time never
* happened .
*/
if (self->clock)
self->current_time = current_time;
self->load_time += duration;
#ifdef DEBUG
time_base = self->load_time;
#endif
animations_update_for_pause (self->content, duration);
timeline_update_for_pause (self->timeline, duration);
invalidate_for_next_update (self);
schedule_next_update (self);
}
else
{
if (self->clock)
gtk_svg_set_load_time (self, current_time);
}
}
else
{
if (self->load_time != INDEFINITE)
self->pause_time = current_time;
if (self->clock)
frame_clock_disconnect (self);
g_clear_handle_id (&self->pending_advance, g_source_remove);
}
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLAYING]);
}
/*< private >
* gtk_svg_clear_content :
* @ self : an SVG paintable
*
* Resets the paintable to the initial , empty state .
*/
void
gtk_svg_clear_content (GtkSvg *self)
{
g_clear_pointer (&self->resource, g_free);
g_clear_pointer (&self->timeline, timeline_free);
g_clear_pointer (&self->content, svg_element_free);
g_clear_pointer (&self->images, g_hash_table_unref);
g_clear_object (&self->fontmap);
g_clear_pointer (&self->font_files, g_ptr_array_unref);
g_clear_pointer (&self->node, gsk_render_node_unref);
self->content = svg_element_new (NULL, SVG_ELEMENT_SVG);
self->timeline = timeline_new ();
self->images = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
self->width = 0 ;
self->height = 0 ;
self->aspect_ratio = 0 ;
self->current_width = 0 ;
self->current_height = 0 ;
self->initial_state = 0 ;
self->state = 0 ;
self->max_state = 0 ;
self->state_change_delay = 0 ;
self->used = 0 ;
self->has_use_cycle = FALSE ;
self->gpa_version = 0 ;
g_clear_pointer (&self->author, g_free);
g_clear_pointer (&self->license, g_free);
g_clear_pointer (&self->description, g_free);
g_clear_pointer (&self->keywords, g_free);
g_clear_pointer (&self->resource, g_free);
g_clear_pointer (&self->state_names, g_strfreev);
self->n_state_names = 0 ;
g_array_set_size (self->user_styles, 0 );
g_array_set_size (self->author_styles, 0 );
g_list_free_full (self->media, (GDestroyNotify) svg_css_media_block_free);
self->media = NULL;
/* Note: we intentionally keep the stylesheet */
self->focus = NULL;
self->initial_focus = NULL;
self->view = NULL;
self->hover = NULL;
self->active = NULL;
}
/*< private>
* gtk_svg_set_state_names :
* @ svg : a ` GtkSvg `
* @ names : ( array zero - terminated = 1 ) : a ` NULL ` - terminated arrayt
* of strings
*
* Sets names for states .
*
* Returns : true if the state names were set successfully
*/
gboolean
gtk_svg_set_state_names (GtkSvg *svg,
const char **names)
{
for (unsigned int i = 0 ; names[i]; i++)
{
if (!valid_state_name (names[i]))
return FALSE ;
}
if (!strv_unique ((GStrv) names))
return FALSE ;
g_strfreev (svg->state_names);
svg->state_names = g_strdupv ((char **) names);
svg->n_state_names = g_strv_length ((char **) names);
return TRUE ;
}
/* {{{ Input */
void
gtk_svg_activate_element (GtkSvg *self,
SvgElement *link)
{
SvgValue *value = svg_element_get_current_value (link, SVG_PROPERTY_HREF);
SvgAnimation *animation = svg_href_get_animation (value);
SvgElement *target = svg_href_get_shape (value);
int64_t current_time = get_current_time (self);
g_assert (svg_element_get_type (link) == SVG_ELEMENT_LINK);
if (animation)
{
svg_animation_start (animation, current_time);
svg_animation_update_state (animation, current_time);
collect_next_update (self);
invalidate_for_next_update (self);
schedule_next_update (self);
}
else if (target && svg_element_get_type (target) == SVG_ELEMENT_VIEW)
{
gtk_svg_set_view (self, target);
}
else
{
if (self->activate_callback)
self->activate_callback (link, self->activate_data);
}
/* For links, browsers treat 'click' as 'device independent'
* and emit it regardless whether key or button press .
*/
timeline_update_for_event (self->timeline, link, EVENT_TYPE_CLICK, current_time);
gtk_svg_advance (self, current_time);
if (!svg_element_get_visited (link))
{
svg_element_set_visited (link, TRUE );
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
}
static void
gtk_svg_set_hover (GtkSvg *self,
SvgElement *target)
{
int64_t current_time = get_current_time (self);
if (self->hover == target)
return ;
if (self->hover)
{
timeline_update_for_event (self->timeline, self->hover, EVENT_TYPE_MOUSE_LEAVE, current_time);
svg_element_set_hover (self->hover, FALSE );
}
if (target)
{
timeline_update_for_event (self->timeline, target, EVENT_TYPE_MOUSE_ENTER, current_time);
svg_element_set_hover (target, TRUE );
}
self->hover = target;
self->style_changed = TRUE ;
gtk_svg_advance (self, current_time);
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
if (self->hover_callback)
self->hover_callback (self->hover, self->hover_data);
}
void
gtk_svg_set_active (GtkSvg *self,
SvgElement *target)
{
if (self->active == target)
return ;
if (self->active)
svg_element_set_active (self->active, FALSE );
if (target)
svg_element_set_active (target, TRUE );
self->active = target;
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
SvgElement *
gtk_svg_get_active (GtkSvg *self)
{
return self->active;
}
gboolean
gtk_svg_handle_event (GtkSvg *self,
GdkEvent *event,
double x,
double y)
{
graphene_point_t p = GRAPHENE_POINT_INIT (x, y);
SvgElement *target;
/* Don't handle events before we're loaded */
if (self->load_time == INDEFINITE)
return FALSE ;
switch ((unsigned int ) gdk_event_get_event_type (event))
{
case GDK_KEY_PRESS:
case GDK_KEY_RELEASE:
if (self->focus)
return svg_element_propagate_event (self->focus, event, self);
break ;
case GDK_MOTION_NOTIFY:
case GDK_ENTER_NOTIFY:
case GDK_LEAVE_NOTIFY:
gtk_svg_set_hover (self, gtk_svg_pick_element (self, &p));
break ;
case GDK_BUTTON_PRESS:
case GDK_BUTTON_RELEASE:
target = gtk_svg_pick_element (self, &p);
if (target)
svg_element_propagate_event (target, event, self);
break ;
default :
break ;
}
if (gdk_event_get_event_type (event) == GDK_BUTTON_RELEASE &&
gdk_button_event_get_button (event) == GDK_BUTTON_PRIMARY &&
gtk_svg_get_active (self) != NULL)
{
gtk_svg_set_active (self, NULL);
}
return FALSE ;
}
void
gtk_svg_handle_crossing (GtkSvg *self,
const GtkCrossingData *crossing,
double x,
double y)
{
/* Don't handle events before we're loaded */
if (self->load_time == INDEFINITE)
return ;
if (crossing->type == GTK_CROSSING_FOCUS ||
crossing->type == GTK_CROSSING_ACTIVE)
{
if (crossing->direction == GTK_CROSSING_IN)
gtk_svg_grab_focus (self);
else
gtk_svg_lose_focus (self);
}
else if (crossing->type == GTK_CROSSING_POINTER)
{
if (crossing->direction == GTK_CROSSING_IN)
gtk_svg_set_hover (self, gtk_svg_pick_element (self, &GRAPHENE_POINT_INIT (x, y)));
else
{
gtk_svg_set_hover (self, NULL);
gtk_svg_set_active (self, NULL);
}
}
}
gboolean
gtk_svg_grab_focus (GtkSvg *self)
{
SvgElement *focus = NULL;
int64_t current_time = get_current_time (self);
/* Don't handle events before we're loaded */
if (self->load_time == INDEFINITE)
return FALSE ;
if (self->focus)
return TRUE ;
if (self->initial_focus)
{
focus = self->initial_focus;
self->initial_focus = NULL;
}
else
{
do
{
if (focus)
focus = svg_element_next (focus);
else
focus = self->content;
if (focus && svg_element_get_focusable (focus))
break ;
}
while (focus);
}
if (!focus)
return FALSE ;
svg_element_set_focus (focus, TRUE );
timeline_update_for_event (self->timeline, focus, EVENT_TYPE_FOCUS, current_time);
gtk_svg_advance (self, current_time);
self->focus = focus;
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
return TRUE ;
}
gboolean
gtk_svg_lose_focus (GtkSvg *self)
{
int64_t current_time = get_current_time (self);
/* Don't handle events before we're loaded */
if (self->load_time == INDEFINITE)
return FALSE ;
if (!self->focus)
return TRUE ;
svg_element_set_focus (self->focus, FALSE );
timeline_update_for_event (self->timeline, self->focus, EVENT_TYPE_BLUR, current_time);
gtk_svg_advance (self, current_time);
self->focus = NULL;
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
return TRUE ;
}
gboolean
gtk_svg_move_focus (GtkSvg *self,
GtkDirectionType direction)
{
SvgElement *focus = NULL;
int64_t current_time;
/* Don't handle events before we're loaded */
if (self->load_time == INDEFINITE)
return FALSE ;
if (direction == GTK_DIR_TAB_FORWARD)
{
SvgElement *next = self->focus;
do
{
if (next)
next = svg_element_next (next);
else
next = svg_element_first (self->content);
if (next == NULL || svg_element_get_focusable (next))
{
focus = next;
break ;
}
}
while (next);
}
else if (direction == GTK_DIR_TAB_BACKWARD)
{
SvgElement *next = self->focus;
do
{
if (next)
next = svg_element_previous (next);
else
next = svg_element_last (self->content);
if (next == NULL || svg_element_get_focusable (next))
{
focus = next;
break ;
}
} while (next);
}
else
{
focus = self->focus;
}
if (self->focus == focus)
return FALSE ;
current_time = get_current_time (self);
if (self->focus)
{
timeline_update_for_event (self->timeline, self->focus, EVENT_TYPE_BLUR, current_time);
svg_element_set_focus (self->focus, FALSE );
}
if (focus)
{
timeline_update_for_event (self->timeline, focus, EVENT_TYPE_FOCUS, current_time);
svg_element_set_focus (focus, TRUE );
}
gtk_svg_advance (self, current_time);
self->focus = focus;
self->style_changed = TRUE ;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
return self->focus != NULL;
}
/* }}} */
/* {{{ Views */
void
gtk_svg_set_view (GtkSvg *self,
SvgElement *view)
{
g_assert (view == NULL || view->type == SVG_ELEMENT_VIEW);
if (self->view == view)
return ;
self->view = view;
self->view_changed = TRUE ;
determine_size (self);
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
/* }}} */
/* }}} */
/* {{{ Public API */
/* {{{ Constructors */
/**
* gtk_svg_new :
*
* Creates a new , empty SVG paintable .
*
* Returns : the paintable
*
* Since : 4 . 22
*/
GtkSvg *
gtk_svg_new (void )
{
return g_object_new (GTK_TYPE_SVG, NULL);
}
/**
* gtk_svg_new_from_bytes :
* @ bytes : the data
*
* Parses the SVG data in @ bytes and creates a paintable .
*
* Returns : the paintable
*
* Since : 4 . 22
*/
GtkSvg *
gtk_svg_new_from_bytes (GBytes *bytes)
{
GtkSvg *self = g_object_new (GTK_TYPE_SVG, NULL);
gtk_svg_init_from_bytes (self, bytes);
return self;
}
/**
* gtk_svg_new_from_resource :
* @ path : the resource path
*
* Parses the SVG data in the resource and creates a paintable .
*
* Returns : the paintable
*
* Since : 4 . 22
*/
GtkSvg *
gtk_svg_new_from_resource (const char *path)
{
GtkSvg *self = g_object_new (GTK_TYPE_SVG, NULL);
gtk_svg_init_from_resource (self, path);
return self;
}
/* }}} */
/* {{{ Loading */
/**
* gtk_svg_load_from_bytes :
* @ self : an SVG paintable
* @ bytes : the data to load
*
* Loads SVG content into an existing SVG paintable .
*
* To track errors while loading SVG content ,
* connect to the [ signal @ Gtk . Svg : : error ] signal .
*
* This clears any previously loaded content .
*
* Since : 4 . 22
*/
void
gtk_svg_load_from_bytes (GtkSvg *self,
GBytes *bytes)
{
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (bytes != NULL);
gtk_svg_clear_content (self);
gtk_svg_init_from_bytes (self, bytes);
gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
/**
* gtk_svg_load_from_resource :
* @ self : an SVG paintable
* @ path : the resource path
*
* Loads SVG content into an existing SVG paintable .
*
* To track errors while loading SVG content ,
* connect to the [ signal @ Gtk . Svg : : error ] signal .
*
* This clears any previously loaded content .
*
* Since : 4 . 22
*/
void
gtk_svg_load_from_resource (GtkSvg *self,
const char *path)
{
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (path != NULL);
gtk_svg_clear_content (self);
self->resource = g_strdup (path);
gtk_svg_init_from_resource (self, path);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RESOURCE]);
}
/* }}} */
/* {{{ Setters and getters */
/**
* gtk_svg_set_weight :
* @ self : an SVG paintable
* @ weight : the font weight , as a value between - 1 and 1000
*
* Sets the weight that is used when rendering .
*
* The weight affects the effective linewidth when stroking
* paths .
*
* The default value of - 1 means to use the font weight
* from CSS .
*
* Since : 4 . 22
*/
void
gtk_svg_set_weight (GtkSvg *self,
double weight)
{
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (-1 <= weight && weight <= 1000 );
if (self->weight == weight)
return ;
self->weight = weight;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WEIGHT]);
}
/**
* gtk_svg_get_weight :
* @ self : an SVG paintable
*
* Gets the value of the weight property .
*
* Returns : the weight
*
* Since : 4 . 22
*/
double
gtk_svg_get_weight (GtkSvg *self)
{
g_return_val_if_fail (GTK_IS_SVG (self), -1 );
return self->weight;
}
static void
update_for_state (GtkSvg *self,
unsigned int previous_state)
{
int64_t current_time;
if ((self->features & GTK_SVG_ANIMATIONS) == 0 || !self->playing)
{
if (self->gpa_version > 0 )
{
apply_state (self, self->state);
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}
}
current_time = get_current_time (self);
timeline_update_for_state (self->timeline,
previous_state, self->state,
current_time + self->state_change_delay);
gtk_svg_advance (self, current_time);
#ifdef DEBUG
animation_state_dump (self);
#endif
}
/**
* gtk_svg_set_state :
* @ self : an SVG paintable
* @ state : the state to set , as a value between 0 and 63
*
* Sets the state of the paintable .
*
* If the paintable is currently playing , the state change
* will apply transitions that are defined in the SVG . If
* the paintable is not playing , the state change will take
* effect instantaneously .
*
* Since : 4 . 22
*/
void
gtk_svg_set_state (GtkSvg *self,
unsigned int state)
{
unsigned int previous_state;
g_return_if_fail (GTK_IS_SVG (self));
g_return_if_fail (state <= 63 );
if (self->state == state)
return ;
previous_state = self->state;
self->state = state;
dbg_print ("state" , "renderer state %u -> %u" , previous_state, state);
if ((self->features & GTK_SVG_EXTENSIONS) == 0 )
goto done;
/* Don't jiggle things while we're still loading */
if (self->load_time != INDEFINITE)
update_for_state (self, previous_state);
done:
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
}
/**
* gtk_svg_get_state :
* @ self : an SVG paintable
*
* Gets the current state of the paintable .
*
* Returns : the state
*
* Since : 4 . 22
*/
unsigned int
gtk_svg_get_state (GtkSvg *self)
{
g_return_val_if_fail (GTK_IS_SVG (self), 0 );
return self->state;
}
/**
* gtk_svg_get_state_names :
* @ self : an SVG paintable
* @ length : ( out ) ( nullable ) : return location for the number
* of strings that are returned
*
* Returns a ` NULL ` - terminated array of
* state names , if available .
*
* Note that the returned array and the strings
* contained in it will only be valid until the
* ` GtkSvg ` is cleared or reloaded , so if you
* want to keep it around , you should make a copy .
*
* Returns : ( nullable ) ( transfer none ) : the state names
*
* Since : 4 . 22
*/
const char **
gtk_svg_get_state_names (GtkSvg *self,
unsigned int *length)
{
g_return_val_if_fail (GTK_IS_SVG (self), NULL);
if (length)
*length = self->n_state_names;
return (const char **) self->state_names;
}
/**
* gtk_svg_set_features :
* @ self : an SVG paintable
* @ features : features to enable
*
* Enables or disables features of the SVG paintable .
*
* By default , all features are enabled .
*
* Note that this call only has an effect before the
* SVG is loaded .
*
* Since : 4 . 22
*/
void
gtk_svg_set_features (GtkSvg *self,
GtkSvgFeatures features)
{
g_return_if_fail (GTK_IS_SVG (self));
if (self->features == features)
return ;
self->features = features;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FEATURES]);
}
/**
* gtk_svg_get_features :
* @ self : an SVG paintable
*
* Returns the currently enabled features .
*
* Returns : the enabled features
*
* Since : 4 . 22
*/
GtkSvgFeatures
gtk_svg_get_features (GtkSvg *self)
{
g_return_val_if_fail (GTK_IS_SVG (self), GTK_SVG_DEFAULT_FEATURES);
return self->features;
}
/**
* gtk_svg_set_overflow :
* @ self : an SVG paintable
* @ overflow : the new overflow value
*
* Sets whether the rendering will be clipped
* to the bounds .
*
* Clipping is expected for [ iface @ Gdk . Paintable ]
* semantics , so this property should not be
* changed when using a ` GtkSvg ` as a paintable .
*
* Since : 4 . 24
*/
void
gtk_svg_set_overflow (GtkSvg *self,
GtkOverflow overflow)
{
g_return_if_fail (GTK_IS_SVG (self));
if (self->overflow == overflow)
return ;
self->overflow = overflow;
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_OVERFLOW]);
}
/**
* gtk_svg_get_overflow :
* @ self : an SVG paintable
*
* Gets the current overflow value .
*
* Returns : the current overflow value
*
* Since : 4 . 24
*/
GtkOverflow
gtk_svg_get_overflow (GtkSvg *self)
{
g_return_val_if_fail (GTK_IS_SVG (self), GTK_OVERFLOW_HIDDEN);
return self->overflow;
}
/**
* gtk_svg_set_stylesheet :
* @ self : an SVG paintable
* @ bytes : ( nullable ) : CSS data
*
* Sets a CSS user stylesheet to use .
*
* Note that styles are applied at load time ,
* so this function must be called before
* loading SVG .
*
* Since : 4 . 24
*/
void
gtk_svg_set_stylesheet (GtkSvg *self,
GBytes *bytes)
{
g_return_if_fail (GTK_IS_SVG (self));
g_clear_pointer (&self->stylesheet, g_bytes_unref);
if (bytes)
self->stylesheet = g_bytes_ref (bytes);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STYLESHEET]);
}
/**
* gtk_svg_get_stylesheet :
* @ self : an SVG paintable
*
* Gets the CSS user stylesheet .
*
* Returns : ( nullable ) : a ` GBytes ` with the CSS data
*
* Since : 4 . 24
*/
GBytes *
gtk_svg_get_stylesheet (GtkSvg *self)
{
g_return_val_if_fail (GTK_IS_SVG (self), NULL);
return self->stylesheet;
}
static void
setting_changed (GtkSvg *self)
{
gtk_svg_update_media (self);
self->style_changed = TRUE ;
}
/**
* gtk_svg_set_settings :
* @ self : an SVG paintable
* @ settings : ( nullable ) : the settings object
*
* Sets a settings object holding settinsg used when
* rendering the SVG .
*
* Since : 4 . 24
*/
void
gtk_svg_set_settings (GtkSvg *self,
GtkSettings *settings)
{
g_return_if_fail (GTK_IS_SVG (self));
if (self->settings == settings)
return ;
if (self->settings)
g_signal_handlers_disconnect_by_func (self->settings, setting_changed, self);
self->settings = settings;
if (settings)
{
g_signal_connect_swapped (self->settings, "notify::gtk-interface-color-scheme" ,
G_CALLBACK (setting_changed), self);
g_signal_connect_swapped (self->settings, "notify::gtk-interface-contrast" ,
G_CALLBACK (setting_changed), self);
g_signal_connect_swapped (self->settings, "notify::gtk-interface-reduced-motion" ,
G_CALLBACK (setting_changed), self);
}
setting_changed (self);
}
/* }}} */
/* {{{ Animation */
/**
* gtk_svg_set_frame_clock :
* @ self : an SVG paintable
* @ clock : the frame clock
*
* Sets a frame clock .
*
* Without a frame clock , GtkSvg will not advance animations .
*
* Since : 4 . 22
*/
void
gtk_svg_set_frame_clock (GtkSvg *self,
GdkFrameClock *clock)
{
g_return_if_fail (GTK_IS_SVG (self));
gboolean was_connected;
if (self->clock == clock)
return ;
was_connected = self->clock_update_id != 0 ;
frame_clock_disconnect (self);
g_set_object (&self->clock, clock);
if (self->clock && self->playing)
{
if (self->load_time == INDEFINITE)
gtk_svg_set_load_time (self, gdk_frame_clock_get_frame_time (clock));
else
gtk_svg_advance (self, gdk_frame_clock_get_frame_time (clock));
}
/* FIXME should account for the difference between old and
* new clock even when paused
*/
if (was_connected)
frame_clock_connect (self);
}
/**
* gtk_svg_play :
* @ self : an SVG paintable
*
* Start playing animations and state transitions .
*
* SvgAnimations can be paused and started repeatedly .
*
* Since : 4 . 22
*/
void
gtk_svg_play (GtkSvg *self)
{
g_return_if_fail (GTK_IS_SVG (self));
gtk_svg_set_playing (self, TRUE );
}
/**
* gtk_svg_pause :
* @ self : an SVG paintable
*
* Stop any playing animations and state transitions .
*
* SvgAnimations can be paused and started repeatedly .
*
* Since : 4 . 22
*/
void
gtk_svg_pause (GtkSvg *self)
{
g_return_if_fail (GTK_IS_SVG (self));
gtk_svg_set_playing (self, FALSE );
}
/* }}} */
/* }}} */
/* vim:set foldmethod=marker: */
Messung V0.5 in Prozent C=98 H=94 G=95
¤ Dauer der Verarbeitung: 0.73 Sekunden
(vorverarbeitet am 2026-07-03)
¤
*© Formatika GbR, Deutschland