gtk2/gtk/gtkcssshadowvalue.c
Alexander Mikhaylenko 46a9538b6a Shrink shadow extents
Long time ago, Cairo shadows in both GTK3 and 4 were drawn at a size about
twice their radius. Eventually this was fixed but the shadow extents are
still calculated for the previous size and appear unreasonably large: for
example, 141px for a 50px radius shadow. This can get very noticeable in
places such as invisible window frame which gets included into screenshots.

https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/3419 just divides the
radius by 2 when drawing a shadow with Cairo, do the same when calculating
extents.

See https://gitlab.gnome.org/GNOME/gtk/-/issues/3841
2021-08-04 14:47:17 +05:00

772 lines
22 KiB
C

/* GTK - The GIMP Toolkit
* Copyright (C) 2011 Red Hat, Inc.
*
* Author: Cosimo Cecchi <cosimoc@gnome.org>
*
* 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 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/>.
*/
#include "config.h"
#include "gtkcssshadowvalueprivate.h"
#include "gtkcsscolorvalueprivate.h"
#include "gtkcssnumbervalueprivate.h"
#include "gtkcsscolorvalueprivate.h"
#include "gtksnapshotprivate.h"
#include "gtkpango.h"
#include "gsk/gskcairoblurprivate.h"
#include "gsk/gskroundedrectprivate.h"
#include <math.h>
typedef struct {
guint inset :1;
GtkCssValue *hoffset;
GtkCssValue *voffset;
GtkCssValue *radius;
GtkCssValue *spread;
GtkCssValue *color;
} ShadowValue;
struct _GtkCssValue {
GTK_CSS_VALUE_BASE
guint is_filter : 1; /* values stored in radius are std_dev, for drop-shadow */
guint n_shadows;
ShadowValue shadows[1];
};
static GtkCssValue * gtk_css_shadow_value_new (ShadowValue *shadows,
guint n_shadows,
gboolean is_filter);
static void
shadow_value_for_transition (ShadowValue *result,
gboolean inset)
{
result->inset = inset;
result->hoffset = _gtk_css_number_value_new (0, GTK_CSS_PX);
result->voffset = _gtk_css_number_value_new (0, GTK_CSS_PX);
result->radius = _gtk_css_number_value_new (0, GTK_CSS_PX);
result->spread = _gtk_css_number_value_new (0, GTK_CSS_PX);
result->color = gtk_css_color_value_new_transparent ();
}
static void
shadow_value_unref (const ShadowValue *shadow)
{
_gtk_css_value_unref (shadow->hoffset);
_gtk_css_value_unref (shadow->voffset);
_gtk_css_value_unref (shadow->spread);
_gtk_css_value_unref (shadow->radius);
_gtk_css_value_unref (shadow->color);
}
static gboolean
shadow_value_transition (const ShadowValue *start,
const ShadowValue *end,
guint property_id,
double progress,
ShadowValue *result)
{
if (start->inset != end->inset)
return FALSE;
result->inset = start->inset;
result->hoffset = _gtk_css_value_transition (start->hoffset, end->hoffset, property_id, progress);
result->voffset = _gtk_css_value_transition (start->voffset, end->voffset, property_id, progress);
result->radius = _gtk_css_value_transition (start->radius, end->radius, property_id, progress);
result->spread = _gtk_css_value_transition (start->spread, end->spread, property_id, progress);
result->color = _gtk_css_value_transition (start->color, end->color, property_id, progress);
return TRUE;
}
static void
gtk_css_value_shadow_free (GtkCssValue *value)
{
guint i;
for (i = 0; i < value->n_shadows; i ++)
{
const ShadowValue *shadow = &value->shadows[i];
shadow_value_unref (shadow);
}
g_slice_free1 (sizeof (GtkCssValue) + sizeof (ShadowValue) * (value->n_shadows - 1), value);
}
static GtkCssValue *
gtk_css_value_shadow_compute (GtkCssValue *value,
guint property_id,
GtkStyleProvider *provider,
GtkCssStyle *style,
GtkCssStyle *parent_style)
{
guint i;
ShadowValue *shadows;
shadows = g_alloca (sizeof (ShadowValue) * value->n_shadows);
for (i = 0; i < value->n_shadows; i++)
{
const ShadowValue *shadow = &value->shadows[i];
shadows[i].hoffset = _gtk_css_value_compute (shadow->hoffset, property_id, provider, style, parent_style);
shadows[i].voffset = _gtk_css_value_compute (shadow->voffset, property_id, provider, style, parent_style);
shadows[i].radius = _gtk_css_value_compute (shadow->radius, property_id, provider, style, parent_style);
shadows[i].spread = _gtk_css_value_compute (shadow->spread, property_id, provider, style, parent_style),
shadows[i].color = _gtk_css_value_compute (shadow->color, property_id, provider, style, parent_style);
shadows[i].inset = shadow->inset;
}
return gtk_css_shadow_value_new (shadows, value->n_shadows, value->is_filter);
}
static gboolean
gtk_css_value_shadow_equal (const GtkCssValue *value1,
const GtkCssValue *value2)
{
guint i;
if (value1->n_shadows != value2->n_shadows)
return FALSE;
for (i = 0; i < value1->n_shadows; i++)
{
const ShadowValue *shadow1 = &value1->shadows[i];
const ShadowValue *shadow2 = &value2->shadows[i];
if (shadow1->inset != shadow2->inset ||
!_gtk_css_value_equal (shadow1->hoffset, shadow2->hoffset) ||
!_gtk_css_value_equal (shadow1->voffset, shadow2->voffset) ||
!_gtk_css_value_equal (shadow1->radius, shadow2->radius) ||
!_gtk_css_value_equal (shadow1->spread, shadow2->spread) ||
!_gtk_css_value_equal (shadow1->color, shadow2->color))
return FALSE;
}
return TRUE;
}
static GtkCssValue *
gtk_css_value_shadow_transition (GtkCssValue *start,
GtkCssValue *end,
guint property_id,
double progress)
{
guint i, len;
ShadowValue *shadows;
if (start->n_shadows > end->n_shadows)
len = start->n_shadows;
else
len = end->n_shadows;
shadows = g_newa (ShadowValue, len);
for (i = 0; i < MIN (start->n_shadows, end->n_shadows); i++)
{
if (!shadow_value_transition (&start->shadows[i], &end->shadows[i],
property_id, progress,
&shadows[i]))
{
while (i--)
shadow_value_unref (&shadows[i]);
return NULL;
}
}
if (start->n_shadows > end->n_shadows)
{
for (; i < len; i++)
{
ShadowValue fill;
shadow_value_for_transition (&fill, start->shadows[i].inset);
if (!shadow_value_transition (&start->shadows[i], &fill, property_id, progress, &shadows[i]))
{
while (i--)
shadow_value_unref (&shadows[i]);
shadow_value_unref (&fill);
return NULL;
}
shadow_value_unref (&fill);
}
}
else
{
for (; i < len; i++)
{
ShadowValue fill;
shadow_value_for_transition (&fill, end->shadows[i].inset);
if (!shadow_value_transition (&fill, &end->shadows[i], property_id, progress, &shadows[i]))
{
while (i--)
shadow_value_unref (&shadows[i]);
shadow_value_unref (&fill);
return NULL;
}
shadow_value_unref (&fill);
}
}
return gtk_css_shadow_value_new (shadows, len, start->is_filter);
}
static void
gtk_css_value_shadow_print (const GtkCssValue *value,
GString *string)
{
guint i;
if (value->n_shadows == 0)
{
g_string_append (string, "none");
return;
}
for (i = 0; i < value->n_shadows; i ++)
{
const ShadowValue *shadow = &value->shadows[i];
if (i > 0)
g_string_append (string, ", ");
_gtk_css_value_print (shadow->hoffset, string);
g_string_append_c (string, ' ');
_gtk_css_value_print (shadow->voffset, string);
g_string_append_c (string, ' ');
if (_gtk_css_number_value_get (shadow->radius, 100) != 0 ||
_gtk_css_number_value_get (shadow->spread, 100) != 0)
{
_gtk_css_value_print (shadow->radius, string);
g_string_append_c (string, ' ');
}
if (_gtk_css_number_value_get (shadow->spread, 100) != 0)
{
_gtk_css_value_print (shadow->spread, string);
g_string_append_c (string, ' ');
}
_gtk_css_value_print (shadow->color, string);
if (shadow->inset)
g_string_append (string, " inset");
}
}
static const GtkCssValueClass GTK_CSS_VALUE_SHADOW = {
"GtkCssShadowValue",
gtk_css_value_shadow_free,
gtk_css_value_shadow_compute,
gtk_css_value_shadow_equal,
gtk_css_value_shadow_transition,
NULL,
NULL,
gtk_css_value_shadow_print
};
static GtkCssValue shadow_none_singleton = { &GTK_CSS_VALUE_SHADOW, 1, TRUE, FALSE, 0 };
GtkCssValue *
gtk_css_shadow_value_new_none (void)
{
return _gtk_css_value_ref (&shadow_none_singleton);
}
static GtkCssValue *
gtk_css_shadow_value_new (ShadowValue *shadows,
guint n_shadows,
gboolean is_filter)
{
GtkCssValue *retval;
guint i;
if (n_shadows == 0)
return gtk_css_shadow_value_new_none ();
retval = _gtk_css_value_alloc (&GTK_CSS_VALUE_SHADOW, sizeof (GtkCssValue) + sizeof (ShadowValue) * (n_shadows - 1));
retval->n_shadows = n_shadows;
retval->is_filter = is_filter;
memcpy (retval->shadows, shadows, sizeof (ShadowValue) * n_shadows);
retval->is_computed = TRUE;
for (i = 0; i < n_shadows; i++)
{
const ShadowValue *shadow = &retval->shadows[i];
if (!gtk_css_value_is_computed (shadow->hoffset) ||
!gtk_css_value_is_computed (shadow->voffset) ||
!gtk_css_value_is_computed (shadow->spread) ||
!gtk_css_value_is_computed (shadow->radius) ||
!gtk_css_value_is_computed (shadow->color))
{
retval->is_computed = FALSE;
break;
}
}
return retval;
}
GtkCssValue *
gtk_css_shadow_value_new_filter (void)
{
ShadowValue value;
value.inset = FALSE;
value.hoffset = _gtk_css_number_value_new (0, GTK_CSS_NUMBER);
value.voffset = _gtk_css_number_value_new (0, GTK_CSS_NUMBER);
value.radius = _gtk_css_number_value_new (0, GTK_CSS_NUMBER);
value.spread = _gtk_css_number_value_new (0, GTK_CSS_NUMBER);
value.color = _gtk_css_color_value_new_current_color ();
return gtk_css_shadow_value_new (&value, 1, TRUE);
}
enum {
HOFFSET,
VOFFSET,
RADIUS,
SPREAD,
N_VALUES
};
static gboolean
has_inset (GtkCssParser *parser,
gpointer option_data,
gpointer box_shadow_mode)
{
return box_shadow_mode && gtk_css_parser_has_ident (parser, "inset");
}
static gboolean
parse_inset (GtkCssParser *parser,
gpointer option_data,
gpointer box_shadow_mode)
{
gboolean *inset = option_data;
if (!gtk_css_parser_try_ident (parser, "inset"))
{
g_assert_not_reached ();
return FALSE;
}
*inset = TRUE;
return TRUE;
}
static gboolean
parse_lengths (GtkCssParser *parser,
gpointer option_data,
gpointer box_shadow_mode)
{
GtkCssValue **values = option_data;
values[HOFFSET] = _gtk_css_number_value_parse (parser,
GTK_CSS_PARSE_LENGTH);
if (values[HOFFSET] == NULL)
return FALSE;
values[VOFFSET] = _gtk_css_number_value_parse (parser,
GTK_CSS_PARSE_LENGTH);
if (values[VOFFSET] == NULL)
return FALSE;
if (gtk_css_number_value_can_parse (parser))
{
values[RADIUS] = _gtk_css_number_value_parse (parser,
GTK_CSS_PARSE_LENGTH
| GTK_CSS_POSITIVE_ONLY);
if (values[RADIUS] == NULL)
return FALSE;
}
else
values[RADIUS] = _gtk_css_number_value_new (0.0, GTK_CSS_PX);
if (box_shadow_mode && gtk_css_number_value_can_parse (parser))
{
values[SPREAD] = _gtk_css_number_value_parse (parser,
GTK_CSS_PARSE_LENGTH);
if (values[SPREAD] == NULL)
return FALSE;
}
else
values[SPREAD] = _gtk_css_number_value_new (0.0, GTK_CSS_PX);
return TRUE;
}
static gboolean
parse_color (GtkCssParser *parser,
gpointer option_data,
gpointer box_shadow_mode)
{
GtkCssValue **color = option_data;
*color = _gtk_css_color_value_parse (parser);
if (*color == NULL)
return FALSE;
return TRUE;
}
static gboolean
gtk_css_shadow_value_parse_one (GtkCssParser *parser,
gboolean box_shadow_mode,
ShadowValue *result)
{
GtkCssValue *values[N_VALUES] = { NULL, };
GtkCssValue *color = NULL;
gboolean inset = FALSE;
GtkCssParseOption options[] =
{
{ (void *) gtk_css_number_value_can_parse, parse_lengths, values },
{ has_inset, parse_inset, &inset },
{ (void *) gtk_css_color_value_can_parse, parse_color, &color },
};
guint i;
if (!gtk_css_parser_consume_any (parser, options, G_N_ELEMENTS (options), GUINT_TO_POINTER (box_shadow_mode)))
goto fail;
if (values[0] == NULL)
{
gtk_css_parser_error_syntax (parser, "Expected shadow value to contain a length");
goto fail;
}
if (color == NULL)
color = _gtk_css_color_value_new_current_color ();
result->hoffset = values[HOFFSET];
result->voffset = values[VOFFSET];
result->spread = values[SPREAD];
result->radius = values[RADIUS];
result->color = color;
result->inset = inset;
return TRUE;
fail:
for (i = 0; i < N_VALUES; i++)
{
g_clear_pointer (&values[i], gtk_css_value_unref);
}
g_clear_pointer (&color, gtk_css_value_unref);
return FALSE;
}
#define MAX_SHADOWS 64
GtkCssValue *
gtk_css_shadow_value_parse (GtkCssParser *parser,
gboolean box_shadow_mode)
{
ShadowValue shadows[MAX_SHADOWS];
int n_shadows = 0;
int i;
if (gtk_css_parser_try_ident (parser, "none"))
return gtk_css_shadow_value_new_none ();
do {
if (n_shadows == MAX_SHADOWS)
{
gtk_css_parser_error_syntax (parser, "Not more than %d shadows supported", MAX_SHADOWS);
goto fail;
}
if (gtk_css_shadow_value_parse_one (parser, box_shadow_mode, &shadows[n_shadows]))
n_shadows++;
} while (gtk_css_parser_try_token (parser, GTK_CSS_TOKEN_COMMA));
return gtk_css_shadow_value_new (shadows, n_shadows, FALSE);
fail:
for (i = 0; i < n_shadows; i++)
{
const ShadowValue *shadow = &shadows[i];
shadow_value_unref (shadow);
}
return NULL;
}
GtkCssValue *
gtk_css_shadow_value_parse_filter (GtkCssParser *parser)
{
ShadowValue shadow;
if (gtk_css_shadow_value_parse_one (parser, FALSE, &shadow))
return gtk_css_shadow_value_new (&shadow, 1, TRUE);
else
return NULL;
}
void
gtk_css_shadow_value_get_extents (const GtkCssValue *value,
GtkBorder *border)
{
guint i;
memset (border, 0, sizeof (GtkBorder));
for (i = 0; i < value->n_shadows; i ++)
{
const ShadowValue *shadow = &value->shadows[i];
double hoffset, voffset, spread, radius, clip_radius;
spread = _gtk_css_number_value_get (shadow->spread, 0);
radius = _gtk_css_number_value_get (shadow->radius, 0);
if (!value->is_filter)
radius = radius / 2.0;
clip_radius = gsk_cairo_blur_compute_pixels (radius);
hoffset = _gtk_css_number_value_get (shadow->hoffset, 0);
voffset = _gtk_css_number_value_get (shadow->voffset, 0);
border->top = MAX (border->top, ceil (clip_radius + spread - voffset));
border->right = MAX (border->right, ceil (clip_radius + spread + hoffset));
border->bottom = MAX (border->bottom, ceil (clip_radius + spread + voffset));
border->left = MAX (border->left, ceil (clip_radius + spread - hoffset));
}
}
void
gtk_css_shadow_value_snapshot_outset (const GtkCssValue *value,
GtkSnapshot *snapshot,
const GskRoundedRect *border_box)
{
guint i;
double dx, dy, spread, radius;
const GdkRGBA *color;
g_return_if_fail (value->class == &GTK_CSS_VALUE_SHADOW);
for (i = 0; i < value->n_shadows; i ++)
{
const ShadowValue *shadow = &value->shadows[i];
if (shadow->inset)
continue;
color = gtk_css_color_value_get_rgba (shadow->color);
/* We don't need to draw invisible shadows */
if (gdk_rgba_is_clear (color))
continue;
dx = _gtk_css_number_value_get (shadow->hoffset, 0);
dy = _gtk_css_number_value_get (shadow->voffset, 0);
spread = _gtk_css_number_value_get (shadow->spread, 0);
radius = _gtk_css_number_value_get (shadow->radius, 0);
if (value->is_filter)
radius = 2 * radius;
gtk_snapshot_append_outset_shadow (snapshot, border_box, color, dx, dy, spread, radius);
}
}
void
gtk_css_shadow_value_snapshot_inset (const GtkCssValue *value,
GtkSnapshot *snapshot,
const GskRoundedRect *padding_box)
{
guint i;
double dx, dy, spread, radius;
const GdkRGBA *color;
g_return_if_fail (value->class == &GTK_CSS_VALUE_SHADOW);
for (i = 0; i < value->n_shadows; i ++)
{
const ShadowValue *shadow = &value->shadows[i];
if (!shadow->inset)
continue;
color = gtk_css_color_value_get_rgba (shadow->color);
/* We don't need to draw invisible shadows */
if (gdk_rgba_is_clear (color))
continue;
dx = _gtk_css_number_value_get (shadow->hoffset, 0);
dy = _gtk_css_number_value_get (shadow->voffset, 0);
spread = _gtk_css_number_value_get (shadow->spread, 0);
radius = _gtk_css_number_value_get (shadow->radius, 0);
if (value->is_filter)
radius = 2 * radius;
/* These are trivial to do with a color node */
if (spread == 0 && radius == 0 &&
gsk_rounded_rect_is_rectilinear (padding_box))
{
const graphene_rect_t *padding_bounds = &padding_box->bounds;
if (dx > 0)
{
const float y = dy > 0 ? dy : 0;
gtk_snapshot_append_color (snapshot, color,
&GRAPHENE_RECT_INIT (
padding_bounds->origin.x,
padding_bounds->origin.y + y,
dx,
padding_bounds->size.height - ABS (dy)
));
}
else if (dx < 0)
{
const float y = dy > 0 ? dy : 0;
gtk_snapshot_append_color (snapshot, color,
&GRAPHENE_RECT_INIT (
padding_bounds->origin.x + padding_bounds->size.width + dx,
padding_bounds->origin.y + y,
- dx,
padding_bounds->size.height - ABS (dy)
));
}
if (dy > 0)
{
gtk_snapshot_append_color (snapshot, color,
&GRAPHENE_RECT_INIT (
padding_bounds->origin.x,
padding_bounds->origin.y,
padding_bounds->size.width,
dy
));
}
else if (dy < 0)
{
gtk_snapshot_append_color (snapshot, color,
&GRAPHENE_RECT_INIT (
padding_bounds->origin.x,
padding_bounds->origin.y + padding_bounds->size.height + dy,
padding_bounds->size.width,
- dy
));
}
}
else
{
gtk_snapshot_append_inset_shadow (snapshot,
padding_box,
color,
dx, dy, spread, radius);
}
}
}
gboolean
gtk_css_shadow_value_is_clear (const GtkCssValue *value)
{
guint i;
if (!value)
return TRUE;
for (i = 0; i < value->n_shadows; i++)
{
const ShadowValue *shadow = &value->shadows[i];
if (!gdk_rgba_is_clear (gtk_css_color_value_get_rgba (shadow->color)))
return FALSE;
}
return TRUE;
}
gboolean
gtk_css_shadow_value_is_none (const GtkCssValue *shadow)
{
return !shadow || shadow->n_shadows == 0;
}
gboolean
gtk_css_shadow_value_push_snapshot (const GtkCssValue *value,
GtkSnapshot *snapshot)
{
gboolean need_shadow = FALSE;
guint i;
for (i = 0; i < value->n_shadows; i++)
{
const ShadowValue *shadow = &value->shadows[i];
if (!gdk_rgba_is_clear (gtk_css_color_value_get_rgba (shadow->color)))
{
need_shadow = TRUE;
break;
}
}
if (need_shadow)
{
GskShadow *shadows = g_newa (GskShadow, value->n_shadows);
for (i = 0; i < value->n_shadows; i++)
{
const ShadowValue *shadow = &value->shadows[i];
shadows[i].dx = _gtk_css_number_value_get (shadow->hoffset, 0);
shadows[i].dy = _gtk_css_number_value_get (shadow->voffset, 0);
shadows[i].color = *gtk_css_color_value_get_rgba (shadow->color);
shadows[i].radius = _gtk_css_number_value_get (shadow->radius, 0);
if (value->is_filter)
shadows[i].radius *= 2;
}
gtk_snapshot_push_shadow (snapshot, shadows, value->n_shadows);
}
return need_shadow;
}
void
gtk_css_shadow_value_pop_snapshot (const GtkCssValue *value,
GtkSnapshot *snapshot)
{
gboolean need_shadow = FALSE;
guint i;
for (i = 0; i < value->n_shadows; i++)
{
const ShadowValue *shadow = &value->shadows[i];
if (!gdk_rgba_is_clear (gtk_css_color_value_get_rgba (shadow->color)))
{
need_shadow = TRUE;
break;
}
}
if (need_shadow)
gtk_snapshot_pop (snapshot);
}