mutter/clutter/clutter/clutter-pick-stack.c
Sebastian Keller a7c4e8cefa clutter/pick-stack: Use exclusive bottom/right box borders when picking
The graphene functions used by clutter for picking assume that boxes are
inclusive in both there start and end coordinates, so picking at y
coordinate 32 for an actor with the height 32 placed at y coordinate 0
would still be considered a hit. This however is wrong as 32 is the
first position that is not in the actor anymore.

Usually this would not be much of a problem, because motion events are
rarely ever at exactly these borders and even if they are there will be
another motion event soon after. But since actors in gnome-shell usually
are aligned with the pixel grid and on X11 enter/leave events are
generated by the X server at integer coordinates, this case is much
more likely for those.

This can cause issues with Firefox which when using client side
decorations, still requests MWM_DECOR_BORDER via _MOTIF_WM_HINTS to have
mutter draw a border + shadow. This means that the Firefox window even
when using CSD is still reparented. For such windows we receive among
others XI_RawMotion and XI_Enter events, but no XI_Motion events. And
the raw motion events are discarded after an enter event, because that
sets has_pointer_focus to TRUE in MetaSeatX11. So when moving the cursor
from the panel to a maximized Firefox window the last event clutter
receives is the enter event at exactly integer coordinates. Since the
panel is 32px tall and the generated enter event is at y position 32,
the picking code will pick a panel actor and the focus will remain on it
as long as the cursor does not leave the Firefox window.

Fix this by excluding the bottom and right border of a box when picking.

Fixes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4041

Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1842>
2021-05-04 14:47:43 +00:00

437 lines
12 KiB
C

/*
* Copyright (C) 2020 Endless OS Foundation, LLC
* Copyright (C) 2018 Canonical Ltd.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "clutter-pick-stack-private.h"
#include "clutter-private.h"
typedef struct
{
graphene_point3d_t vertices[4];
CoglMatrixEntry *matrix_entry;
ClutterActorBox rect;
gboolean projected;
} Record;
typedef struct
{
Record base;
ClutterActor *actor;
int clip_index;
} PickRecord;
typedef struct
{
Record base;
int prev;
} PickClipRecord;
struct _ClutterPickStack
{
grefcount ref_count;
CoglMatrixStack *matrix_stack;
GArray *vertices_stack;
GArray *clip_stack;
int current_clip_stack_top;
gboolean sealed : 1;
};
G_DEFINE_BOXED_TYPE (ClutterPickStack, clutter_pick_stack,
clutter_pick_stack_ref, clutter_pick_stack_unref)
static void
project_vertices (CoglMatrixEntry *matrix_entry,
const ClutterActorBox *box,
graphene_point3d_t vertices[4])
{
graphene_matrix_t m;
int i;
cogl_matrix_entry_get (matrix_entry, &m);
graphene_point3d_init (&vertices[0], box->x1, box->y1, 0.f);
graphene_point3d_init (&vertices[1], box->x2, box->y1, 0.f);
graphene_point3d_init (&vertices[2], box->x2, box->y2, 0.f);
graphene_point3d_init (&vertices[3], box->x1, box->y2, 0.f);
for (i = 0; i < 4; i++)
{
float w = 1.f;
cogl_graphene_matrix_project_point (&m,
&vertices[i].x,
&vertices[i].y,
&vertices[i].z,
&w);
}
}
static void
maybe_project_record (Record *rec)
{
if (!rec->projected)
{
project_vertices (rec->matrix_entry, &rec->rect, rec->vertices);
rec->projected = TRUE;
}
}
static inline gboolean
is_axis_aligned_2d_rectangle (const graphene_point3d_t vertices[4])
{
int i;
for (i = 0; i < 4; i++)
{
if (!G_APPROX_VALUE (vertices[i].z,
vertices[(i + 1) % 4].z,
FLT_EPSILON))
return FALSE;
if (!G_APPROX_VALUE (vertices[i].x,
vertices[(i + 1) % 4].x,
FLT_EPSILON) &&
!G_APPROX_VALUE (vertices[i].y,
vertices[(i + 1) % 4].y,
FLT_EPSILON))
return FALSE;
}
return TRUE;
}
static gboolean
ray_intersects_input_region (Record *rec,
const graphene_ray_t *ray,
const graphene_point3d_t *point)
{
maybe_project_record (rec);
if (G_LIKELY (is_axis_aligned_2d_rectangle (rec->vertices)))
{
graphene_box_t box;
graphene_box_t right_border;
graphene_box_t bottom_border;
/* Graphene considers both the start and end coordinates of boxes to be
* inclusive, while the vertices of a clutter actor are exclusive. So we
* need to manually exclude hits on these borders
*/
graphene_box_init_from_points (&box, 4, rec->vertices);
graphene_box_init_from_points (&right_border, 2, rec->vertices + 1);
graphene_box_init_from_points (&bottom_border, 2, rec->vertices + 2);
/* Fast path for actors without 3D transforms */
if (graphene_box_contains_point (&box, point))
{
return !graphene_box_contains_point (&right_border, point) &&
!graphene_box_contains_point (&bottom_border, point);
}
return graphene_ray_intersects_box (ray, &box) &&
!graphene_ray_intersects_box (ray, &right_border) &&
!graphene_ray_intersects_box (ray, &bottom_border);
}
else
{
graphene_triangle_t t0, t1;
/*
* Degrade the projected quad into the following triangles:
*
* 0 -------------- 1
* | • |
* | • t0 |
* | • |
* | t1 • |
* | • |
* 3 -------------- 2
*/
graphene_triangle_init_from_point3d (&t0,
&rec->vertices[0],
&rec->vertices[1],
&rec->vertices[2]);
graphene_triangle_init_from_point3d (&t1,
&rec->vertices[0],
&rec->vertices[2],
&rec->vertices[3]);
return graphene_triangle_contains_point (&t0, point) ||
graphene_triangle_contains_point (&t1, point) ||
graphene_ray_intersects_triangle (ray, &t0) ||
graphene_ray_intersects_triangle (ray, &t1);
}
}
static gboolean
ray_intersects_record (ClutterPickStack *pick_stack,
PickRecord *rec,
const graphene_point3d_t *point,
const graphene_ray_t *ray)
{
int clip_index;
if (!ray_intersects_input_region (&rec->base, ray, point))
return FALSE;
clip_index = rec->clip_index;
while (clip_index >= 0)
{
PickClipRecord *clip =
&g_array_index (pick_stack->clip_stack, PickClipRecord, clip_index);
if (!ray_intersects_input_region (&clip->base, ray, point))
return FALSE;
clip_index = clip->prev;
}
return TRUE;
}
static void
add_pick_stack_weak_refs (ClutterPickStack *pick_stack)
{
int i;
g_assert (!pick_stack->sealed);
for (i = 0; i < pick_stack->vertices_stack->len; i++)
{
PickRecord *rec =
&g_array_index (pick_stack->vertices_stack, PickRecord, i);
if (rec->actor)
g_object_add_weak_pointer (G_OBJECT (rec->actor),
(gpointer) &rec->actor);
}
}
static void
remove_pick_stack_weak_refs (ClutterPickStack *pick_stack)
{
int i;
for (i = 0; i < pick_stack->vertices_stack->len; i++)
{
PickRecord *rec =
&g_array_index (pick_stack->vertices_stack, PickRecord, i);
if (rec->actor)
g_object_remove_weak_pointer (G_OBJECT (rec->actor),
(gpointer) &rec->actor);
}
}
static void
clutter_pick_stack_dispose (ClutterPickStack *pick_stack)
{
remove_pick_stack_weak_refs (pick_stack);
g_clear_pointer (&pick_stack->matrix_stack, cogl_object_unref);
g_clear_pointer (&pick_stack->vertices_stack, g_array_unref);
g_clear_pointer (&pick_stack->clip_stack, g_array_unref);
}
static void
clear_pick_record (gpointer data)
{
PickRecord *rec = data;
g_clear_pointer (&rec->base.matrix_entry, cogl_matrix_entry_unref);
}
static void
clear_clip_record (gpointer data)
{
PickClipRecord *clip = data;
g_clear_pointer (&clip->base.matrix_entry, cogl_matrix_entry_unref);
}
/**
* clutter_pick_stack_new:
* @context: a #CoglContext
*
* Creates a new #ClutterPickStack.
*
* Returns: (transfer full): A newly created #ClutterPickStack
*/
ClutterPickStack *
clutter_pick_stack_new (CoglContext *context)
{
ClutterPickStack *pick_stack;
pick_stack = g_new0 (ClutterPickStack, 1);
g_ref_count_init (&pick_stack->ref_count);
pick_stack->matrix_stack = cogl_matrix_stack_new (context);
pick_stack->vertices_stack = g_array_new (FALSE, FALSE, sizeof (PickRecord));
pick_stack->clip_stack = g_array_new (FALSE, FALSE, sizeof (PickClipRecord));
pick_stack->current_clip_stack_top = -1;
g_array_set_clear_func (pick_stack->vertices_stack, clear_pick_record);
g_array_set_clear_func (pick_stack->clip_stack, clear_clip_record);
return pick_stack;
}
/**
* clutter_pick_stack_ref:
* @pick_stack: A #ClutterPickStack
*
* Increments the reference count of @pick_stack by one.
*
* Returns: (transfer full): @pick_stack
*/
ClutterPickStack *
clutter_pick_stack_ref (ClutterPickStack *pick_stack)
{
g_ref_count_inc (&pick_stack->ref_count);
return pick_stack;
}
/**
* clutter_pick_stack_unref:
* @pick_stack: A #ClutterPickStack
*
* Decrements the reference count of @pick_stack by one, freeing the structure
* when the reference count reaches zero.
*/
void
clutter_pick_stack_unref (ClutterPickStack *pick_stack)
{
if (g_ref_count_dec (&pick_stack->ref_count))
{
clutter_pick_stack_dispose (pick_stack);
g_free (pick_stack);
}
}
void
clutter_pick_stack_seal (ClutterPickStack *pick_stack)
{
g_assert (!pick_stack->sealed);
add_pick_stack_weak_refs (pick_stack);
pick_stack->sealed = TRUE;
}
void
clutter_pick_stack_log_pick (ClutterPickStack *pick_stack,
const ClutterActorBox *box,
ClutterActor *actor)
{
PickRecord rec;
g_return_if_fail (actor != NULL);
g_assert (!pick_stack->sealed);
rec.actor = actor;
rec.clip_index = pick_stack->current_clip_stack_top;
rec.base.rect = *box;
rec.base.projected = FALSE;
rec.base.matrix_entry = cogl_matrix_stack_get_entry (pick_stack->matrix_stack);
cogl_matrix_entry_ref (rec.base.matrix_entry);
g_array_append_val (pick_stack->vertices_stack, rec);
}
void
clutter_pick_stack_push_clip (ClutterPickStack *pick_stack,
const ClutterActorBox *box)
{
PickClipRecord clip;
g_assert (!pick_stack->sealed);
clip.prev = pick_stack->current_clip_stack_top;
clip.base.rect = *box;
clip.base.projected = FALSE;
clip.base.matrix_entry = cogl_matrix_stack_get_entry (pick_stack->matrix_stack);
cogl_matrix_entry_ref (clip.base.matrix_entry);
g_array_append_val (pick_stack->clip_stack, clip);
pick_stack->current_clip_stack_top = pick_stack->clip_stack->len - 1;
}
void
clutter_pick_stack_pop_clip (ClutterPickStack *pick_stack)
{
const PickClipRecord *top;
g_assert (!pick_stack->sealed);
g_assert (pick_stack->current_clip_stack_top >= 0);
/* Individual elements of clip_stack are not freed. This is so they can
* be shared as part of a tree of different stacks used by different
* actors in the pick_stack. The whole clip_stack does however get
* freed later in clutter_pick_stack_dispose.
*/
top = &g_array_index (pick_stack->clip_stack,
PickClipRecord,
pick_stack->current_clip_stack_top);
pick_stack->current_clip_stack_top = top->prev;
}
void
clutter_pick_stack_push_transform (ClutterPickStack *pick_stack,
const graphene_matrix_t *transform)
{
cogl_matrix_stack_push (pick_stack->matrix_stack);
cogl_matrix_stack_multiply (pick_stack->matrix_stack, transform);
}
void
clutter_pick_stack_get_transform (ClutterPickStack *pick_stack,
graphene_matrix_t *out_transform)
{
cogl_matrix_stack_get (pick_stack->matrix_stack, out_transform);
}
void
clutter_pick_stack_pop_transform (ClutterPickStack *pick_stack)
{
cogl_matrix_stack_pop (pick_stack->matrix_stack);
}
ClutterActor *
clutter_pick_stack_search_actor (ClutterPickStack *pick_stack,
const graphene_point3d_t *point,
const graphene_ray_t *ray)
{
int i;
/* Search all "painted" pickable actors from front to back. A linear search
* is required, and also performs fine since there is typically only
* on the order of dozens of actors in the list (on screen) at a time.
*/
for (i = pick_stack->vertices_stack->len - 1; i >= 0; i--)
{
PickRecord *rec =
&g_array_index (pick_stack->vertices_stack, PickRecord, i);
if (rec->actor && ray_intersects_record (pick_stack, rec, point, ray))
return rec->actor;
}
return NULL;
}