mutter/clutter/clutter/clutter-pick-stack.c
Ivan Molodetskikh edbc9a2086 clutter/pick-stack: Add an allocation check to calculate_clear_area ()
We might pick an actor that needs relayout. I've seen this happen inside
hiding / unmapping in particular. In this case, calculate_clear_area ()
will call clutter_actor_get_abs_allocation_vertices () which in turn
will force a relayout. However, this is not what we want, because:

1. We don't want to run layout during picking.
2. If the actor needs an allocation, then the pick stack could not have
   used an up-to-date allocation, because it is not computed. Therefore
   this clear area would use a potentially completely different
   allocation than the one stored in the pick stack.

Thankfully, clear area seems to be used as a cache/optimization, so
let's just avoid computing it if the actor is not allocated.

Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/3425>
2023-12-01 15:36:32 +00:00

545 lines
15 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/clutter-pick-stack-private.h"
#include "clutter/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;
gboolean is_overlap;
} 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_object (&pick_stack->matrix_stack);
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.is_overlap = FALSE;
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_log_overlap (ClutterPickStack *pick_stack,
ClutterActor *actor)
{
PickRecord rec = { 0 };
g_assert (!pick_stack->sealed);
rec.is_overlap = TRUE;
rec.actor = actor;
rec.clip_index = pick_stack->current_clip_stack_top;
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);
}
static gboolean
get_verts_rectangle (graphene_point3d_t verts[4],
MtkRectangle *rect)
{
if (verts[0].x != verts[2].x ||
verts[0].y != verts[1].y ||
verts[3].x != verts[1].x ||
verts[3].y != verts[2].y ||
verts[0].x > verts[3].x ||
verts[0].y > verts[3].y)
return FALSE;
*rect = (MtkRectangle) {
.x = ceilf (verts[0].x),
.y = ceilf (verts[0].y),
.width = floor (verts[1].x - ceilf (verts[0].x)),
.height = floor (verts[2].y - ceilf (verts[0].y)),
};
return TRUE;
}
static void
calculate_clear_area (ClutterPickStack *pick_stack,
PickRecord *pick_rec,
int elem,
MtkRegion **clear_area)
{
MtkRegion *area = NULL;
graphene_point3d_t verts[4];
MtkRectangle rect;
int i;
if (!clutter_actor_has_allocation (pick_rec->actor))
{
if (clear_area)
*clear_area = NULL;
return;
}
clutter_actor_get_abs_allocation_vertices (pick_rec->actor,
(graphene_point3d_t *) &verts);
if (!get_verts_rectangle (verts, &rect))
{
if (clear_area)
*clear_area = NULL;
return;
}
rect.x += ceil (pick_rec->base.rect.x1);
rect.y += ceil (pick_rec->base.rect.y1);
rect.width =
MIN (rect.width, floor (pick_rec->base.rect.x2 - pick_rec->base.rect.x1));
rect.height =
MIN (rect.height, floor (pick_rec->base.rect.y2 - pick_rec->base.rect.y1));
area = mtk_region_create_rectangle (&rect);
for (i = elem + 1; i < pick_stack->vertices_stack->len; i++)
{
PickRecord *rec =
&g_array_index (pick_stack->vertices_stack, PickRecord, i);
ClutterActorBox paint_box;
if (!rec->is_overlap &&
(rec->base.rect.x1 == rec->base.rect.x2 ||
rec->base.rect.y1 == rec->base.rect.y2))
continue;
if (!clutter_actor_get_paint_box (rec->actor, &paint_box))
continue;
mtk_region_subtract_rectangle (area,
&MTK_RECTANGLE_INIT (paint_box.x1, paint_box.y1,
paint_box.x2 - paint_box.x1,
paint_box.y2 - paint_box.y1)
);
}
if (clear_area)
*clear_area = g_steal_pointer (&area);
g_clear_pointer (&area, mtk_region_unref);
}
ClutterActor *
clutter_pick_stack_search_actor (ClutterPickStack *pick_stack,
const graphene_point3d_t *point,
const graphene_ray_t *ray,
MtkRegion **clear_area)
{
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->is_overlap && rec->actor &&
ray_intersects_record (pick_stack, rec, point, ray))
{
if (clear_area)
calculate_clear_area (pick_stack, rec, i, clear_area);
return rec->actor;
}
}
return NULL;
}