From 34398b273c7cf4806b3d0986b9d655500e7bb819 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Fri, 6 Sep 2013 14:14:31 +0200 Subject: wayland: add support for pointer barriers Use the clutter pointer constrain callback and a lot of copypasted code from Xorg to implement reactive pointer barriers and pointer barrier events. https://bugzilla.gnome.org/show_bug.cgi?id=706655 --- src/Makefile.am | 1 + src/core/barrier-private.h | 39 +++ src/core/barrier.c | 528 +++++++++++++++++++++++++++++++++++-- src/core/display.c | 2 +- src/meta/barrier.h | 1 + src/wayland/meta-wayland-pointer.c | 33 ++- 6 files changed, 567 insertions(+), 37 deletions(-) create mode 100644 src/core/barrier-private.h diff --git a/src/Makefile.am b/src/Makefile.am index 29172020f..b63fa176d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -52,6 +52,7 @@ libmutter_wayland_la_SOURCES = \ core/async-getprop.h \ core/barrier.c \ meta/barrier.h \ + core/barrier-private.h \ core/bell.c \ core/bell.h \ core/boxes.c \ diff --git a/src/core/barrier-private.h b/src/core/barrier-private.h new file mode 100644 index 000000000..e8a2e7a99 --- /dev/null +++ b/src/core/barrier-private.h @@ -0,0 +1,39 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ + +/* + * Copyright 2012, 2013 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: Jaster St. Pierre + * Giovanni Campagna + */ + +#ifndef BARRIER_PRIVATE_H +#define BARRIER_PRIVATE_H + +typedef struct _MetaBarrierManager MetaBarrierManager; + +MetaBarrierManager *meta_barrier_manager_get (void); + +void meta_barrier_manager_constrain_cursor (MetaBarrierManager *manager, + guint32 time, + float current_x, + float current_y, + float *new_x, + float *new_y); + +#endif diff --git a/src/core/barrier.c b/src/core/barrier.c index 643b26ed7..ff1f4361b 100644 --- a/src/core/barrier.c +++ b/src/core/barrier.c @@ -1,5 +1,27 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* + * Copyright 2012, 2013 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: Jaster St. Pierre + * Giovanni Campagna + */ + /** * SECTION:barrier * @Title: MetaBarrier @@ -9,6 +31,7 @@ #include "config.h" #include +#include #include #include @@ -16,6 +39,7 @@ #include #include "display-private.h" #include "mutter-enum-types.h" +#include "barrier-private.h" #include "core.h" G_DEFINE_TYPE (MetaBarrier, meta_barrier, G_TYPE_OBJECT) @@ -56,9 +80,23 @@ struct _MetaBarrierPrivate MetaBarrierDirection directions; + /* x11 */ PointerBarrier xbarrier; + + /* wayland */ + gboolean active; + gboolean seen, hit; + + int barrier_event_id; + int release_event_id; + guint32 last_timestamp; }; +struct _MetaBarrierManager +{ + GList *barriers; +} *global_barrier_manager; + static void meta_barrier_event_unref (MetaBarrierEvent *event); static void @@ -148,7 +186,10 @@ meta_barrier_dispose (GObject *object) gboolean meta_barrier_is_active (MetaBarrier *barrier) { - return barrier->priv->xbarrier != 0; + if (meta_is_wayland_compositor ()) + return barrier->priv->active; + else + return barrier->priv->xbarrier != 0; } /** @@ -165,15 +206,25 @@ void meta_barrier_release (MetaBarrier *barrier, MetaBarrierEvent *event) { -#ifdef HAVE_XI23 - MetaBarrierPrivate *priv = barrier->priv; - if (META_DISPLAY_HAS_XINPUT_23 (priv->display)) + MetaBarrierPrivate *priv; + + priv = barrier->priv; + + if (meta_is_wayland_compositor ()) { - XIBarrierReleasePointer (priv->display->xdisplay, - META_VIRTUAL_CORE_POINTER_ID, - priv->xbarrier, event->event_id); + priv->release_event_id = event->event_id; } + else + { +#ifdef HAVE_XI23 + if (META_DISPLAY_HAS_XINPUT_23 (priv->display)) + { + XIBarrierReleasePointer (priv->display->xdisplay, + META_VIRTUAL_CORE_POINTER_ID, + priv->xbarrier, event->event_id); + } #endif /* HAVE_XI23 */ + } } static void @@ -192,19 +243,29 @@ meta_barrier_constructed (GObject *object) return; } - dpy = priv->display->xdisplay; - root = DefaultRootWindow (dpy); + if (meta_is_wayland_compositor ()) + { + MetaBarrierManager *manager = meta_barrier_manager_get (); - priv->xbarrier = XFixesCreatePointerBarrier (dpy, root, - priv->x1, priv->y1, - priv->x2, priv->y2, - priv->directions, 0, NULL); + manager->barriers = g_list_prepend (manager->barriers, g_object_ref (barrier)); + priv->active = TRUE; + } + else + { + dpy = priv->display->xdisplay; + root = DefaultRootWindow (dpy); - /* Take a ref that we'll release when the XID dies inside destroy(), - * so that the object stays alive and doesn't get GC'd. */ - g_object_ref (barrier); + priv->xbarrier = XFixesCreatePointerBarrier (dpy, root, + priv->x1, priv->y1, + priv->x2, priv->y2, + priv->directions, 0, NULL); - g_hash_table_insert (priv->display->xids, &priv->xbarrier, barrier); + /* Take a ref that we'll release when the XID dies inside destroy(), + * so that the object stays alive and doesn't get GC'd. */ + g_object_ref (barrier); + + g_hash_table_insert (priv->display->xids, &priv->xbarrier, barrier); + } G_OBJECT_CLASS (meta_barrier_parent_class)->constructed (object); } @@ -312,16 +373,26 @@ meta_barrier_destroy (MetaBarrier *barrier) if (priv->display == NULL) return; - dpy = priv->display->xdisplay; + if (meta_is_wayland_compositor ()) + { + MetaBarrierManager *manager = meta_barrier_manager_get (); - if (!meta_barrier_is_active (barrier)) - return; + manager->barriers = g_list_remove (manager->barriers, barrier); + g_object_unref (barrier); + } + else + { + dpy = priv->display->xdisplay; + + if (!meta_barrier_is_active (barrier)) + return; - XFixesDestroyPointerBarrier (dpy, priv->xbarrier); - g_hash_table_remove (priv->display->xids, &priv->xbarrier); - priv->xbarrier = 0; + XFixesDestroyPointerBarrier (dpy, priv->xbarrier); + g_hash_table_remove (priv->display->xids, &priv->xbarrier); + priv->xbarrier = 0; - g_object_unref (barrier); + g_object_unref (barrier); + } } static void @@ -371,6 +442,9 @@ meta_display_process_barrier_event (MetaDisplay *display, { MetaBarrier *barrier; + if (meta_is_wayland_compositor ()) + return FALSE; + barrier = g_hash_table_lookup (display->xids, &xev->barrier); if (barrier != NULL) { @@ -382,6 +456,405 @@ meta_display_process_barrier_event (MetaDisplay *display, } #endif /* HAVE_XI23 */ +/* + * The following code was copied and adapted from the X server (Xi/xibarriers.c) + * + * Copyright 2012 Red Hat, Inc. + * Copyright © 2002 Keith Packard + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +static gboolean +barrier_is_horizontal(MetaBarrier *barrier) +{ + return barrier->priv->y1 == barrier->priv->y2; +} + +static gboolean +barrier_is_vertical(MetaBarrier *barrier) +{ + return barrier->priv->x1 == barrier->priv->x2; +} + +/* + * @return The set of barrier movement directions the movement vector + * x1/y1 → x2/y2 represents. + */ +static int +barrier_get_direction(int x1, int y1, int x2, int y2) +{ + int direction = 0; + + /* which way are we trying to go */ + if (x2 > x1) + direction |= META_BARRIER_DIRECTION_POSITIVE_X; + if (x2 < x1) + direction |= META_BARRIER_DIRECTION_NEGATIVE_X; + if (y2 > y1) + direction |= META_BARRIER_DIRECTION_POSITIVE_Y; + if (y2 < y1) + direction |= META_BARRIER_DIRECTION_NEGATIVE_Y; + + return direction; +} + +/* + * Test if the barrier may block movement in the direction defined by + * x1/y1 → x2/y2. This function only tests whether the directions could be + * blocked, it does not test if the barrier actually blocks the movement. + * + * @return TRUE if the barrier blocks the direction of movement or FALSE + * otherwise. + */ +static gboolean +barrier_is_blocking_direction(MetaBarrier *barrier, + MetaBarrierDirection direction) +{ + /* Barriers define which way is ok, not which way is blocking */ + return (barrier->priv->directions & direction) != direction; +} + +static gboolean +inside_segment(int v, int v1, int v2) +{ + if (v1 < 0 && v2 < 0) /* line */ + return TRUE; + else if (v1 < 0) /* ray */ + return v <= v2; + else if (v2 < 0) /* ray */ + return v >= v1; + else /* line segment */ + return v >= v1 && v <= v2; +} + +#define T(v, a, b) (((float)v) - (a)) / ((b) - (a)) +#define F(t, a, b) ((t) * ((a) - (b)) + (a)) + +/* + * Test if the movement vector x1/y1 → x2/y2 is intersecting with the + * barrier. A movement vector with the startpoint or endpoint adjacent to + * the barrier itself counts as intersecting. + * + * @param x1 X start coordinate of movement vector + * @param y1 Y start coordinate of movement vector + * @param x2 X end coordinate of movement vector + * @param y2 Y end coordinate of movement vector + * @param[out] distance The distance between the start point and the + * intersection with the barrier (if applicable). + * @return TRUE if the barrier intersects with the given vector + */ +static gboolean +barrier_is_blocking(MetaBarrier *barrier, + int x1, int y1, int x2, int y2, double *distance) +{ + if (barrier_is_vertical (barrier)) + { + float t, y; + t = T (barrier->priv->x1, x1, x2); + if (t < 0 || t > 1) + return FALSE; + + /* Edge case: moving away from barrier. */ + if (x2 > x1 && t == 0) + return FALSE; + + y = F (t, y1, y2); + if (!inside_segment (y, barrier->priv->y1, barrier->priv->y2)) + return FALSE; + + *distance = sqrt ((y - y1) * (y - y1) + (barrier->priv->x1 - x1) * (barrier->priv->x1 - x1)); + return TRUE; + } + else + { + float t, x; + t = T (barrier->priv->y1, y1, y2); + if (t < 0 || t > 1) + return FALSE; + + /* Edge case: moving away from barrier. */ + if (y2 > y1 && t == 0) + return FALSE; + + x = F(t, x1, x2); + if (!inside_segment (x, barrier->priv->x1, barrier->priv->x2)) + return FALSE; + + *distance = sqrt ((x - x1) * (x - x1) + (barrier->priv->y1 - y1) * (barrier->priv->y1 - y1)); + return TRUE; + } +} + +#define HIT_EDGE_EXTENTS 2 +static gboolean +barrier_inside_hit_box(MetaBarrier *barrier, int x, int y) +{ + int x1, x2, y1, y2; + int dir; + + x1 = barrier->priv->x1; + x2 = barrier->priv->x2; + y1 = barrier->priv->y1; + y2 = barrier->priv->y2; + dir = ~(barrier->priv->directions); + + if (barrier_is_vertical (barrier)) + { + if (dir & META_BARRIER_DIRECTION_POSITIVE_X) + x1 -= HIT_EDGE_EXTENTS; + if (dir & META_BARRIER_DIRECTION_NEGATIVE_X) + x2 += HIT_EDGE_EXTENTS; + } + if (barrier_is_horizontal (barrier)) + { + if (dir & META_BARRIER_DIRECTION_POSITIVE_Y) + y1 -= HIT_EDGE_EXTENTS; + if (dir & META_BARRIER_DIRECTION_NEGATIVE_Y) + y2 += HIT_EDGE_EXTENTS; + } + + return x >= x1 && x <= x2 && y >= y1 && y <= y2; +} + +/* + * Find the nearest barrier client that is blocking movement from x1/y1 to x2/y2. + * + * @param dir Only barriers blocking movement in direction dir are checked + * @param x1 X start coordinate of movement vector + * @param y1 Y start coordinate of movement vector + * @param x2 X end coordinate of movement vector + * @param y2 Y end coordinate of movement vector + * @return The barrier nearest to the movement origin that blocks this movement. + */ +static MetaBarrier * +barrier_find_nearest(MetaBarrierManager *manager, + int dir, + int x1, + int y1, + int x2, + int y2) +{ + GList *iter; + MetaBarrier *nearest = NULL; + double min_distance = INT_MAX; /* can't get higher than that in X anyway */ + + for (iter = manager->barriers; iter; iter = iter->next) + { + MetaBarrier *b = iter->data; + double distance; + + if (b->priv->seen || !b->priv->active) + continue; + + if (!barrier_is_blocking_direction (b, dir)) + continue; + + if (barrier_is_blocking (b, x1, y1, x2, y2, &distance)) + { + if (min_distance > distance) + { + min_distance = distance; + nearest = b; + } + } + } + + return nearest; +} + +/* + * Clamp to the given barrier given the movement direction specified in dir. + * + * @param barrier The barrier to clamp to + * @param dir The movement direction + * @param[out] x The clamped x coordinate. + * @param[out] y The clamped x coordinate. + */ +static void +barrier_clamp_to_barrier(MetaBarrier *barrier, + int dir, + float *x, + float *y) +{ + if (barrier_is_vertical (barrier)) + { + if ((dir & META_BARRIER_DIRECTION_NEGATIVE_X) & ~barrier->priv->directions) + *x = barrier->priv->x1; + if ((dir & META_BARRIER_DIRECTION_POSITIVE_X) & ~barrier->priv->directions) + *x = barrier->priv->x1 - 1; + } + if (barrier_is_horizontal (barrier)) + { + if ((dir & META_BARRIER_DIRECTION_NEGATIVE_Y) & ~barrier->priv->directions) + *y = barrier->priv->y1; + if ((dir & META_BARRIER_DIRECTION_POSITIVE_Y) & ~barrier->priv->directions) + *y = barrier->priv->y1 - 1; + } +} + +static gboolean +emit_hit_event (gpointer data) +{ + MetaBarrierEvent *event = data; + + g_signal_emit (event->barrier, obj_signals[HIT], 0, event); + + meta_barrier_event_unref (event); + return FALSE; +} + +static gboolean +emit_left_event (gpointer data) +{ + MetaBarrierEvent *event = data; + + g_signal_emit (event->barrier, obj_signals[LEFT], 0, event); + + meta_barrier_event_unref (event); + return FALSE; +} + +void +meta_barrier_manager_constrain_cursor (MetaBarrierManager *manager, + guint32 time, + float current_x, + float current_y, + float *new_x, + float *new_y) +{ + float x = *new_x; + float y = *new_y; + int dir; + MetaBarrier *nearest = NULL; + GList *iter; + float dx = x - current_x; + float dy = y - current_y; + + /* How this works: + * Given the origin and the movement vector, get the nearest barrier + * to the origin that is blocking the movement. + * Clamp to that barrier. + * Then, check from the clamped intersection to the original + * destination, again finding the nearest barrier and clamping. + */ + dir = barrier_get_direction (current_x, current_y, x, y); + + while (dir != 0) + { + MetaBarrierEvent *event; + gboolean new_sequence; + + nearest = barrier_find_nearest (manager, dir, current_x, current_y, x, y); + if (!nearest) + break; + + new_sequence = !nearest->priv->hit; + + nearest->priv->seen = TRUE; + nearest->priv->hit = TRUE; + + if (nearest->priv->barrier_event_id == nearest->priv->release_event_id) + continue; + + barrier_clamp_to_barrier (nearest, dir, &x, &y); + + if (barrier_is_vertical (nearest)) + { + dir &= ~(META_BARRIER_DIRECTION_NEGATIVE_X | META_BARRIER_DIRECTION_POSITIVE_X); + current_x = x; + } + else if (barrier_is_horizontal (nearest)) + { + dir &= ~(META_BARRIER_DIRECTION_NEGATIVE_Y | META_BARRIER_DIRECTION_POSITIVE_Y); + current_y = y; + } + + event = g_slice_new0 (MetaBarrierEvent); + + event->ref_count = 1; + event->barrier = g_object_ref (nearest); + event->event_id = nearest->priv->barrier_event_id; + event->time = time; + event->dt = new_sequence ? 0 : time - nearest->priv->last_timestamp; + event->x = current_x; + event->y = current_y; + event->dx = dx; + event->dy = dy; + event->released = FALSE; + event->grabbed = FALSE; + + g_idle_add (emit_hit_event, event); + } + + for (iter = manager->barriers; iter; iter = iter->next) + { + MetaBarrierEvent *event; + MetaBarrier *barrier = iter->data; + + if (!barrier->priv->active) + continue; + + barrier->priv->seen = FALSE; + if (!barrier->priv->hit) + continue; + + if (barrier_inside_hit_box (barrier, x, y)) + continue; + + barrier->priv->hit = FALSE; + + event = g_slice_new0 (MetaBarrierEvent); + + event->ref_count = 1; + event->barrier = g_object_ref (barrier); + event->event_id = barrier->priv->barrier_event_id; + event->time = time; + event->dt = time - barrier->priv->last_timestamp; + event->x = current_x; + event->y = current_y; + event->dx = dx; + event->dy = dy; + event->released = barrier->priv->barrier_event_id == barrier->priv->release_event_id; + event->grabbed = FALSE; + + g_idle_add (emit_left_event, event); + + /* If we've left the hit box, this is the + * start of a new event ID. */ + barrier->priv->barrier_event_id++; + } + + *new_x = x; + *new_y = y; +} + +MetaBarrierManager * +meta_barrier_manager_get (void) +{ + if (!global_barrier_manager) + global_barrier_manager = g_new0 (MetaBarrierManager, 1); + + return global_barrier_manager; +} + static MetaBarrierEvent * meta_barrier_event_ref (MetaBarrierEvent *event) { @@ -399,7 +872,12 @@ meta_barrier_event_unref (MetaBarrierEvent *event) g_return_if_fail (event->ref_count > 0); if (g_atomic_int_dec_and_test ((volatile int *)&event->ref_count)) - g_slice_free (MetaBarrierEvent, event); + { + if (event->barrier) + g_object_unref (event->barrier); + + g_slice_free (MetaBarrierEvent, event); + } } G_DEFINE_BOXED_TYPE (MetaBarrierEvent, diff --git a/src/core/display.c b/src/core/display.c index 48087a90c..11cdcd57d 100644 --- a/src/core/display.c +++ b/src/core/display.c @@ -6078,7 +6078,7 @@ meta_display_get_xinput_opcode (MetaDisplay *display) gboolean meta_display_supports_extended_barriers (MetaDisplay *display) { - return META_DISPLAY_HAS_XINPUT_23 (display) && !meta_is_wayland_compositor (); + return meta_is_wayland_compositor () || META_DISPLAY_HAS_XINPUT_23 (display); } /** diff --git a/src/meta/barrier.h b/src/meta/barrier.h index d7b1666c7..7fb545726 100644 --- a/src/meta/barrier.h +++ b/src/meta/barrier.h @@ -94,6 +94,7 @@ typedef enum { struct _MetaBarrierEvent { /* < private > */ volatile guint ref_count; + MetaBarrier *barrier; /* < public > */ int event_id; diff --git a/src/wayland/meta-wayland-pointer.c b/src/wayland/meta-wayland-pointer.c index 50620a88f..55c0638b7 100644 --- a/src/wayland/meta-wayland-pointer.c +++ b/src/wayland/meta-wayland-pointer.c @@ -49,6 +49,7 @@ #include "meta-wayland-pointer.h" #include "meta-wayland-private.h" +#include "barrier-private.h" #include @@ -165,10 +166,10 @@ static const MetaWaylandPointerGrabInterface default_pointer_grab_interface = { */ static gboolean -check_all_screen_monitors(MetaMonitorInfo *monitors, - unsigned n_monitors, - float x, - float y) +check_all_screen_monitors (MetaMonitorInfo *monitors, + unsigned n_monitors, + float x, + float y) { unsigned int i; @@ -193,14 +194,13 @@ static void constrain_all_screen_monitors (ClutterInputDevice *device, MetaMonitorInfo *monitors, unsigned n_monitors, + float current_x, + float current_y, float *x, float *y) { - ClutterPoint current; unsigned int i; - clutter_input_device_get_coords (device, NULL, ¤t); - /* if we're trying to escape, clamp to the CRTC we're coming from */ for (i = 0; i < n_monitors; i++) { @@ -213,8 +213,8 @@ constrain_all_screen_monitors (ClutterInputDevice *device, top = monitor->rect.y; bottom = left + monitor->rect.height; - nx = current.x; - ny = current.y; + nx = current_x; + ny = current_y; if ((nx >= left) && (nx < right) && (ny >= top) && (ny < bottom)) { @@ -239,21 +239,32 @@ pointer_constrain_callback (ClutterInputDevice *device, float *new_y, gpointer user_data) { + MetaBarrierManager *barrier_manager; MetaMonitorManager *monitor_manager; MetaMonitorInfo *monitors; unsigned int n_monitors; gboolean ret; + ClutterPoint current; + clutter_input_device_get_coords (device, NULL, ¤t); + + barrier_manager = meta_barrier_manager_get (); monitor_manager = meta_monitor_manager_get (); monitors = meta_monitor_manager_get_monitor_infos (monitor_manager, &n_monitors); + meta_barrier_manager_constrain_cursor (barrier_manager, time, + current.x, current.y, + new_x, new_y); + /* if we're moving inside a monitor, we're fine */ ret = check_all_screen_monitors(monitors, n_monitors, *new_x, *new_y); - if (ret == TRUE) + if (ret) return; /* if we're trying to escape, clamp to the CRTC we're coming from */ - constrain_all_screen_monitors(device, monitors, n_monitors, new_x, new_y); + constrain_all_screen_monitors(device, monitors, n_monitors, + current.x, current.y, + new_x, new_y); } void -- cgit v1.2.1