gnome-shell/src/st/st-box-layout.c
Georges Basile Stavracas Neto 2717ca9d08
st-box-layout: Pass correct allocation box to layout manager
StBoxLayout implements StScrollable, which, semantically, means that
the StBoxLayout size may not match the minimum size reported by the
layout manager. In this specific case, the layout manager by is a
ClutterBoxLayout by default. For example:

        +--------------+
        |   Viewport   |
 +------+--------------+-----------------+
 |      |              |                 |
 |      |              |     Content     |
 |      |              |                 |
 +------+--------------+-----------------+
        |              |
        +--------------+

So, assuming that:

 - ContentSize = the minimum size of the content;
 - ViewportSize = the allocated size of the viewport;

When allocating StBoxLayout, it must assume ViewportSize, but must
pass ContentSize to the layout manager. That way, the children of
StBoxLayout are correctly placed within it, even if it's bigger than
ViewportSize.

And here's the problem: right now, StBoxLayout assumes ViewportSize
AND also passes it to layout manager. Commit 77c4c6b6d specifically
exposed this bug by relying entirely on StBoxLayout to arrange the
app and window icons.

Fix that by using ViewportSize to allocate StBoxLayout itself, but
passing ContentSize to the layout manager.

https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/153
2018-10-08 22:42:24 -03:00

780 lines
24 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* st-box-layout.h: box layout actor
*
* Copyright 2009 Intel Corporation.
* Copyright 2009 Abderrahim Kitouni
* Copyright 2009, 2010 Red Hat, Inc.
* Copyright 2010 Florian Muellner
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU Lesser General Public License,
* version 2.1, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
/* Portions copied from Clutter:
* Clutter.
*
* An OpenGL based 'interactive canvas' library.
*
* Authored By Matthew Allum <mallum@openedhand.com>
*
* Copyright (C) 2006 OpenedHand
* 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.
*/
/**
* SECTION:st-box-layout
* @short_description: a layout container arranging children in a single line
*
* The #StBoxLayout arranges its children along a single line, where each
* child can be allocated either its preferred size or larger if the expand
* option is set. If the fill option is set, the actor will be allocated more
* than its requested size. If the fill option is not set, but the expand option
* is enabled, then the position of the actor within the available space can
* be determined by the alignment child property.
*
*/
#include <stdlib.h>
#include "st-box-layout.h"
#include "st-private.h"
#include "st-scrollable.h"
#include "st-box-layout-child.h"
static void st_box_container_iface_init (ClutterContainerIface *iface);
static void st_box_scrollable_interface_init (StScrollableInterface *iface);
enum {
PROP_0,
PROP_VERTICAL,
PROP_PACK_START,
PROP_HADJUST,
PROP_VADJUST
};
struct _StBoxLayoutPrivate
{
StAdjustment *hadjustment;
StAdjustment *vadjustment;
};
G_DEFINE_TYPE_WITH_CODE (StBoxLayout, st_box_layout, ST_TYPE_WIDGET,
G_ADD_PRIVATE (StBoxLayout)
G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
st_box_container_iface_init)
G_IMPLEMENT_INTERFACE (ST_TYPE_SCROLLABLE,
st_box_scrollable_interface_init));
/*
* StScrollable Interface Implementation
*/
static void
adjustment_value_notify_cb (StAdjustment *adjustment,
GParamSpec *pspec,
StBoxLayout *box)
{
clutter_actor_queue_relayout (CLUTTER_ACTOR (box));
}
static void
scrollable_set_adjustments (StScrollable *scrollable,
StAdjustment *hadjustment,
StAdjustment *vadjustment)
{
StBoxLayoutPrivate *priv = ST_BOX_LAYOUT (scrollable)->priv;
g_object_freeze_notify (G_OBJECT (scrollable));
if (hadjustment != priv->hadjustment)
{
if (priv->hadjustment)
{
g_signal_handlers_disconnect_by_func (priv->hadjustment,
adjustment_value_notify_cb,
scrollable);
g_object_unref (priv->hadjustment);
}
if (hadjustment)
{
g_object_ref (hadjustment);
g_signal_connect (hadjustment, "notify::value",
G_CALLBACK (adjustment_value_notify_cb),
scrollable);
}
priv->hadjustment = hadjustment;
g_object_notify (G_OBJECT (scrollable), "hadjustment");
}
if (vadjustment != priv->vadjustment)
{
if (priv->vadjustment)
{
g_signal_handlers_disconnect_by_func (priv->vadjustment,
adjustment_value_notify_cb,
scrollable);
g_object_unref (priv->vadjustment);
}
if (vadjustment)
{
g_object_ref (vadjustment);
g_signal_connect (vadjustment, "notify::value",
G_CALLBACK (adjustment_value_notify_cb),
scrollable);
}
priv->vadjustment = vadjustment;
g_object_notify (G_OBJECT (scrollable), "vadjustment");
}
g_object_thaw_notify (G_OBJECT (scrollable));
}
static void
scrollable_get_adjustments (StScrollable *scrollable,
StAdjustment **hadjustment,
StAdjustment **vadjustment)
{
StBoxLayoutPrivate *priv;
priv = (ST_BOX_LAYOUT (scrollable))->priv;
if (hadjustment)
*hadjustment = priv->hadjustment;
if (vadjustment)
*vadjustment = priv->vadjustment;
}
static void
st_box_scrollable_interface_init (StScrollableInterface *iface)
{
iface->set_adjustments = scrollable_set_adjustments;
iface->get_adjustments = scrollable_get_adjustments;
}
static void
st_box_container_iface_init (ClutterContainerIface *iface)
{
iface->child_meta_type = ST_TYPE_BOX_LAYOUT_CHILD;
}
static void
st_box_layout_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
ClutterLayoutManager *layout;
StAdjustment *adjustment;
ClutterOrientation orientation;
switch (property_id)
{
case PROP_VERTICAL:
layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (object));
orientation = clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout));
g_value_set_boolean (value, orientation == CLUTTER_ORIENTATION_VERTICAL);
break;
case PROP_PACK_START:
layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (object));
g_value_set_boolean (value, clutter_box_layout_get_pack_start (CLUTTER_BOX_LAYOUT (layout)));
break;
case PROP_HADJUST:
scrollable_get_adjustments (ST_SCROLLABLE (object), &adjustment, NULL);
g_value_set_object (value, adjustment);
break;
case PROP_VADJUST:
scrollable_get_adjustments (ST_SCROLLABLE (object), NULL, &adjustment);
g_value_set_object (value, adjustment);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
st_box_layout_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
StBoxLayout *box = ST_BOX_LAYOUT (object);
switch (property_id)
{
case PROP_VERTICAL:
st_box_layout_set_vertical (box, g_value_get_boolean (value));
break;
case PROP_PACK_START:
st_box_layout_set_pack_start (box, g_value_get_boolean (value));
break;
case PROP_HADJUST:
scrollable_set_adjustments (ST_SCROLLABLE (object),
g_value_get_object (value),
box->priv->vadjustment);
break;
case PROP_VADJUST:
scrollable_set_adjustments (ST_SCROLLABLE (object),
box->priv->hadjustment,
g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
st_box_layout_dispose (GObject *object)
{
StBoxLayoutPrivate *priv = ST_BOX_LAYOUT (object)->priv;
if (priv->hadjustment)
{
g_object_unref (priv->hadjustment);
priv->hadjustment = NULL;
}
if (priv->vadjustment)
{
g_object_unref (priv->vadjustment);
priv->vadjustment = NULL;
}
G_OBJECT_CLASS (st_box_layout_parent_class)->dispose (object);
}
static void
st_box_layout_allocate (ClutterActor *actor,
const ClutterActorBox *box,
ClutterAllocationFlags flags)
{
StBoxLayoutPrivate *priv = ST_BOX_LAYOUT (actor)->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
ClutterLayoutManager *layout = clutter_actor_get_layout_manager (actor);
ClutterActorBox viewport_content_box;
ClutterActorBox content_box;
gfloat avail_width, avail_height, min_width, natural_width, min_height, natural_height;
st_theme_node_get_content_box (theme_node, box, &viewport_content_box);
clutter_actor_box_get_size (&viewport_content_box, &avail_width, &avail_height);
clutter_layout_manager_get_preferred_width (layout, CLUTTER_CONTAINER (actor),
avail_height,
&min_width, &natural_width);
clutter_layout_manager_get_preferred_height (layout, CLUTTER_CONTAINER (actor),
MAX (avail_width, min_width),
&min_height, &natural_height);
/* Because StBoxLayout implements StScrollable, the allocation box passed here
* may not match the minimum sizes reported by the layout manager. When that
* happens, the content box needs to be adjusted to match the reported minimum
* sizes before being passed to clutter_layout_manager_allocate() */
clutter_actor_set_allocation (actor, box, flags);
content_box = viewport_content_box;
content_box.x2 += MAX (0, min_width - avail_width);
content_box.y2 += MAX (0, min_height - avail_height);
clutter_layout_manager_allocate (layout, CLUTTER_CONTAINER (actor),
&content_box, flags);
/* update adjustments for scrolling */
if (priv->vadjustment)
{
gdouble prev_value;
g_object_set (G_OBJECT (priv->vadjustment),
"lower", 0.0,
"upper", MAX (min_height, avail_height),
"page-size", avail_height,
"step-increment", avail_height / 6,
"page-increment", avail_height - avail_height / 6,
NULL);
prev_value = st_adjustment_get_value (priv->vadjustment);
st_adjustment_set_value (priv->vadjustment, prev_value);
}
if (priv->hadjustment)
{
gdouble prev_value;
g_object_set (G_OBJECT (priv->hadjustment),
"lower", 0.0,
"upper", MAX (min_width, avail_width),
"page-size", avail_width,
"step-increment", avail_width / 6,
"page-increment", avail_width - avail_width / 6,
NULL);
prev_value = st_adjustment_get_value (priv->hadjustment);
st_adjustment_set_value (priv->hadjustment, prev_value);
}
}
static void
st_box_layout_apply_transform (ClutterActor *a,
CoglMatrix *m)
{
StBoxLayoutPrivate *priv = ST_BOX_LAYOUT (a)->priv;
gdouble x, y;
CLUTTER_ACTOR_CLASS (st_box_layout_parent_class)->apply_transform (a, m);
if (priv->hadjustment)
x = st_adjustment_get_value (priv->hadjustment);
else
x = 0;
if (priv->vadjustment)
y = st_adjustment_get_value (priv->vadjustment);
else
y = 0;
cogl_matrix_translate (m, (int) -x, (int) -y, 0);
}
/* If we are translated, then we need to translate back before chaining
* up or the background and borders will be drawn in the wrong place */
static void
get_border_paint_offsets (StBoxLayout *self,
double *x,
double *y)
{
StBoxLayoutPrivate *priv = self->priv;
if (priv->hadjustment)
*x = st_adjustment_get_value (priv->hadjustment);
else
*x = 0;
if (priv->vadjustment)
*y = st_adjustment_get_value (priv->vadjustment);
else
*y = 0;
}
static void
st_box_layout_paint (ClutterActor *actor)
{
StBoxLayout *self = ST_BOX_LAYOUT (actor);
StBoxLayoutPrivate *priv = self->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
gdouble x, y;
ClutterActorBox allocation_box;
ClutterActorBox content_box;
ClutterActor *child;
CoglFramebuffer *fb = cogl_get_draw_framebuffer ();
get_border_paint_offsets (self, &x, &y);
if (x != 0 || y != 0)
{
cogl_framebuffer_push_matrix (fb);
cogl_framebuffer_translate (fb, (int)x, (int)y, 0);
}
st_widget_paint_background (ST_WIDGET (actor));
if (x != 0 || y != 0)
{
cogl_framebuffer_pop_matrix (fb);
}
if (clutter_actor_get_n_children (actor) == 0)
return;
clutter_actor_get_allocation_box (actor, &allocation_box);
st_theme_node_get_content_box (theme_node, &allocation_box, &content_box);
content_box.x1 += x;
content_box.y1 += y;
content_box.x2 += x;
content_box.y2 += y;
/* The content area forms the viewport into the scrolled contents, while
* the borders and background stay in place; after drawing the borders and
* background, we clip to the content area */
if (priv->hadjustment || priv->vadjustment)
cogl_framebuffer_push_rectangle_clip (fb,
(int)content_box.x1,
(int)content_box.y1,
(int)content_box.x2,
(int)content_box.y2);
for (child = clutter_actor_get_first_child (actor);
child != NULL;
child = clutter_actor_get_next_sibling (child))
clutter_actor_paint (child);
if (priv->hadjustment || priv->vadjustment)
cogl_framebuffer_pop_clip (fb);
}
static void
st_box_layout_pick (ClutterActor *actor,
const ClutterColor *color)
{
StBoxLayout *self = ST_BOX_LAYOUT (actor);
StBoxLayoutPrivate *priv = self->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
gdouble x, y;
ClutterActorBox allocation_box;
ClutterActorBox content_box;
ClutterActor *child;
CoglFramebuffer *fb = cogl_get_draw_framebuffer ();
get_border_paint_offsets (self, &x, &y);
if (x != 0 || y != 0)
{
cogl_framebuffer_push_matrix (fb);
cogl_framebuffer_translate (fb, (int)x, (int)y, 0);
}
CLUTTER_ACTOR_CLASS (st_box_layout_parent_class)->pick (actor, color);
if (x != 0 || y != 0)
{
cogl_framebuffer_pop_matrix (fb);
}
if (clutter_actor_get_n_children (actor) == 0)
return;
clutter_actor_get_allocation_box (actor, &allocation_box);
st_theme_node_get_content_box (theme_node, &allocation_box, &content_box);
content_box.x1 += x;
content_box.y1 += y;
content_box.x2 += x;
content_box.y2 += y;
if (priv->hadjustment || priv->vadjustment)
cogl_framebuffer_push_rectangle_clip (fb,
(int)content_box.x1,
(int)content_box.y1,
(int)content_box.x2,
(int)content_box.y2);
for (child = clutter_actor_get_first_child (actor);
child != NULL;
child = clutter_actor_get_next_sibling (child))
clutter_actor_paint (child);
if (priv->hadjustment || priv->vadjustment)
cogl_framebuffer_pop_clip (fb);
}
static gboolean
st_box_layout_get_paint_volume (ClutterActor *actor,
ClutterPaintVolume *volume)
{
StBoxLayout *self = ST_BOX_LAYOUT (actor);
gdouble x, y, lower, upper;
StBoxLayoutPrivate *priv = self->priv;
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
ClutterActorBox allocation_box;
ClutterActorBox content_box;
ClutterVertex origin;
/* Setting the paint volume does not make sense when we don't have any allocation */
if (!clutter_actor_has_allocation (actor))
return FALSE;
/* When have an adjustment we are clipped to the content box, so base
* our paint volume on that. */
if (priv->hadjustment || priv->vadjustment)
{
gdouble width, height;
clutter_actor_get_allocation_box (actor, &allocation_box);
st_theme_node_get_content_box (theme_node, &allocation_box, &content_box);
origin.x = content_box.x1 - allocation_box.x1;
origin.y = content_box.y1 - allocation_box.y2;
origin.z = 0.f;
if (priv->hadjustment)
{
g_object_get (priv->hadjustment,
"lower", &lower,
"upper", &upper,
NULL);
width = upper - lower;
}
else
{
width = content_box.x2 - content_box.x1;
}
if (priv->vadjustment)
{
g_object_get (priv->vadjustment,
"lower", &lower,
"upper", &upper,
NULL);
height = upper - lower;
}
else
{
height = content_box.y2 - content_box.y1;
}
clutter_paint_volume_set_width (volume, width);
clutter_paint_volume_set_height (volume, height);
}
else if (!CLUTTER_ACTOR_CLASS (st_box_layout_parent_class)->get_paint_volume (actor, volume))
return FALSE;
/* When scrolled, st_box_layout_apply_transform() includes the scroll offset
* and affects paint volumes. This is right for our children, but our paint volume
* is determined by our allocation and borders and doesn't scroll, so we need
* to reverse-compensate here, the same as we do when painting.
*/
get_border_paint_offsets (self, &x, &y);
if (x != 0 || y != 0)
{
clutter_paint_volume_get_origin (volume, &origin);
origin.x += x;
origin.y += y;
clutter_paint_volume_set_origin (volume, &origin);
}
return TRUE;
}
static void
st_box_layout_style_changed (StWidget *self)
{
StThemeNode *theme_node = st_widget_get_theme_node (self);
ClutterBoxLayout *layout;
double spacing;
layout = CLUTTER_BOX_LAYOUT (clutter_actor_get_layout_manager (CLUTTER_ACTOR (self)));
spacing = st_theme_node_get_length (theme_node, "spacing");
clutter_box_layout_set_spacing (layout, (int)(spacing + 0.5));
ST_WIDGET_CLASS (st_box_layout_parent_class)->style_changed (self);
}
static void
layout_notify (GObject *object,
GParamSpec *pspec,
gpointer user_data)
{
GObject *self = user_data;
const char *prop_name = g_param_spec_get_name (pspec);
if (g_object_class_find_property (G_OBJECT_GET_CLASS (self), prop_name))
g_object_notify (self, prop_name);
}
static void
on_layout_manager_notify (GObject *object,
GParamSpec *pspec,
gpointer user_data)
{
ClutterActor *actor = CLUTTER_ACTOR (object);
ClutterLayoutManager *layout = clutter_actor_get_layout_manager (actor);
g_warn_if_fail (CLUTTER_IS_BOX_LAYOUT (layout));
if (layout == NULL)
return;
g_signal_connect_swapped (layout, "layout-changed",
G_CALLBACK (clutter_actor_queue_relayout), actor);
g_signal_connect (layout, "notify", G_CALLBACK (layout_notify), object);
}
static void
st_box_layout_class_init (StBoxLayoutClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
StWidgetClass *widget_class = ST_WIDGET_CLASS (klass);
GParamSpec *pspec;
object_class->get_property = st_box_layout_get_property;
object_class->set_property = st_box_layout_set_property;
object_class->dispose = st_box_layout_dispose;
actor_class->allocate = st_box_layout_allocate;
actor_class->apply_transform = st_box_layout_apply_transform;
actor_class->paint = st_box_layout_paint;
actor_class->get_paint_volume = st_box_layout_get_paint_volume;
actor_class->pick = st_box_layout_pick;
widget_class->style_changed = st_box_layout_style_changed;
pspec = g_param_spec_boolean ("vertical",
"Vertical",
"Whether the layout should be vertical, rather"
"than horizontal",
FALSE,
ST_PARAM_READWRITE);
g_object_class_install_property (object_class, PROP_VERTICAL, pspec);
pspec = g_param_spec_boolean ("pack-start",
"Pack Start",
"Whether to pack items at the start of the box",
FALSE,
ST_PARAM_READWRITE);
g_object_class_install_property (object_class, PROP_PACK_START, pspec);
/* StScrollable properties */
g_object_class_override_property (object_class,
PROP_HADJUST,
"hadjustment");
g_object_class_override_property (object_class,
PROP_VADJUST,
"vadjustment");
}
static void
st_box_layout_init (StBoxLayout *self)
{
self->priv = st_box_layout_get_instance_private (self);
g_signal_connect (self, "notify::layout-manager",
G_CALLBACK (on_layout_manager_notify), NULL);
clutter_actor_set_layout_manager (CLUTTER_ACTOR (self), clutter_box_layout_new ());
}
/**
* st_box_layout_new:
*
* Create a new #StBoxLayout.
*
* Returns: a newly allocated #StBoxLayout
*/
StWidget *
st_box_layout_new (void)
{
return g_object_new (ST_TYPE_BOX_LAYOUT, NULL);
}
/**
* st_box_layout_set_vertical:
* @box: A #StBoxLayout
* @vertical: %TRUE if the layout should be vertical
*
* Set the value of the #StBoxLayout::vertical property
*
*/
void
st_box_layout_set_vertical (StBoxLayout *box,
gboolean vertical)
{
ClutterLayoutManager *layout;
ClutterOrientation orientation;
g_return_if_fail (ST_IS_BOX_LAYOUT (box));
layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (box));
orientation = vertical ? CLUTTER_ORIENTATION_VERTICAL
: CLUTTER_ORIENTATION_HORIZONTAL;
if (clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout)) != orientation)
{
clutter_box_layout_set_orientation (CLUTTER_BOX_LAYOUT (layout), orientation);
g_object_notify (G_OBJECT (box), "vertical");
}
}
/**
* st_box_layout_get_vertical:
* @box: A #StBoxLayout
*
* Get the value of the #StBoxLayout::vertical property.
*
* Returns: %TRUE if the layout is vertical
*/
gboolean
st_box_layout_get_vertical (StBoxLayout *box)
{
ClutterLayoutManager *layout;
ClutterOrientation orientation;
g_return_val_if_fail (ST_IS_BOX_LAYOUT (box), FALSE);
layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (box));
orientation = clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout));
return orientation == CLUTTER_ORIENTATION_VERTICAL;
}
/**
* st_box_layout_set_pack_start:
* @box: A #StBoxLayout
* @pack_start: %TRUE if the layout should use pack-start
*
* Set the value of the #StBoxLayout::pack-start property.
*
*/
void
st_box_layout_set_pack_start (StBoxLayout *box,
gboolean pack_start)
{
ClutterBoxLayout *layout;
g_return_if_fail (ST_IS_BOX_LAYOUT (box));
layout = CLUTTER_BOX_LAYOUT (clutter_actor_get_layout_manager (CLUTTER_ACTOR (box)));
if (clutter_box_layout_get_pack_start (layout) != pack_start)
{
clutter_box_layout_set_pack_start (layout, pack_start);
g_object_notify (G_OBJECT (box), "pack-start");
}
}
/**
* st_box_layout_get_pack_start:
* @box: A #StBoxLayout
*
* Get the value of the #StBoxLayout::pack-start property.
*
* Returns: %TRUE if pack-start is enabled
*/
gboolean
st_box_layout_get_pack_start (StBoxLayout *box)
{
g_return_val_if_fail (ST_IS_BOX_LAYOUT (box), FALSE);
return clutter_box_layout_get_pack_start (CLUTTER_BOX_LAYOUT (clutter_actor_get_layout_manager (CLUTTER_ACTOR (box))));
}