Add frame drawing API to GdkWindow

Existing code drawing on a GDK window has to handle the direct drawing
and the buffered drawing by itself, by checking the window type and
whether or not the window is backed by a native windowing surface. After
that, the calling code has to create a Cairo context from the window and
keep an association between the context and the window itself.

This is completely unnecessary: GDK can determine whether or not it
should use a backing store to draw on a GdkWindow as well as create a
Cairo context, and keep track of it.

This allows to simplify the calling code, and enforce some of the
drawing behavior we want to guarantee to users.

https://bugzilla.gnome.org/show_bug.cgi?id=766675
This commit is contained in:
Emmanuele Bassi 2016-05-20 16:55:12 +01:00
parent d6187c9a02
commit fc569f1ac6
4 changed files with 302 additions and 162 deletions

View File

@ -412,6 +412,9 @@ gdk_window_get_clip_region
gdk_window_begin_paint_rect
gdk_window_begin_paint_region
gdk_window_end_paint
gdk_window_begin_draw_frame
gdk_window_end_draw_fram
gdk_window_should_draw
gdk_window_get_visible_region
GdkWindowInvalidateHandlerFunc
gdk_window_set_invalidate_handler
@ -622,6 +625,7 @@ gdk_window_create_similar_surface
gdk_window_create_similar_image_surface
gdk_cairo_create
gdk_cairo_get_clip_rectangle
gdk_cairo_get_window
gdk_cairo_set_source_color
gdk_cairo_set_source_rgba
gdk_cairo_set_source_pixbuf

View File

@ -32,6 +32,8 @@ G_BEGIN_DECLS
GDK_AVAILABLE_IN_ALL
cairo_t * gdk_cairo_create (GdkWindow *window);
GDK_AVAILABLE_IN_3_22
GdkWindow * gdk_cairo_get_window (cairo_t *cr);
GDK_AVAILABLE_IN_ALL
gboolean gdk_cairo_get_clip_rectangle (cairo_t *cr,
GdkRectangle *rect);

View File

@ -2841,77 +2841,9 @@ gdk_window_create_gl_context (GdkWindow *window,
error);
}
/**
* gdk_window_begin_paint_rect:
* @window: a #GdkWindow
* @rectangle: rectangle you intend to draw to
*
* A convenience wrapper around gdk_window_begin_paint_region() which
* creates a rectangular region for you. See
* gdk_window_begin_paint_region() for details.
*
**/
void
gdk_window_begin_paint_rect (GdkWindow *window,
const GdkRectangle *rectangle)
{
cairo_region_t *region;
g_return_if_fail (GDK_IS_WINDOW (window));
region = cairo_region_create_rectangle (rectangle);
gdk_window_begin_paint_region (window, region);
cairo_region_destroy (region);
}
/**
* gdk_window_begin_paint_region:
* @window: a #GdkWindow
* @region: region you intend to draw to
*
* Indicates that you are beginning the process of redrawing @region.
* A backing store (offscreen buffer) large enough to contain @region
* will be created. The backing store will be initialized with the
* background color or background surface for @window. Then, all
* drawing operations performed on @window will be diverted to the
* backing store. When you call gdk_window_end_paint(), the backing
* store will be copied to @window, making it visible onscreen. Only
* the part of @window contained in @region will be modified; that is,
* drawing operations are clipped to @region.
*
* The net result of all this is to remove flicker, because the user
* sees the finished product appear all at once when you call
* gdk_window_end_paint(). If you draw to @window directly without
* calling gdk_window_begin_paint_region(), the user may see flicker
* as individual drawing operations are performed in sequence. The
* clipping and background-initializing features of
* gdk_window_begin_paint_region() are conveniences for the
* programmer, so you can avoid doing that work yourself.
*
* When using GTK+, the widget system automatically places calls to
* gdk_window_begin_paint_region() and gdk_window_end_paint() around
* emissions of the expose_event signal. That is, if youre writing an
* expose event handler, you can assume that the exposed area in
* #GdkEventExpose has already been cleared to the window background,
* is already set as the clip region, and already has a backing store.
* Therefore in most cases, application code need not call
* gdk_window_begin_paint_region(). (You can disable the automatic
* calls around expose events on a widget-by-widget basis by calling
* gtk_widget_set_double_buffered().)
*
* If you call this function multiple times before calling the
* matching gdk_window_end_paint(), the backing stores are pushed onto
* a stack. gdk_window_end_paint() copies the topmost backing store
* onscreen, subtracts the topmost region from all other regions in
* the stack, and pops the stack. All drawing operations affect only
* the topmost backing store in the stack. One matching call to
* gdk_window_end_paint() is required for each call to
* gdk_window_begin_paint_region().
*
**/
void
gdk_window_begin_paint_region (GdkWindow *window,
const cairo_region_t *region)
static void
gdk_window_begin_paint_internal (GdkWindow *window,
const cairo_region_t *region)
{
GdkRectangle clip_box;
GdkWindowImplClass *impl_class;
@ -2919,16 +2851,14 @@ gdk_window_begin_paint_region (GdkWindow *window,
gboolean needs_surface;
cairo_content_t surface_content;
g_return_if_fail (GDK_IS_WINDOW (window));
if (GDK_WINDOW_DESTROYED (window) ||
!gdk_window_has_impl (window))
return;
if (window->current_paint.surface != NULL)
{
g_warning ("gdk_window_begin_paint_region called while a paint was "
"alredy in progress. This is not allowed.");
g_warning ("A paint operation on the window is alredy in progress. "
"This is not allowed.");
return;
}
@ -3004,99 +2934,14 @@ gdk_window_begin_paint_region (GdkWindow *window,
gdk_window_clear_backing_region (window);
}
/**
* gdk_window_mark_paint_from_clip:
* @window: a #GdkWindow
* @cr: a #cairo_t
*
* If you call this during a paint (e.g. between gdk_window_begin_paint_region()
* and gdk_window_end_paint() then GDK will mark the current clip region of the
* window as being drawn. This is required when mixing GL rendering via
* gdk_cairo_draw_from_gl() and cairo rendering, as otherwise GDK has no way
* of knowing when something paints over the GL-drawn regions.
*
* This is typically called automatically by GTK+ and you don't need
* to care about this.
*
* Since: 3.16
**/
void
gdk_window_mark_paint_from_clip (GdkWindow *window,
cairo_t *cr)
{
cairo_region_t *clip_region;
GdkWindow *impl_window = window->impl_window;
if (impl_window->current_paint.surface == NULL ||
cairo_get_target (cr) != impl_window->current_paint.surface)
return;
if (cairo_region_is_empty (impl_window->current_paint.flushed_region))
return;
/* This here seems a bit weird, but basically, we're taking the current
clip and applying also the flushed region, and the result is that the
new clip is the intersection of these. This is the area where the newly
drawn region overlaps a previosly flushed area, which is an area of the
double buffer surface that need to be blended OVER the back buffer rather
than SRCed. */
cairo_save (cr);
/* We set the identity matrix here so we get and apply regions in native
window coordinates. */
cairo_identity_matrix (cr);
gdk_cairo_region (cr, impl_window->current_paint.flushed_region);
cairo_clip (cr);
clip_region = gdk_cairo_region_from_clip (cr);
if (clip_region == NULL)
{
/* Failed to represent clip as region, mark all as requiring
blend */
cairo_region_union (impl_window->current_paint.need_blend_region,
impl_window->current_paint.flushed_region);
cairo_region_destroy (impl_window->current_paint.flushed_region);
impl_window->current_paint.flushed_region = cairo_region_create ();
}
else
{
cairo_region_subtract (impl_window->current_paint.flushed_region, clip_region);
cairo_region_union (impl_window->current_paint.need_blend_region, clip_region);
}
cairo_region_destroy (clip_region);
/* Clear the area on the double buffer surface to transparent so we
can start drawing from scratch the area "above" the flushed
region */
cairo_set_source_rgba (cr, 0, 0, 0, 0);
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
cairo_paint (cr);
cairo_restore (cr);
}
/**
* gdk_window_end_paint:
* @window: a #GdkWindow
*
* Indicates that the backing store created by the most recent call
* to gdk_window_begin_paint_region() should be copied onscreen and
* deleted, leaving the next-most-recent backing store or no backing
* store at all as the active paint region. See
* gdk_window_begin_paint_region() for full details.
*
* It is an error to call this function without a matching
* gdk_window_begin_paint_region() first.
**/
void
gdk_window_end_paint (GdkWindow *window)
static void
gdk_window_end_paint_internal (GdkWindow *window)
{
GdkWindow *composited;
GdkWindowImplClass *impl_class;
GdkRectangle clip_box = { 0, };
cairo_t *cr;
g_return_if_fail (GDK_IS_WINDOW (window));
if (GDK_WINDOW_DESTROYED (window) ||
!gdk_window_has_impl (window))
return;
@ -3188,6 +3033,285 @@ gdk_window_end_paint (GdkWindow *window)
}
}
/**
* gdk_window_begin_paint_rect:
* @window: a #GdkWindow
* @rectangle: rectangle you intend to draw to
*
* A convenience wrapper around gdk_window_begin_paint_region() which
* creates a rectangular region for you. See
* gdk_window_begin_paint_region() for details.
*
**/
void
gdk_window_begin_paint_rect (GdkWindow *window,
const GdkRectangle *rectangle)
{
cairo_region_t *region;
g_return_if_fail (GDK_IS_WINDOW (window));
region = cairo_region_create_rectangle (rectangle);
gdk_window_begin_paint_internal (window, region);
cairo_region_destroy (region);
}
/**
* gdk_window_begin_paint_region:
* @window: a #GdkWindow
* @region: region you intend to draw to
*
* Indicates that you are beginning the process of redrawing @region.
* A backing store (offscreen buffer) large enough to contain @region
* will be created. The backing store will be initialized with the
* background color or background surface for @window. Then, all
* drawing operations performed on @window will be diverted to the
* backing store. When you call gdk_window_end_paint(), the backing
* store will be copied to @window, making it visible onscreen. Only
* the part of @window contained in @region will be modified; that is,
* drawing operations are clipped to @region.
*
* The net result of all this is to remove flicker, because the user
* sees the finished product appear all at once when you call
* gdk_window_end_paint(). If you draw to @window directly without
* calling gdk_window_begin_paint_region(), the user may see flicker
* as individual drawing operations are performed in sequence. The
* clipping and background-initializing features of
* gdk_window_begin_paint_region() are conveniences for the
* programmer, so you can avoid doing that work yourself.
*
* When using GTK+, the widget system automatically places calls to
* gdk_window_begin_paint_region() and gdk_window_end_paint() around
* emissions of the expose_event signal. That is, if youre writing an
* expose event handler, you can assume that the exposed area in
* #GdkEventExpose has already been cleared to the window background,
* is already set as the clip region, and already has a backing store.
* Therefore in most cases, application code need not call
* gdk_window_begin_paint_region(). (You can disable the automatic
* calls around expose events on a widget-by-widget basis by calling
* gtk_widget_set_double_buffered().)
*
* If you call this function multiple times before calling the
* matching gdk_window_end_paint(), the backing stores are pushed onto
* a stack. gdk_window_end_paint() copies the topmost backing store
* onscreen, subtracts the topmost region from all other regions in
* the stack, and pops the stack. All drawing operations affect only
* the topmost backing store in the stack. One matching call to
* gdk_window_end_paint() is required for each call to
* gdk_window_begin_paint_region().
*
**/
void
gdk_window_begin_paint_region (GdkWindow *window,
const cairo_region_t *region)
{
g_return_if_fail (GDK_IS_WINDOW (window));
gdk_window_begin_paint_internal (window, region);
}
static const cairo_user_data_key_t draw_context_window_key;
static void
gdk_cairo_set_window (cairo_t *cr,
GdkWindow *window)
{
cairo_set_user_data (cr, &draw_context_window_key, window, NULL);
}
/**
* gdk_cairo_get_window:
* @cr: a Cairo context created by gdk_window_begin_draw_frame()
*
* Retrieves the #GdkWindow that created the Cairo context @cr.
*
* Returns: (nullable) (transfer none): a #GdkWindow
*
* Since: 3.22
*/
GdkWindow *
gdk_cairo_get_window (cairo_t *cr)
{
g_return_val_if_fail (cr != NULL, NULL);
return cairo_get_user_data (cr, &draw_context_window_key);
}
/**
* gdk_window_begin_draw_frame:
* @window: a #GdkWindow
* @region: a Cairo region
*
* Indicates that you are beginning the process of redrawing @region
* on @window, and provides you with a Cairo context for drawing.
*
* If @window is a top level #GdkWindow, backed by a native window
* implementation, a backing store (offscreen buffer) large enough to
* contain @region will be created. The backing store will be initialized
* with the background color or background surface for @window. Then, all
* drawing operations performed on @window will be diverted to the
* backing store. When you call gdk_window_end_frame(), the contents of
* the backing store will be copied to @window, making it visible
* on screen. Only the part of @window contained in @region will be
* modified; that is, drawing operations are clipped to @region.
*
* The net result of all this is to remove flicker, because the user
* sees the finished product appear all at once when you call
* gdk_window_end_draw_frame(). If you draw to @window directly without
* calling gdk_window_begin_draw_frame(), the user may see flicker
* as individual drawing operations are performed in sequence.
*
* When using GTK+, the widget system automatically places calls to
* gdk_window_begin_draw_frame() and gdk_window_end_draw_frame() around
* emissions of the `GtkWidget::draw` signal. That is, if youre
* drawing the contents of the widget yourself, you can assume that the
* widget has a cleared background, is already set as the clip region,
* and already has a backing store. Therefore in most cases, application
* code in GTK does not need to call gdk_window_begin_draw_frame()
* explicitly.
*
* Returns: (transfer none): a Cairo context that should be used to
* draw the contents of the window; the returned context is owned
* by GDK and should not be destroyed directly
*
* Since: 3.22
*/
cairo_t *
gdk_window_begin_draw_frame (GdkWindow *window,
const cairo_region_t *region)
{
cairo_t *retval;
g_return_val_if_fail (GDK_IS_WINDOW (window), NULL);
if (gdk_window_has_native (window) && gdk_window_is_toplevel (window))
gdk_window_begin_paint_internal (window, region);
retval = gdk_cairo_create (window);
gdk_cairo_region (retval, region);
cairo_clip (retval);
return retval;
}
/**
* gdk_window_end_draw_frame:
* @window: a #GdkWindow
* @cr: the Cairo context created by gdk_window_begin_draw_frame()
*
* Indicates that the drawing of the contents of @window started with
* gdk_window_begin_frame() has been completed.
*
* This function will take care of destroying the Cairo context.
*
* It is an error to call this function without a matching
* gdk_window_begin_frame() first.
*
* Since: 3.22
*/
void
gdk_window_end_draw_frame (GdkWindow *window,
cairo_t *cr)
{
if (gdk_window_has_native (window) && gdk_window_is_toplevel (window))
gdk_window_end_paint_internal (window);
gdk_cairo_set_window (cr, NULL);
cairo_destroy (cr);
}
/**
* gdk_window_mark_paint_from_clip:
* @window: a #GdkWindow
* @cr: a #cairo_t
*
* If you call this during a paint (e.g. between gdk_window_begin_paint_region()
* and gdk_window_end_paint() then GDK will mark the current clip region of the
* window as being drawn. This is required when mixing GL rendering via
* gdk_cairo_draw_from_gl() and cairo rendering, as otherwise GDK has no way
* of knowing when something paints over the GL-drawn regions.
*
* This is typically called automatically by GTK+ and you don't need
* to care about this.
*
* Since: 3.16
**/
void
gdk_window_mark_paint_from_clip (GdkWindow *window,
cairo_t *cr)
{
cairo_region_t *clip_region;
GdkWindow *impl_window = window->impl_window;
if (impl_window->current_paint.surface == NULL ||
cairo_get_target (cr) != impl_window->current_paint.surface)
return;
if (cairo_region_is_empty (impl_window->current_paint.flushed_region))
return;
/* This here seems a bit weird, but basically, we're taking the current
clip and applying also the flushed region, and the result is that the
new clip is the intersection of these. This is the area where the newly
drawn region overlaps a previosly flushed area, which is an area of the
double buffer surface that need to be blended OVER the back buffer rather
than SRCed. */
cairo_save (cr);
/* We set the identity matrix here so we get and apply regions in native
window coordinates. */
cairo_identity_matrix (cr);
gdk_cairo_region (cr, impl_window->current_paint.flushed_region);
cairo_clip (cr);
clip_region = gdk_cairo_region_from_clip (cr);
if (clip_region == NULL)
{
/* Failed to represent clip as region, mark all as requiring
blend */
cairo_region_union (impl_window->current_paint.need_blend_region,
impl_window->current_paint.flushed_region);
cairo_region_destroy (impl_window->current_paint.flushed_region);
impl_window->current_paint.flushed_region = cairo_region_create ();
}
else
{
cairo_region_subtract (impl_window->current_paint.flushed_region, clip_region);
cairo_region_union (impl_window->current_paint.need_blend_region, clip_region);
}
cairo_region_destroy (clip_region);
/* Clear the area on the double buffer surface to transparent so we
can start drawing from scratch the area "above" the flushed
region */
cairo_set_source_rgba (cr, 0, 0, 0, 0);
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
cairo_paint (cr);
cairo_restore (cr);
}
/**
* gdk_window_end_paint:
* @window: a #GdkWindow
*
* Indicates that the backing store created by the most recent call
* to gdk_window_begin_paint_region() should be copied onscreen and
* deleted, leaving the next-most-recent backing store or no backing
* store at all as the active paint region. See
* gdk_window_begin_paint_region() for full details.
*
* It is an error to call this function without a matching
* gdk_window_begin_paint_region() first.
**/
void
gdk_window_end_paint (GdkWindow *window)
{
g_return_if_fail (GDK_IS_WINDOW (window));
gdk_window_end_paint_internal (window);
}
/**
* gdk_window_flush:
* @window: a #GdkWindow
@ -3358,6 +3482,8 @@ gdk_cairo_create (GdkWindow *window)
cr = cairo_create (surface);
gdk_cairo_set_window (cr, window);
if (window->impl_window->current_paint.region != NULL)
{
region = cairo_region_copy (window->impl_window->current_paint.region);

View File

@ -704,6 +704,14 @@ void gdk_window_begin_paint_region (GdkWindow *window,
const cairo_region_t *region);
GDK_AVAILABLE_IN_ALL
void gdk_window_end_paint (GdkWindow *window);
GDK_AVAILABLE_IN_3_22
cairo_t * gdk_window_begin_draw_frame (GdkWindow *window,
const cairo_region_t *region);
GDK_AVAILABLE_IN_3_22
void gdk_window_end_draw_frame (GdkWindow *window,
cairo_t *cr);
GDK_DEPRECATED_IN_3_14
void gdk_window_flush (GdkWindow *window);