gnome-shell/src/st/st-private.c
Florian Müllner 7015bb2ca9 shadow: Only use correctly sized textures for shortcut
When creating a shadow for a ClutterTexture, we currently use the
underlying CoglTexture directly instead of rendering the actor to
an offscreen buffer. This assumes that the CoglTexture is directly
suitable as shadow source, which isn't necessarily the case - it
may have a very different size than what is shown and scaled up or
down by the hardware. In that case we end up with a scaled shadow
texture as well, which messes up the desired blur effect - the
result will be too light when scaling up, or too sharp when scaling
down. To fix this, only take the shortcut when a ClutterTexture's
underlying texture has the correct size and fall back to offscreen
rendering otherwise.

https://bugzilla.gnome.org/show_bug.cgi?id=788039
2017-09-22 17:11:28 +02:00

668 lines
22 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* st-private.h: Private declarations and functions
*
* Copyright 2009, 2010 Red Hat, Inc.
* Copyright 2010 Florian Müllner
* Copyright 2010 Intel Corporation
* Copyright 2010 Giovanni Campagna
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU Lesser General Public License,
* version 2.1, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
* more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <math.h>
#include <string.h>
#include "st-private.h"
/**
* _st_actor_get_preferred_width:
* @actor: a #ClutterActor
* @for_height: as with clutter_actor_get_preferred_width()
* @y_fill: %TRUE if @actor will fill its allocation vertically
* @min_width_p: as with clutter_actor_get_preferred_width()
* @natural_width_p: as with clutter_actor_get_preferred_width()
*
* Like clutter_actor_get_preferred_width(), but if @y_fill is %FALSE,
* then it will compute a width request based on the assumption that
* @actor will be given an allocation no taller than its natural
* height.
*/
void
_st_actor_get_preferred_width (ClutterActor *actor,
gfloat for_height,
gboolean y_fill,
gfloat *min_width_p,
gfloat *natural_width_p)
{
if (!y_fill && for_height != -1)
{
ClutterRequestMode mode;
gfloat natural_height;
mode = clutter_actor_get_request_mode (actor);
if (mode == CLUTTER_REQUEST_WIDTH_FOR_HEIGHT)
{
clutter_actor_get_preferred_height (actor, -1, NULL, &natural_height);
if (for_height > natural_height)
for_height = natural_height;
}
}
clutter_actor_get_preferred_width (actor, for_height, min_width_p, natural_width_p);
}
/**
* _st_actor_get_preferred_height:
* @actor: a #ClutterActor
* @for_width: as with clutter_actor_get_preferred_height()
* @x_fill: %TRUE if @actor will fill its allocation horizontally
* @min_height_p: as with clutter_actor_get_preferred_height()
* @natural_height_p: as with clutter_actor_get_preferred_height()
*
* Like clutter_actor_get_preferred_height(), but if @x_fill is
* %FALSE, then it will compute a height request based on the
* assumption that @actor will be given an allocation no wider than
* its natural width.
*/
void
_st_actor_get_preferred_height (ClutterActor *actor,
gfloat for_width,
gboolean x_fill,
gfloat *min_height_p,
gfloat *natural_height_p)
{
if (!x_fill && for_width != -1)
{
ClutterRequestMode mode;
gfloat natural_width;
mode = clutter_actor_get_request_mode (actor);
if (mode == CLUTTER_REQUEST_HEIGHT_FOR_WIDTH)
{
clutter_actor_get_preferred_width (actor, -1, NULL, &natural_width);
if (for_width > natural_width)
for_width = natural_width;
}
}
clutter_actor_get_preferred_height (actor, for_width, min_height_p, natural_height_p);
}
/**
* _st_set_text_from_style:
* @text: Target #ClutterText
* @theme_node: Source #StThemeNode
*
* Set various GObject properties of the @text object using
* CSS information from @theme_node.
*/
void
_st_set_text_from_style (ClutterText *text,
StThemeNode *theme_node)
{
ClutterColor color;
StTextDecoration decoration;
PangoAttrList *attribs = NULL;
const PangoFontDescription *font;
StTextAlign align;
st_theme_node_get_foreground_color (theme_node, &color);
clutter_text_set_color (text, &color);
font = st_theme_node_get_font (theme_node);
clutter_text_set_font_description (text, (PangoFontDescription *) font);
decoration = st_theme_node_get_text_decoration (theme_node);
if (decoration)
{
attribs = pango_attr_list_new ();
if (decoration & ST_TEXT_DECORATION_UNDERLINE)
{
PangoAttribute *underline = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
pango_attr_list_insert (attribs, underline);
}
if (decoration & ST_TEXT_DECORATION_LINE_THROUGH)
{
PangoAttribute *strikethrough = pango_attr_strikethrough_new (TRUE);
pango_attr_list_insert (attribs, strikethrough);
}
/* Pango doesn't have an equivalent attribute for _OVERLINE, and we deliberately
* skip BLINK (for now...)
*/
}
clutter_text_set_attributes (text, attribs);
if (attribs)
pango_attr_list_unref (attribs);
align = st_theme_node_get_text_align (theme_node);
if (align == ST_TEXT_ALIGN_JUSTIFY)
{
clutter_text_set_justify (text, TRUE);
clutter_text_set_line_alignment (text, PANGO_ALIGN_LEFT);
}
else
{
clutter_text_set_justify (text, FALSE);
clutter_text_set_line_alignment (text, (PangoAlignment) align);
}
}
/**
* _st_create_texture_pipeline:
* @src_texture: The CoglTexture for the pipeline
*
* Creates a simple pipeline which contains the given texture as a
* single layer.
*/
CoglPipeline *
_st_create_texture_pipeline (CoglTexture *src_texture)
{
static CoglPipeline *texture_pipeline_template = NULL;
CoglPipeline *pipeline;
g_return_val_if_fail (src_texture != NULL, NULL);
/* The only state used in the pipeline that would affect the shader
generation is the texture type on the layer. Therefore we create
a template pipeline which sets this state and all texture
pipelines are created as a copy of this. That way Cogl can find
the shader state for the pipeline more quickly by looking at the
pipeline ancestry instead of resorting to the shader cache. */
if (G_UNLIKELY (texture_pipeline_template == NULL))
{
CoglContext *ctx =
clutter_backend_get_cogl_context (clutter_get_default_backend ());
texture_pipeline_template = cogl_pipeline_new (ctx);
cogl_pipeline_set_layer_null_texture (texture_pipeline_template,
0, /* layer */
COGL_TEXTURE_TYPE_2D);
}
pipeline = cogl_pipeline_copy (texture_pipeline_template);
if (src_texture != NULL)
cogl_pipeline_set_layer_texture (pipeline, 0, src_texture);
return pipeline;
}
/*****
* Shadows
*****/
static gdouble *
calculate_gaussian_kernel (gdouble sigma,
guint n_values)
{
gdouble *ret, sum;
gdouble exp_divisor;
int half, i;
g_return_val_if_fail (sigma > 0, NULL);
half = n_values / 2;
ret = g_malloc (n_values * sizeof (gdouble));
sum = 0.0;
exp_divisor = 2 * sigma * sigma;
/* n_values of 1D Gauss function */
for (i = 0; i < (int)n_values; i++)
{
ret[i] = exp (-(i - half) * (i - half) / exp_divisor);
sum += ret[i];
}
/* normalize */
for (i = 0; i < (int)n_values; i++)
ret[i] /= sum;
return ret;
}
static guchar *
blur_pixels (guchar *pixels_in,
gint width_in,
gint height_in,
gint rowstride_in,
gdouble blur,
gint *width_out,
gint *height_out,
gint *rowstride_out)
{
guchar *pixels_out;
float sigma;
/* The CSS specification defines (or will define) the blur radius as twice
* the Gaussian standard deviation. See:
*
* http://lists.w3.org/Archives/Public/www-style/2010Sep/0002.html
*/
sigma = blur / 2.;
if ((guint) blur == 0)
{
*width_out = width_in;
*height_out = height_in;
*rowstride_out = rowstride_in;
pixels_out = g_memdup (pixels_in, *rowstride_out * *height_out);
}
else
{
gdouble *kernel;
guchar *line;
gint n_values, half;
gint x_in, y_in, x_out, y_out, i;
n_values = (gint) 5 * sigma;
half = n_values / 2;
*width_out = width_in + 2 * half;
*height_out = height_in + 2 * half;
*rowstride_out = (*width_out + 3) & ~3;
pixels_out = g_malloc0 (*rowstride_out * *height_out);
line = g_malloc0 (*rowstride_out);
kernel = calculate_gaussian_kernel (sigma, n_values);
/* vertical blur */
for (x_in = 0; x_in < width_in; x_in++)
for (y_out = 0; y_out < *height_out; y_out++)
{
guchar *pixel_in, *pixel_out;
gint i0, i1;
y_in = y_out - half;
/* We read from the source at 'y = y_in + i - half'; clamp the
* full i range [0, n_values) so that y is in [0, height_in).
*/
i0 = MAX (half - y_in, 0);
i1 = MIN (height_in + half - y_in, n_values);
pixel_in = pixels_in + (y_in + i0 - half) * rowstride_in + x_in;
pixel_out = pixels_out + y_out * *rowstride_out + (x_in + half);
for (i = i0; i < i1; i++)
{
*pixel_out += *pixel_in * kernel[i];
pixel_in += rowstride_in;
}
}
/* horizontal blur */
for (y_out = 0; y_out < *height_out; y_out++)
{
memcpy (line, pixels_out + y_out * *rowstride_out, *rowstride_out);
for (x_out = 0; x_out < *width_out; x_out++)
{
gint i0, i1;
guchar *pixel_out, *pixel_in;
/* We read from the source at 'x = x_out + i - half'; clamp the
* full i range [0, n_values) so that x is in [0, width_out).
*/
i0 = MAX (half - x_out, 0);
i1 = MIN (*width_out + half - x_out, n_values);
pixel_in = line + x_out + i0 - half;
pixel_out = pixels_out + *rowstride_out * y_out + x_out;
*pixel_out = 0;
for (i = i0; i < i1; i++)
{
*pixel_out += *pixel_in * kernel[i];
pixel_in++;
}
}
}
g_free (kernel);
g_free (line);
}
return pixels_out;
}
CoglPipeline *
_st_create_shadow_pipeline (StShadow *shadow_spec,
CoglTexture *src_texture)
{
ClutterBackend *backend = clutter_get_default_backend ();
CoglContext *ctx = clutter_backend_get_cogl_context (backend);
CoglError *error = NULL;
static CoglPipeline *shadow_pipeline_template = NULL;
CoglPipeline *pipeline;
CoglTexture *texture;
guchar *pixels_in, *pixels_out;
gint width_in, height_in, rowstride_in;
gint width_out, height_out, rowstride_out;
g_return_val_if_fail (shadow_spec != NULL, NULL);
g_return_val_if_fail (src_texture != NULL, NULL);
width_in = cogl_texture_get_width (src_texture);
height_in = cogl_texture_get_height (src_texture);
rowstride_in = (width_in + 3) & ~3;
pixels_in = g_malloc0 (rowstride_in * height_in);
cogl_texture_get_data (src_texture, COGL_PIXEL_FORMAT_A_8,
rowstride_in, pixels_in);
pixels_out = blur_pixels (pixels_in, width_in, height_in, rowstride_in,
shadow_spec->blur,
&width_out, &height_out, &rowstride_out);
g_free (pixels_in);
texture = COGL_TEXTURE (cogl_texture_2d_new_from_data (ctx, width_out, height_out,
COGL_PIXEL_FORMAT_A_8,
rowstride_out,
pixels_out,
&error));
if (error)
{
g_warning ("Failed to allocate texture: %s", error->message);
cogl_error_free (error);
}
g_free (pixels_out);
if (G_UNLIKELY (shadow_pipeline_template == NULL))
{
shadow_pipeline_template = cogl_pipeline_new (ctx);
/* We set up the pipeline to blend the shadow texture with the combine
* constant, but defer setting the latter until painting, so that we can
* take the actor's overall opacity into account. */
cogl_pipeline_set_layer_combine (shadow_pipeline_template, 0,
"RGBA = MODULATE (CONSTANT, TEXTURE[A])",
NULL);
}
pipeline = cogl_pipeline_copy (shadow_pipeline_template);
cogl_pipeline_set_layer_texture (pipeline, 0, texture);
if (texture)
cogl_object_unref (texture);
return pipeline;
}
CoglPipeline *
_st_create_shadow_pipeline_from_actor (StShadow *shadow_spec,
ClutterActor *actor)
{
CoglPipeline *shadow_pipeline = NULL;
ClutterActorBox box;
float width, height;
clutter_actor_get_allocation_box (actor, &box);
clutter_actor_box_get_size (&box, &width, &height);
if (width == 0 || height == 0)
return NULL;
if (CLUTTER_IS_TEXTURE (actor))
{
CoglTexture *texture;
texture = clutter_texture_get_cogl_texture (CLUTTER_TEXTURE (actor));
if (texture &&
cogl_texture_get_width (texture) == width &&
cogl_texture_get_height (texture) == height)
shadow_pipeline = _st_create_shadow_pipeline (shadow_spec, texture);
}
if (shadow_pipeline == NULL)
{
CoglTexture *buffer;
CoglOffscreen *offscreen;
CoglFramebuffer *fb;
CoglColor clear_color;
CoglError *catch_error = NULL;
buffer = cogl_texture_new_with_size (width,
height,
COGL_TEXTURE_NO_SLICING,
COGL_PIXEL_FORMAT_ANY);
if (buffer == NULL)
return NULL;
offscreen = cogl_offscreen_new_with_texture (buffer);
fb = COGL_FRAMEBUFFER (offscreen);
if (!cogl_framebuffer_allocate (fb, &catch_error))
{
cogl_error_free (catch_error);
cogl_object_unref (offscreen);
cogl_object_unref (buffer);
return NULL;
}
cogl_color_init_from_4ub (&clear_color, 0, 0, 0, 0);
/* XXX: There's no way to render a ClutterActor to an offscreen
* as it uses the implicit API. */
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
cogl_push_framebuffer (fb);
G_GNUC_END_IGNORE_DEPRECATIONS;
cogl_framebuffer_clear (fb, COGL_BUFFER_BIT_COLOR, &clear_color);
cogl_framebuffer_translate (fb, -box.x1, -box.y1, 0);
cogl_framebuffer_orthographic (fb, 0, 0, width, height, 0, 1.0);
clutter_actor_set_opacity_override (actor, 255);
clutter_actor_paint (actor);
clutter_actor_set_opacity_override (actor, -1);
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
cogl_pop_framebuffer ();
G_GNUC_END_IGNORE_DEPRECATIONS;
cogl_object_unref (fb);
shadow_pipeline = _st_create_shadow_pipeline (shadow_spec, buffer);
cogl_object_unref (buffer);
}
return shadow_pipeline;
}
/**
* _st_create_shadow_cairo_pattern:
* @shadow_spec: the definition of the shadow
* @src_pattern: surface pattern for which we create the shadow
* (must be a surface pattern)
*
* This is a utility function for creating shadows used by
* st-theme-node.c; it's in this file to share the gaussian
* blur implementation. The usage of this function is quite different
* depending on whether shadow_spec->inset is %TRUE or not. If
* shadow_spec->inset is %TRUE, the caller should pass in a @src_pattern
* which is the <i>inverse</i> of what they want shadowed, and must take
* care of the spread and offset from the shadow spec themselves. If
* shadow_spec->inset is %FALSE then the caller should pass in what they
* want shadowed directly, and this function takes care of the spread and
* the offset.
*/
cairo_pattern_t *
_st_create_shadow_cairo_pattern (StShadow *shadow_spec,
cairo_pattern_t *src_pattern)
{
static cairo_user_data_key_t shadow_pattern_user_data;
cairo_t *cr;
cairo_surface_t *src_surface;
cairo_surface_t *surface_in;
cairo_surface_t *surface_out;
cairo_pattern_t *dst_pattern;
guchar *pixels_in, *pixels_out;
gint width_in, height_in, rowstride_in;
gint width_out, height_out, rowstride_out;
cairo_matrix_t shadow_matrix;
int i, j;
g_return_val_if_fail (shadow_spec != NULL, NULL);
g_return_val_if_fail (src_pattern != NULL, NULL);
if (cairo_pattern_get_surface (src_pattern, &src_surface) != CAIRO_STATUS_SUCCESS)
/* The most likely reason we can't get the pattern is that sizing went hairwire
* and the caller tried to create a surface too big for memory, leaving us with
* a pattern in an error state; we return a transparent pattern for the shadow.
*/
return cairo_pattern_create_rgba(1.0, 1.0, 1.0, 0.0);
width_in = cairo_image_surface_get_width (src_surface);
height_in = cairo_image_surface_get_height (src_surface);
/* We want the output to be a color agnostic alpha mask,
* so we need to strip the color channels from the input
*/
if (cairo_image_surface_get_format (src_surface) != CAIRO_FORMAT_A8)
{
surface_in = cairo_image_surface_create (CAIRO_FORMAT_A8,
width_in, height_in);
cr = cairo_create (surface_in);
cairo_set_source_surface (cr, src_surface, 0, 0);
cairo_paint (cr);
cairo_destroy (cr);
}
else
{
surface_in = cairo_surface_reference (src_surface);
}
pixels_in = cairo_image_surface_get_data (surface_in);
rowstride_in = cairo_image_surface_get_stride (surface_in);
pixels_out = blur_pixels (pixels_in, width_in, height_in, rowstride_in,
shadow_spec->blur,
&width_out, &height_out, &rowstride_out);
cairo_surface_destroy (surface_in);
/* Invert pixels for inset shadows */
if (shadow_spec->inset)
{
for (j = 0; j < height_out; j++)
{
guchar *p = pixels_out + rowstride_out * j;
for (i = 0; i < width_out; i++, p++)
*p = ~*p;
}
}
surface_out = cairo_image_surface_create_for_data (pixels_out,
CAIRO_FORMAT_A8,
width_out,
height_out,
rowstride_out);
cairo_surface_set_user_data (surface_out, &shadow_pattern_user_data,
pixels_out, (cairo_destroy_func_t) g_free);
dst_pattern = cairo_pattern_create_for_surface (surface_out);
cairo_surface_destroy (surface_out);
cairo_pattern_get_matrix (src_pattern, &shadow_matrix);
if (shadow_spec->inset)
{
/* For inset shadows, offsets and spread radius have already been
* applied to the original pattern, so all left to do is shift the
* blurred image left, so that it aligns centered under the
* unblurred one
*/
cairo_matrix_translate (&shadow_matrix,
(width_out - width_in) / 2.0,
(height_out - height_in) / 2.0);
cairo_pattern_set_matrix (dst_pattern, &shadow_matrix);
return dst_pattern;
}
/* Read all the code from the cairo_pattern_set_matrix call
* at the end of this function to here from bottom to top,
* because each new affine transformation is applied in
* front of all the previous ones */
/* 6. Invert the matrix back */
cairo_matrix_invert (&shadow_matrix);
/* 5. Adjust based on specified offsets */
cairo_matrix_translate (&shadow_matrix,
shadow_spec->xoffset,
shadow_spec->yoffset);
/* 4. Recenter the newly scaled image */
cairo_matrix_translate (&shadow_matrix,
- shadow_spec->spread,
- shadow_spec->spread);
/* 3. Scale up the blurred image to fill the spread */
cairo_matrix_scale (&shadow_matrix,
(width_in + 2.0 * shadow_spec->spread) / width_in,
(height_in + 2.0 * shadow_spec->spread) / height_in);
/* 2. Shift the blurred image left, so that it aligns centered
* under the unblurred one */
cairo_matrix_translate (&shadow_matrix,
- (width_out - width_in) / 2.0,
- (height_out - height_in) / 2.0);
/* 1. Invert the matrix so we can work with it in pattern space
*/
cairo_matrix_invert (&shadow_matrix);
cairo_pattern_set_matrix (dst_pattern, &shadow_matrix);
return dst_pattern;
}
void
_st_paint_shadow_with_opacity (StShadow *shadow_spec,
CoglPipeline *shadow_pipeline,
ClutterActorBox *box,
guint8 paint_opacity)
{
CoglFramebuffer *fb = cogl_get_draw_framebuffer ();
ClutterActorBox shadow_box;
CoglColor color;
g_return_if_fail (shadow_spec != NULL);
g_return_if_fail (shadow_pipeline != NULL);
st_shadow_get_box (shadow_spec, box, &shadow_box);
cogl_color_init_from_4ub (&color,
shadow_spec->color.red * paint_opacity / 255,
shadow_spec->color.green * paint_opacity / 255,
shadow_spec->color.blue * paint_opacity / 255,
shadow_spec->color.alpha * paint_opacity / 255);
cogl_color_premultiply (&color);
cogl_pipeline_set_layer_combine_constant (shadow_pipeline, 0, &color);
cogl_framebuffer_draw_rectangle (fb, shadow_pipeline,
shadow_box.x1, shadow_box.y1,
shadow_box.x2, shadow_box.y2);
}