mirror of
https://github.com/brl/mutter.git
synced 2024-11-22 08:00:42 -05:00
a7c4e8cefa
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>
437 lines
12 KiB
C
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;
|
|
}
|