diff --git a/src/st/st-theme-node-drawing.c b/src/st/st-theme-node-drawing.c index 59b3f84c0..52c737ef4 100644 --- a/src/st/st-theme-node-drawing.c +++ b/src/st/st-theme-node-drawing.c @@ -485,9 +485,233 @@ create_cairo_pattern_of_background_gradient (StThemeNode *node) return pattern; } +static cairo_pattern_t * +create_cairo_pattern_of_background_image (StThemeNode *node, + gboolean *needs_background_fill) +{ + cairo_surface_t *surface; + cairo_pattern_t *pattern; + cairo_content_t content; + cairo_matrix_t matrix; + const char *file; + double height_ratio, width_ratio; + int file_width; + int file_height; + + StTextureCache *texture_cache; + + file = st_theme_node_get_background_image (node); + + texture_cache = st_texture_cache_get_default (); + + surface = st_texture_cache_load_file_to_cairo_surface (texture_cache, file); + + if (surface == NULL) + return NULL; + + g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE); + + content = cairo_surface_get_content (surface); + pattern = cairo_pattern_create_for_surface (surface); + + file_width = cairo_image_surface_get_width (surface); + file_height = cairo_image_surface_get_height (surface); + + height_ratio = file_height / node->alloc_height; + width_ratio = file_width / node->alloc_width; + + *needs_background_fill = TRUE; + if ((file_width > node->alloc_width || file_height > node->alloc_height) + && !node->background_position_set) + { + double scale_factor; + double x_offset, y_offset; + + if (width_ratio > height_ratio) + { + double scaled_height; + + /* center vertically */ + + scale_factor = width_ratio; + scaled_height = file_height / scale_factor; + + x_offset = 0.; + y_offset = - (node->alloc_height / 2. - scaled_height / 2.); + } + else + { + double scaled_width; + + /* center horizontally */ + + scale_factor = height_ratio; + scaled_width = file_width / scale_factor; + + y_offset = 0.; + x_offset = - (node->alloc_width / 2. - scaled_width / 2.); + } + + cairo_matrix_init_translate (&matrix, x_offset, y_offset); + cairo_matrix_scale (&matrix, scale_factor, scale_factor); + + cairo_pattern_set_matrix (pattern, &matrix); + + /* If it's opaque, and when scaled, fills up the entire allocated + * area, then don't bother doing a background fill first + */ + if (content != CAIRO_CONTENT_COLOR_ALPHA && width_ratio == height_ratio) + *needs_background_fill = FALSE; + } + else + { + double x_offset, y_offset; + + if (node->background_position_set) + { + x_offset = -node->background_position_x; + y_offset = -node->background_position_y; + } + else + { + if (node->alloc_width > file_width) + x_offset = - (node->alloc_width / 2.0 - file_width / 2.0); + else + x_offset = - (file_width / 2.0 - node->alloc_width / 2.0); + + if (node->alloc_height > file_height) + y_offset = - (node->alloc_height / 2.0 - file_height / 2.0); + else + y_offset = - (file_height / 2.0 - node->alloc_height / 2.0); + } + + /* If it's opaque, and when translated, fills up the entire allocated + * area, then don't bother doing a background fill first + */ + if (content != CAIRO_CONTENT_COLOR_ALPHA + && -x_offset <= 0 + && -x_offset + file_width >= node->alloc_width + && -y_offset <= 0 + && -y_offset + file_height >= node->alloc_height) + *needs_background_fill = FALSE; + + cairo_matrix_init_translate (&matrix, x_offset, y_offset); + cairo_pattern_set_matrix (pattern, &matrix); + } + + return pattern; +} + +static void +paint_background_image_shadow_to_cairo_context (StThemeNode *node, + StShadow *shadow_spec, + cairo_pattern_t *pattern, + cairo_t *cr, + cairo_path_t *interior_path, + cairo_path_t *outline_path, + int x, + int y, + int width, + int height) +{ + cairo_pattern_t *shadow_pattern; + + g_assert (shadow_spec != NULL); + g_assert (pattern != NULL); + + if (outline_path != NULL) + { + cairo_surface_t *clipped_surface; + cairo_pattern_t *clipped_pattern; + cairo_t *temp_cr; + + /* Prerender the pattern to a temporary surface, + * so it's properly clipped before we create a shadow from it + */ + clipped_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + temp_cr = cairo_create (clipped_surface); + + cairo_set_operator (temp_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (temp_cr); + cairo_set_operator (temp_cr, CAIRO_OPERATOR_SOURCE); + + if (interior_path != NULL) + { + cairo_append_path (temp_cr, interior_path); + cairo_clip (temp_cr); + } + + cairo_append_path (temp_cr, outline_path); + cairo_translate (temp_cr, x, y); + cairo_set_source (temp_cr, pattern); + cairo_clip (temp_cr); + cairo_paint (temp_cr); + cairo_destroy (temp_cr); + + clipped_pattern = cairo_pattern_create_for_surface (clipped_surface); + cairo_surface_destroy (clipped_surface); + + shadow_pattern = _st_create_shadow_cairo_pattern (shadow_spec, + clipped_pattern); + cairo_pattern_destroy (clipped_pattern); + } + else + { + shadow_pattern = _st_create_shadow_cairo_pattern (shadow_spec, + pattern); + } + + /* Stamp the shadow pattern out in the appropriate color + * in a new layer + */ + cairo_push_group (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (cr); + + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source_rgba (cr, + shadow_spec->color.red / 255.0, + shadow_spec->color.green / 255.0, + shadow_spec->color.blue / 255.0, + shadow_spec->color.alpha / 255.0); + cairo_paint (cr); + + cairo_set_operator (cr, CAIRO_OPERATOR_DEST_IN); + + cairo_set_source (cr, shadow_pattern); + cairo_paint (cr); + cairo_pattern_destroy (shadow_pattern); + + cairo_pop_group_to_source (cr); + + /* mask and merge the shadow + */ + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + cairo_save (cr); + if (interior_path != NULL) + { + /* If there are borders, clip the shadow to the interior + * of the borders + */ + cairo_append_path (cr, interior_path); + cairo_clip (cr); + } + else if (outline_path != NULL) + { + /* If there is a visible outline, clip the shadow to + * that outline + */ + cairo_append_path (cr, outline_path); + cairo_clip (cr); + } + + cairo_paint (cr); + cairo_restore (cr); +} + /* In order for borders to be smoothly blended with non-solid backgrounds, * we need to use cairo. This function is a slow fallback path for those - * cases. It currently only supports gradients, however. + * cases (gradients, background images, etc). */ static CoglHandle st_theme_node_render_background_with_border (StThemeNode *node) @@ -497,21 +721,54 @@ st_theme_node_render_background_with_border (StThemeNode *node) int radius[4], i; cairo_t *cr; cairo_surface_t *surface; + StShadow *shadow_spec; cairo_pattern_t *pattern = NULL; - gboolean draw_solid_background; + cairo_path_t *outline_path = NULL; + gboolean draw_solid_background = TRUE; + gboolean draw_background_image_shadow = FALSE; + gboolean has_visible_outline; ClutterColor border_color; int border_width[4]; guint rowstride; guchar *data; + ClutterActorBox actor_box; + ClutterActorBox paint_box; + cairo_path_t *interior_path = NULL; border_image = st_theme_node_get_border_image (node); - rowstride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, node->alloc_width); - data = g_new0 (guchar, node->alloc_height * rowstride); + shadow_spec = st_theme_node_get_background_image_shadow (node); + + actor_box.x1 = 0; + actor_box.x2 = node->alloc_width; + actor_box.y1 = 0; + actor_box.y2 = node->alloc_height; + + /* If there's a background image shadow, we + * may need to create an image bigger than the nodes + * allocation + */ + st_theme_node_get_paint_box (node, &actor_box, &paint_box); + + /* translate the boxes so the paint box is at 0,0 + */ + actor_box.x1 += - paint_box.x1; + actor_box.x2 += - paint_box.x1; + actor_box.y1 += - paint_box.y1; + actor_box.y2 += - paint_box.y1; + + paint_box.x2 += - paint_box.x1; + paint_box.x1 += - paint_box.x1; + paint_box.y2 += - paint_box.y1; + paint_box.y1 += - paint_box.y1; + + rowstride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, + paint_box.x2 - paint_box.x1); + data = g_new0 (guchar, (paint_box.y2 - paint_box.y1) * rowstride); surface = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, - node->alloc_width, - node->alloc_height, + paint_box.x2 - paint_box.x1, + paint_box.y2 - paint_box.y1, rowstride); cr = cairo_create (surface); @@ -525,6 +782,9 @@ st_theme_node_render_background_with_border (StThemeNode *node) radius[i] = st_theme_node_get_border_radius (node, i); } + /* Note we don't support translucent background images on top + * of gradients. It's strictly either/or. + */ if (node->background_gradient_type != ST_GRADIENT_NONE) { pattern = create_cairo_pattern_of_background_gradient (node); @@ -532,38 +792,54 @@ st_theme_node_render_background_with_border (StThemeNode *node) } else { - g_warning ("st_theme_node_render_background_with_border called with non-gradient background (which isn't yet supported). Falling back to solid background color."); - draw_solid_background = TRUE; + const char *background_image; + + background_image = st_theme_node_get_background_image (node); + + if (background_image != NULL) + { + pattern = create_cairo_pattern_of_background_image (node, + &draw_solid_background); + if (shadow_spec && pattern != NULL) + draw_background_image_shadow = TRUE; + } } + if (pattern == NULL) + draw_solid_background = TRUE; + + has_visible_outline = st_theme_node_has_visible_outline (node); + /* Create a path for the background's outline first */ if (radius[ST_CORNER_TOPLEFT] > 0) cairo_arc (cr, - radius[ST_CORNER_TOPLEFT], - radius[ST_CORNER_TOPLEFT], + actor_box.x1 + radius[ST_CORNER_TOPLEFT], + actor_box.y1 + radius[ST_CORNER_TOPLEFT], radius[ST_CORNER_TOPLEFT], M_PI, 3 * M_PI / 2); else - cairo_move_to (cr, 0, 0); - cairo_line_to (cr, node->alloc_width - radius[ST_CORNER_TOPRIGHT], 0); + cairo_move_to (cr, actor_box.x1, actor_box.y1); + cairo_line_to (cr, actor_box.x2 - radius[ST_CORNER_TOPRIGHT], actor_box.x1); if (radius[ST_CORNER_TOPRIGHT] > 0) cairo_arc (cr, - node->alloc_width - radius[ST_CORNER_TOPRIGHT], - radius[ST_CORNER_TOPRIGHT], + actor_box.x2 - radius[ST_CORNER_TOPRIGHT], + actor_box.x1 + radius[ST_CORNER_TOPRIGHT], radius[ST_CORNER_TOPRIGHT], 3 * M_PI / 2, 2 * M_PI); - cairo_line_to (cr, node->alloc_width, node->alloc_height - radius[ST_CORNER_BOTTOMRIGHT]); + cairo_line_to (cr, actor_box.x2, actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT]); if (radius[ST_CORNER_BOTTOMRIGHT] > 0) cairo_arc (cr, - node->alloc_width - radius[ST_CORNER_BOTTOMRIGHT], - node->alloc_height - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.x2 - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT], radius[ST_CORNER_BOTTOMRIGHT], 0, M_PI / 2); - cairo_line_to (cr, radius[ST_CORNER_BOTTOMLEFT], node->alloc_height); + cairo_line_to (cr, actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], actor_box.y2); if (radius[ST_CORNER_BOTTOMLEFT] > 0) cairo_arc (cr, - radius[ST_CORNER_BOTTOMLEFT], - node->alloc_height - radius[ST_CORNER_BOTTOMLEFT], + actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], + actor_box.y2 - radius[ST_CORNER_BOTTOMLEFT], radius[ST_CORNER_BOTTOMLEFT], M_PI / 2, M_PI); cairo_close_path (cr); + outline_path = cairo_copy_path (cr); + /* If we have a solid border, we fill the outline shape with the border * color and create the inline shape for the background; * otherwise the outline shape is filled with the background @@ -585,68 +861,80 @@ st_theme_node_render_background_with_border (StThemeNode *node) if (radius[ST_CORNER_TOPLEFT] > MAX(border_width[ST_SIDE_TOP], border_width[ST_SIDE_LEFT])) elliptical_arc (cr, - radius[ST_CORNER_TOPLEFT], - radius[ST_CORNER_TOPLEFT], + actor_box.x1 + radius[ST_CORNER_TOPLEFT], + actor_box.y1 + radius[ST_CORNER_TOPLEFT], radius[ST_CORNER_TOPLEFT] - border_width[ST_SIDE_LEFT], radius[ST_CORNER_TOPLEFT] - border_width[ST_SIDE_TOP], M_PI, 3 * M_PI / 2); else cairo_move_to (cr, - border_width[ST_SIDE_LEFT], - border_width[ST_SIDE_TOP]); + actor_box.x1 + border_width[ST_SIDE_LEFT], + actor_box.y1 + border_width[ST_SIDE_TOP]); cairo_line_to (cr, - node->alloc_width - MAX(radius[ST_CORNER_TOPRIGHT], border_width[ST_SIDE_RIGHT]), - border_width[ST_SIDE_TOP]); + actor_box.x2 - MAX(radius[ST_CORNER_TOPRIGHT], border_width[ST_SIDE_RIGHT]), + actor_box.y1 + border_width[ST_SIDE_TOP]); if (radius[ST_CORNER_TOPRIGHT] > MAX(border_width[ST_SIDE_TOP], border_width[ST_SIDE_RIGHT])) elliptical_arc (cr, - node->alloc_width - radius[ST_CORNER_TOPRIGHT], - radius[ST_CORNER_TOPRIGHT], + actor_box.x2 - radius[ST_CORNER_TOPRIGHT], + actor_box.y1 + radius[ST_CORNER_TOPRIGHT], radius[ST_CORNER_TOPRIGHT] - border_width[ST_SIDE_RIGHT], radius[ST_CORNER_TOPRIGHT] - border_width[ST_SIDE_TOP], 3 * M_PI / 2, 2 * M_PI); else cairo_line_to (cr, - node->alloc_width - border_width[ST_SIDE_RIGHT], - border_width[ST_SIDE_TOP]); + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y1 + border_width[ST_SIDE_TOP]); cairo_line_to (cr, - node->alloc_width - border_width[ST_SIDE_RIGHT], - node->alloc_height - MAX(radius[ST_CORNER_BOTTOMRIGHT], border_width[ST_SIDE_BOTTOM])); + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y2 - MAX(radius[ST_CORNER_BOTTOMRIGHT], border_width[ST_SIDE_BOTTOM])); if (radius[ST_CORNER_BOTTOMRIGHT] > MAX(border_width[ST_SIDE_BOTTOM], border_width[ST_SIDE_RIGHT])) elliptical_arc (cr, - node->alloc_width - radius[ST_CORNER_BOTTOMRIGHT], - node->alloc_height - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.x2 - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT], radius[ST_CORNER_BOTTOMRIGHT] - border_width[ST_SIDE_RIGHT], radius[ST_CORNER_BOTTOMRIGHT] - border_width[ST_SIDE_BOTTOM], 0, M_PI / 2); else cairo_line_to (cr, - node->alloc_width - border_width[ST_SIDE_RIGHT], - node->alloc_height - border_width[ST_SIDE_BOTTOM]); + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); cairo_line_to (cr, MAX(radius[ST_CORNER_BOTTOMLEFT], border_width[ST_SIDE_LEFT]), - node->alloc_height - border_width[ST_SIDE_BOTTOM]); + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); if (radius[ST_CORNER_BOTTOMLEFT] > MAX(border_width[ST_SIDE_BOTTOM], border_width[ST_SIDE_LEFT])) elliptical_arc (cr, - radius[ST_CORNER_BOTTOMLEFT], - node->alloc_height - radius[ST_CORNER_BOTTOMLEFT], + actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], + actor_box.y2 - radius[ST_CORNER_BOTTOMLEFT], radius[ST_CORNER_BOTTOMLEFT] - border_width[ST_SIDE_LEFT], radius[ST_CORNER_BOTTOMLEFT] - border_width[ST_SIDE_BOTTOM], M_PI / 2, M_PI); else cairo_line_to (cr, - border_width[ST_SIDE_LEFT], - node->alloc_height - border_width[ST_SIDE_BOTTOM]); + actor_box.x1 + border_width[ST_SIDE_LEFT], + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); cairo_close_path (cr); + + interior_path = cairo_copy_path (cr); + + /* clip drawing to the region inside of the borders + */ + cairo_clip (cr); + + /* But fill the pattern as if it started at the edge of outline, + * behind the borders. This is similar to + * background-clip: border-box; semantics. + */ + cairo_append_path (cr, outline_path); } if (draw_solid_background) @@ -659,6 +947,23 @@ st_theme_node_render_background_with_border (StThemeNode *node) cairo_fill_preserve (cr); } + if (draw_background_image_shadow) + { + paint_background_image_shadow_to_cairo_context (node, + shadow_spec, + pattern, + cr, + interior_path, + has_visible_outline? outline_path : NULL, + actor_box.x1, + actor_box.y1, + paint_box.x2 - paint_box.x1, + paint_box.y2 - paint_box.y1); + cairo_append_path (cr, outline_path); + } + + cairo_translate (cr, actor_box.x1, actor_box.y1); + if (pattern != NULL) { cairo_set_source (cr, pattern); @@ -666,7 +971,14 @@ st_theme_node_render_background_with_border (StThemeNode *node) cairo_pattern_destroy (pattern); } - texture = cogl_texture_new_from_data (node->alloc_width, node->alloc_height, + if (outline_path != NULL) + cairo_path_destroy (outline_path); + + if (interior_path != NULL) + cairo_path_destroy (interior_path); + + texture = cogl_texture_new_from_data (paint_box.x2 - paint_box.x1, + paint_box.y2 - paint_box.y1, COGL_TEXTURE_NONE, #if G_BYTE_ORDER == G_LITTLE_ENDIAN COGL_PIXEL_FORMAT_BGRA_8888_PRE, @@ -745,6 +1057,8 @@ st_theme_node_render_resources (StThemeNode *node, { StTextureCache *texture_cache; StBorderImage *border_image; + gboolean has_border; + gboolean has_border_radius; StShadow *box_shadow_spec; StShadow *background_image_shadow_spec; const char *background_image; @@ -765,9 +1079,26 @@ st_theme_node_render_resources (StThemeNode *node, box_shadow_spec = st_theme_node_get_box_shadow (node); - /* Load referenced images from disk and draw anything we need with cairo now */ + if (node->border_width[ST_SIDE_TOP] > 0 || + node->border_width[ST_SIDE_LEFT] > 0 || + node->border_width[ST_SIDE_RIGHT] > 0 || + node->border_width[ST_SIDE_BOTTOM] > 0) + has_border = TRUE; + else + has_border = FALSE; + if (node->border_radius[ST_CORNER_TOPLEFT] > 0 || + node->border_radius[ST_CORNER_TOPRIGHT] > 0 || + node->border_radius[ST_CORNER_BOTTOMLEFT] > 0 || + node->border_radius[ST_CORNER_BOTTOMRIGHT] > 0) + has_border_radius = TRUE; + else + has_border_radius = FALSE; + + /* Load referenced images from disk and draw anything we need with cairo now */ + background_image = st_theme_node_get_background_image (node); border_image = st_theme_node_get_border_image (node); + if (border_image) { const char *filename; @@ -777,14 +1108,23 @@ st_theme_node_render_resources (StThemeNode *node, node->border_slices_texture = st_texture_cache_load_file_to_cogl_texture (texture_cache, filename); } - if (node->background_gradient_type != ST_GRADIENT_NONE) - node->prerendered_texture = st_theme_node_render_background_with_border (node); - if (node->border_slices_texture) node->border_slices_material = _st_create_texture_material (node->border_slices_texture); else node->border_slices_material = COGL_INVALID_HANDLE; + /* Use cairo to prerender the node if there is a gradient, or + * background image with borders and/or rounded corners, + * since we can't do those things easily with cogl. + * + * FIXME: if we could figure out ahead of time that a + * background image won't overlap with the node borders, + * then we could use cogl for that case. + */ + if ((node->background_gradient_type != ST_GRADIENT_NONE) + || (background_image && (has_border || has_border_radius))) + node->prerendered_texture = st_theme_node_render_background_with_border (node); + if (node->prerendered_texture) node->prerendered_material = _st_create_texture_material (node->prerendered_texture); else @@ -798,11 +1138,7 @@ st_theme_node_render_resources (StThemeNode *node, else if (node->prerendered_texture != COGL_INVALID_HANDLE) node->box_shadow_material = _st_create_shadow_material (box_shadow_spec, node->prerendered_texture); - else if (node->background_color.alpha > 0 || - node->border_width[ST_SIDE_TOP] > 0 || - node->border_width[ST_SIDE_LEFT] > 0 || - node->border_width[ST_SIDE_RIGHT] > 0 || - node->border_width[ST_SIDE_BOTTOM] > 0) + else if (node->background_color.alpha > 0 || has_border) { CoglHandle buffer, offscreen; @@ -829,10 +1165,8 @@ st_theme_node_render_resources (StThemeNode *node, } } - background_image = st_theme_node_get_background_image (node); background_image_shadow_spec = st_theme_node_get_background_image_shadow (node); - - if (background_image != NULL) + if (background_image != NULL && !has_border && !has_border_radius) { CoglHandle texture; @@ -1339,7 +1673,9 @@ st_theme_node_paint (StThemeNode *node, * not supported; the background color will be drawn with square * corners. * - The background image is drawn above the border color, not below it. - * - We don't clip the background image to the (rounded) border area. + * - We clip the background image to the inside edges of the border + * instead of the outside edges of the border (but position the image + * such that it's aligned to the outside edges) */ if (node->box_shadow_material) @@ -1352,7 +1688,15 @@ st_theme_node_paint (StThemeNode *node, node->border_slices_material != COGL_INVALID_HANDLE) { if (node->prerendered_material != COGL_INVALID_HANDLE) - paint_material_with_opacity (node->prerendered_material, &allocation, paint_opacity); + { + ClutterActorBox paint_box; + + st_theme_node_get_paint_box (node, &allocation, &paint_box); + + paint_material_with_opacity (node->prerendered_material, + &paint_box, + paint_opacity); + } if (node->border_slices_material != COGL_INVALID_HANDLE) st_theme_node_paint_sliced_border_image (node, &allocation, paint_opacity); @@ -1367,8 +1711,6 @@ st_theme_node_paint (StThemeNode *node, if (node->background_texture != COGL_INVALID_HANDLE) { ClutterActorBox background_box; - - get_background_position (node, &allocation, &background_box); gboolean has_visible_outline; /* If the background doesn't have a border or opaque background, @@ -1377,6 +1719,8 @@ st_theme_node_paint (StThemeNode *node, */ has_visible_outline = st_theme_node_has_visible_outline (node); + get_background_position (node, &allocation, &background_box); + if (has_visible_outline) cogl_clip_push_rectangle (allocation.x1, allocation.y1, allocation.x2, allocation.y2);