#include #include #include "backingstore.h" typedef struct InputPath InputPath; typedef struct InputRegion InputRegion; struct BackingStore { GdkPixmap * pixmap; GdkRegion * update_region; int width; int height; /* Input */ InputRegion * current_input; GPtrArray * input_regions; int current_x; int current_y; BackingStoreEventFunc last_func; gpointer last_data; int last_x; int last_y; gboolean last_was_motion; gboolean implicit_grab; gboolean grabbed; BackingStoreEventFunc grab_func; gpointer grab_data; guint32 motion_notify_time; gboolean need_fake_motion; gboolean in_process_updates; }; struct InputPath { cairo_fill_rule_t fill_rule; double line_width; cairo_path_t *path; gboolean is_stroke; BackingStoreEventFunc func; gpointer data; }; /* InputRegions are mutually disjoint */ struct InputRegion { GdkRegion *region; GArray *paths; }; void backing_store_free (BackingStore *store) { g_object_unref (store->pixmap); gdk_region_destroy (store->update_region); g_free (store); } void backing_store_move_area (BackingStore *store, int x, int y, int width, int height, int dx, int dy) { GdkRegion *moving_invalid; GdkRegion *region; GdkRectangle rect; GdkGC *gc; gc = gdk_gc_new (store->pixmap); gdk_draw_drawable (store->pixmap, gc, store->pixmap, x, y, x + dx, y + dy, width, height); g_object_unref (gc); rect.x = x; rect.y = y; rect.width = width; rect.height = height; region = gdk_region_rectangle (&rect); /* Compute the invalid part of the region that moves */ moving_invalid = gdk_region_copy (region); gdk_region_intersect (moving_invalid, store->update_region); /* Invalidate the source rectangle */ gdk_region_union (store->update_region, region); /* Uninvalidate the destination rectangle */ gdk_region_offset (region, dx, dy); gdk_region_subtract (store->update_region, region); /* Invalidate the invalid parts that moved */ gdk_region_union (store->update_region, moving_invalid); gdk_region_destroy (moving_invalid); gdk_region_destroy (region); /* Intersect invalid area with store extents */ rect.x = 0; rect.y = 0; rect.width = store->width; rect.height = store->height; region = gdk_region_rectangle (&rect); gdk_region_intersect (store->update_region, region); gdk_region_destroy (region); /* FIXME: move the input areas instead */ backing_store_invalidate_all (store); if (!gdk_region_empty (store->update_region)) store->need_fake_motion = TRUE; } static void process_event (BackingStore *store, BackingStoreEventType input_type, int x, int y, int button); void backing_store_scroll (BackingStore *store, int dx, int dy) { backing_store_move_area (store, 0, 0, store->width, store->height, dx, dy); } static void print_region (const char *header, GdkRegion *region) { GdkRectangle *rects; int n_rects; int i; g_print ("%s\n", header); gdk_region_get_rectangles (region, &rects, &n_rects); for (i = 0; i < n_rects; ++i) { GdkRectangle *rect = &(rects[i]); g_print (" %d %d %d %d\n", rect->x, rect->y, rect->width, rect->height); } g_free (rects); } static void begin_grab_internal (BackingStore *store, gboolean implicit, BackingStoreEventFunc func, gpointer data) { #if 0 g_print ("begin%s grab\n", implicit? " implicit" : ""); #endif /* FIXME: we should probably take a server grab */ /* Also, there should be support for setting the grab cursor */ /* Grabs can be overridden if desired - FIXME: make sure this right */ /* We may need a grab counter here if there are both implicit and * explicit grabs in play at the same time */ store->implicit_grab = implicit; store->grabbed = TRUE; store->grab_func = func; store->grab_data = data; } static void end_grab_internal (BackingStore *store, gboolean only_implicit) { if (only_implicit && !store->implicit_grab) return; #if 0 g_print ("end grab\n"); #endif store->implicit_grab = FALSE; store->grabbed = FALSE; store->grab_func = NULL; store->grab_data = NULL; } static void emit_input (BackingStore *store, BackingStoreEventType type, int x, int y, int button, BackingStoreEventFunc func, gpointer data) { BackingStoreEvent event; gboolean finished_grab = FALSE; event.x = x; event.y = y; event.button = button; #if 0 if (store->grabbed) g_print ("grabbed\n"); #endif /* Handle crossing events */ if (store->last_func != func || store->last_data != data) { if (!store->grabbed || (store->last_func == store->grab_func && store->last_data == store->grab_data)) { event.type = BACKING_STORE_LEAVE; if (store->last_func) { #if 0 g_print ("leave: %d\n", ec); g_print ("leave %p -> %p\n", store->last_func, func); #endif store->last_func (store, &event, store->last_data); } } if (!store->grabbed || (func == store->grab_func && data == store->grab_data)) { event.type = BACKING_STORE_ENTER; if (func) { #if 0 g_print ("enter %d\n", ec); #endif func (store, &event, data); } } } event.type = type; if (event.type != BACKING_STORE_NONE) { BackingStoreEventFunc emit_func; gpointer emit_data; if (store->grabbed) { emit_func = store->grab_func; emit_data = store->grab_data; } else { emit_func = func; emit_data = data; } if (emit_func) { if (event.type == BACKING_STORE_BUTTON_PRESS) begin_grab_internal (store, TRUE, emit_func, emit_data); #if 0 g_print ("other %d\n", ec); #endif emit_func (store, &event, emit_data); if (event.type == BACKING_STORE_BUTTON_RELEASE) { end_grab_internal (store, TRUE); finished_grab = TRUE; } } } store->last_func = func; store->last_data = data; store->last_x = x; store->last_y = y; store->last_was_motion = (event.type == BACKING_STORE_MOTION); if (finished_grab) { /* When a grab has finished, fake a motion to where * the last grabbed event happened. */ process_event (store, BACKING_STORE_MOTION, x, y, 0); } } static void process_event (BackingStore *store, BackingStoreEventType input_type, int x, int y, int button) { int i; for (i = 0; i < store->input_regions->len; ++i) { InputRegion *region = store->input_regions->pdata[i]; if (gdk_region_point_in (region->region, x, y)) { int j; /* Iterate backwards since the topmost paths are at * the back. */ for (j = region->paths->len - 1; j >= 0; --j) { InputPath *path = &g_array_index (region->paths, InputPath, j); cairo_t *cr; gboolean inside; if (store->grabbed && (path->func != store->grab_func || path->data != store->grab_data)) { /* When there is a grab in place we only care * about input regions for the purposes of * generating enter events for the grab * function. */ continue; } cr = gdk_cairo_create (store->pixmap); cairo_set_fill_rule (cr, path->fill_rule); cairo_set_line_width (cr, path->line_width); cairo_append_path (cr, path->path); if (path->is_stroke) inside = cairo_in_stroke (cr, x, y); else inside = cairo_in_fill (cr, x, y); cairo_destroy (cr); if (inside) { emit_input (store, input_type, x, y, button, path->func, path->data); return; } } /* Since the regions are all disjoint, no other region * can match. Of course we could be clever and try and * sort the regions, but so far I have been unable to * make this loop show up on a profile. */ break; } } /* If we got here, no event was emitted, so we emit * an event with a NULL event function. This will cause some * internal bookkeeping to be updated, and an event to be * emitted if a grab happens to be active. */ emit_input (store, input_type, x, y, button, NULL, NULL); } typedef struct { Window window; guint32 last_motion_time; gboolean stop_compressing; } EventScannerData; static Bool get_motion_time (Display *dpy, XEvent *xevent, XPointer arg) { EventScannerData *esd = (EventScannerData *)arg; if (esd->stop_compressing) return FALSE; if (esd->window != xevent->xany.window) { esd->stop_compressing = TRUE; return FALSE; } if (xevent->type == MotionNotify) { esd->last_motion_time = xevent->xmotion.time; return FALSE; } if (xevent->type == EnterNotify || xevent->type == LeaveNotify) { esd->last_motion_time = xevent->xcrossing.time; return FALSE; } return FALSE; } static gboolean use_this_motion_notify (GdkWindow *window, guint32 event_time, guint32 *motion_notify_time) { if (*motion_notify_time) { if (event_time == *motion_notify_time) { *motion_notify_time = 0; return TRUE; } else { /* There are more motion events already queued up */ return FALSE; } } else { Display *dpy = gdk_x11_get_default_xdisplay(); EventScannerData esd = { GDK_WINDOW_XWINDOW (window), 0, FALSE }; XEvent dummy; XCheckIfEvent (dpy, &dummy, get_motion_time, (XPointer)&esd); if (esd.last_motion_time == 0) { return TRUE; } else { *motion_notify_time = esd.last_motion_time; return FALSE; } } } /* Input */ void backing_store_process_event (BackingStore *store, GdkEvent *event) { BackingStoreEventType input_type = BACKING_STORE_NONE; int button = 0; int x, y; if (event->type == GDK_BUTTON_PRESS) { input_type = BACKING_STORE_BUTTON_PRESS; button = event->button.button; x = event->button.x; y = event->button.y; } else if (event->type == GDK_BUTTON_RELEASE) { input_type = BACKING_STORE_BUTTON_RELEASE; button = event->button.button; x = event->button.x; y = event->button.y; } else if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_LEAVE_NOTIFY || event->type == GDK_ENTER_NOTIFY) { GdkWindow *window; guint32 time; input_type = BACKING_STORE_MOTION; button = 0; if (event->type == GDK_MOTION_NOTIFY) { x = event->motion.x; y = event->motion.y; window = event->motion.window; time = event->motion.time; } else { x = event->crossing.x; y = event->crossing.y; window = event->crossing.window; time = event->crossing.time; } if (!use_this_motion_notify (window, time, &store->motion_notify_time)) return; } else { return; } process_event (store, input_type, x, y, button); store->current_x = x; store->current_y = y; } static void user_to_device (double *x, double *y, gpointer data) { cairo_surface_t *surface = cairo_get_target (data); double dev_x; double dev_y; cairo_surface_get_device_offset (surface, &dev_x, &dev_y); cairo_user_to_device (data, x, y); *x += dev_x; *y += dev_y; } typedef void (* PathForeachFunc) (double *x, double *y, gpointer data); static void path_foreach_point (cairo_path_t *path, PathForeachFunc func, gpointer user_data) { int i; for (i = 0; i < path->num_data; i += path->data[i].header.length) { cairo_path_data_t *data = &(path->data[i]); switch (data->header.type) { case CAIRO_PATH_MOVE_TO: case CAIRO_PATH_LINE_TO: func (&(data[1].point.x), &(data[1].point.y), user_data); break; case CAIRO_PATH_CURVE_TO: func (&(data[1].point.x), &(data[1].point.y), user_data); func (&(data[2].point.x), &(data[2].point.y), user_data); func (&(data[3].point.x), &(data[3].point.y), user_data); break; case CAIRO_PATH_CLOSE_PATH: break; } } } void backing_store_add_input (BackingStore *store, cairo_t *cr, gboolean is_stroke, BackingStoreEventFunc func, gpointer data) { InputPath path; path.is_stroke = is_stroke; path.fill_rule = cairo_get_fill_rule (cr); path.line_width = cairo_get_line_width (cr); path.path = cairo_copy_path (cr); path_foreach_point (path.path, user_to_device, cr); path.func = func; path.data = data; g_array_append_val (store->current_input->paths, path); } BackingStore * backing_store_new (GdkWindow *window, int width, int height) { BackingStore *store = g_new0 (BackingStore, 1); GdkRectangle rect = { 0, 0, width, height }; store->pixmap = gdk_pixmap_new (window, width, height, -1); store->update_region = gdk_region_rectangle (&rect); store->width = width; store->height = height; store->current_input = NULL; store->input_regions = g_ptr_array_new (); return store; } void backing_store_invalidate_rect (BackingStore *store, GdkRectangle *rect) { g_return_if_fail (store != NULL); g_return_if_fail (rect != NULL); gdk_region_union_with_rect (store->update_region, rect); } void backing_store_invalidate_region (BackingStore *store, GdkRegion *region) { g_return_if_fail (store != NULL); g_return_if_fail (region != NULL); gdk_region_union (store->update_region, region); } void backing_store_invalidate_all (BackingStore *store) { GdkRectangle rect; g_return_if_fail (store != NULL); gdk_region_destroy (store->update_region); rect.x = 0; rect.y = 0; rect.width = store->width; rect.height = store->height; store->update_region = gdk_region_rectangle (&rect); } void backing_store_resize (BackingStore *store, int width, int height) { GdkPixmap *pixmap; GdkRegion *old, *invalid; GdkRectangle r; GdkGC *gc; width = MAX (width, 1); height = MAX (height, 1); pixmap = gdk_pixmap_new (store->pixmap, width, height, -1); /* Unfortunately we don't know in which direction we were resized, * so we just assume we were dragged from the south-east corner. * * Although, maybe we could get the root coordinates of the input-window? * That might just work, actually. We need to make sure metacity uses * static gravity for the window before this will be useful. */ gc = gdk_gc_new (pixmap); gdk_draw_drawable (pixmap, gc, store->pixmap, 0, 0, 0, 0, -1, -1); g_object_unref (gc); g_object_unref (store->pixmap); store->pixmap = pixmap; r.x = r.y = 0; r.height = store->height; r.width = store->width; old = gdk_region_rectangle (&r); store->width = width; store->height = height; r.height = store->height; r.width = store->width; invalid = gdk_region_rectangle (&r); gdk_region_subtract (invalid, old); backing_store_invalidate_region (store, invalid); gdk_region_destroy (old); gdk_region_destroy (invalid); } void backing_store_draw (BackingStore *store, GdkDrawable *dest, GdkRegion *clip, int dest_x, int dest_y) { GdkGC *gc; if (store->in_process_updates) { g_critical ("backing_store_draw() cannot be called when " "backing_store_process_updates() is running\n"); return; } gc = gdk_gc_new (dest); gdk_gc_set_clip_region (gc, clip); gdk_draw_drawable (dest, gc, store->pixmap, 0, 0, dest_x, dest_y, store->width, store->height); g_object_unref (gc); } static GdkRegion * make_store_region (BackingStore *store) { GdkRectangle store_rect; store_rect.x = 0; store_rect.y = 0; store_rect.width = store->width; store_rect.height = store->height; return gdk_region_rectangle (&store_rect); } static void input_region_free (InputRegion *region) { int i; for (i = 0; i < region->paths->len; ++i) { InputPath *path = &g_array_index (region->paths, InputPath, i); cairo_path_destroy (path->path); } g_array_free (region->paths, TRUE); gdk_region_destroy (region->region); g_free (region); } static void clear_exposed_input_region (BackingStore *store, GdkRegion *region) { int i; GdkRegion *store_reg = make_store_region (store); gdk_region_subtract (store_reg, region); for (i = 0; i < store->input_regions->len; ++i) { InputRegion *region = store->input_regions->pdata[i]; gdk_region_intersect (region->region, store_reg); if (gdk_region_empty (region->region)) { input_region_free (region); g_ptr_array_remove_index_fast (store->input_regions, i--); } } gdk_region_destroy (store_reg); } static void clip_to_region (cairo_t *cr, GdkRegion *region) { int n_rects; GdkRectangle *rects; gdk_region_get_rectangles (region, &rects, &n_rects); cairo_new_path (cr); while (n_rects--) { GdkRectangle *rect = &(rects[n_rects]); cairo_rectangle (cr, rect->x, rect->y, rect->width, rect->height); } cairo_clip (cr); g_free (rects); } void backing_store_process_updates (BackingStore *store, BackingPaintFunc func, gpointer data) { store->in_process_updates = TRUE; /* FIXME: * * The expose process itself can generate invalidations, * so we need to continue to process updates until * the update region is empty * * Yes, this can lead to infinite loops. Something should be * done about that, but it's not completely clear what exactly. * * Consider the case where hovering an object causes the object to * move out of the way, which causes non-hovering, which causes * the object to move back - and so on. If the application is * actually in this situation, then an infnitely loop is * in some sense the logical result. * * A possibility is to only loop, say, five times, and just let * applications sort it out by itself. (If they don't do anything, * they will get flickering back and forth, which is probably the * correct default behavior). */ while (!gdk_region_empty (store->update_region)) { cairo_t *cr = gdk_cairo_create (store->pixmap); GdkRegion *region = store->update_region; GdkRegion *store_reg = make_store_region (store); gdk_region_intersect (region, store_reg); gdk_region_destroy (store_reg); store->update_region = gdk_region_new (); clip_to_region (cr, region); { /* To make sure all of the clip region is painted */ cairo_set_source_rgb (cr, g_random_double(), 0, 0); cairo_paint (cr); } clear_exposed_input_region (store, region); store->current_input = g_new0 (InputRegion, 1); store->current_input->region = region; store->current_input->paths = g_array_new ( FALSE, FALSE, sizeof (InputPath)); func (cr, region, data); g_ptr_array_add (store->input_regions, store->current_input); store->current_input = NULL; cairo_destroy (cr); if (store->need_fake_motion) { /* The backing store was scrolled, so generate a fake * motion event so that scrolling while dragging produces * the correct result. */ process_event (store, BACKING_STORE_MOTION, store->current_x, store->current_y, 0); store->need_fake_motion = FALSE; } else { /* Make sure any crossing events generated during the * expose process are emitted here */ process_event (store, BACKING_STORE_NONE, store->current_x, store->current_y, 0); } } store->in_process_updates = FALSE; }