/* * Copyright © 2015 Red Hat Inc. * * 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.1 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 . * * Authors: Benjamin Otte */ #include "config.h" #include "gtkboxgadgetprivate.h" #include "gtkcssnodeprivate.h" #include "gtkcsspositionvalueprivate.h" #include "gtkmain.h" #include "gtkprivate.h" #include "gtksizerequest.h" #include "gtkwidgetprivate.h" #include "gtkcontainerprivate.h" /* GtkBoxGadget is a container gadget implementation that arranges its * children in a row, either horizontally or vertically. Children can * be either widgets or gadgets, and can be set to expand horizontally * or vertically, or both. */ typedef struct _GtkBoxGadgetPrivate GtkBoxGadgetPrivate; struct _GtkBoxGadgetPrivate { GtkOrientation orientation; GArray *children; guint draw_focus : 1; guint draw_reverse : 1; guint allocate_reverse : 1; guint align_reverse : 1; }; typedef gboolean (* ComputeExpandFunc) (GObject *object, GtkOrientation orientation); typedef struct _GtkBoxGadgetChild GtkBoxGadgetChild; struct _GtkBoxGadgetChild { GObject *object; gboolean expand; GtkAlign align; }; G_DEFINE_TYPE_WITH_CODE (GtkBoxGadget, gtk_box_gadget, GTK_TYPE_CSS_GADGET, G_ADD_PRIVATE (GtkBoxGadget)) static gboolean gtk_box_gadget_child_is_visible (GObject *child) { if (GTK_IS_WIDGET (child)) return gtk_widget_get_visible (GTK_WIDGET (child)); else return gtk_css_gadget_get_visible (GTK_CSS_GADGET (child)); } static gboolean gtk_box_gadget_child_compute_expand (GtkBoxGadget *gadget, GtkBoxGadgetChild *child) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); if (child->expand) return TRUE; if (GTK_IS_WIDGET (child->object)) return gtk_widget_compute_expand (GTK_WIDGET (child->object), priv->orientation); return FALSE; } static GtkAlign gtk_box_gadget_child_get_align (GtkBoxGadget *gadget, GtkBoxGadgetChild *child) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); GtkAlign align; if (GTK_IS_WIDGET (child->object)) { if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) g_object_get (child->object, "valign", &align, NULL); else g_object_get (child->object, "halign", &align, NULL); } else align = child->align; return align; } static GtkAlign effective_align (GtkAlign align, gboolean reverse) { switch (align) { case GTK_ALIGN_START: return reverse ? GTK_ALIGN_END : GTK_ALIGN_START; case GTK_ALIGN_END: return reverse ? GTK_ALIGN_START : GTK_ALIGN_END; default: return align; } } static int get_spacing (GtkCssGadget *gadget) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); GtkCssValue *spacing = gtk_css_style_get_value (gtk_css_gadget_get_style (gadget), GTK_CSS_PROPERTY_BORDER_SPACING); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) return _gtk_css_position_value_get_x (spacing, 100); else return _gtk_css_position_value_get_y (spacing, 100); } static void gtk_box_gadget_measure_child (GObject *child, GtkOrientation orientation, gint for_size, gint *minimum, gint *natural, gint *minimum_baseline, gint *natural_baseline) { if (GTK_IS_WIDGET (child)) { gtk_widget_measure (GTK_WIDGET (child), orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); } else { gtk_css_gadget_get_preferred_size (GTK_CSS_GADGET (child), orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); } } static void gtk_box_gadget_distribute (GtkBoxGadget *gadget, gint for_size, gint size, GtkRequestedSize *sizes) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); guint i, n_expand; gint spacing; n_expand = 0; spacing = get_spacing (GTK_CSS_GADGET (gadget)); size += spacing; for (i = 0 ; i < priv->children->len; i++) { GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, i); gtk_box_gadget_measure_child (child->object, priv->orientation, for_size, &sizes[i].minimum_size, &sizes[i].natural_size, NULL, NULL); if (gtk_box_gadget_child_is_visible (child->object)) { size -= spacing; if (gtk_box_gadget_child_compute_expand (gadget, child)) n_expand++; } size -= sizes[i].minimum_size; } if G_UNLIKELY (size < 0) { g_critical ("%s: assertion 'size >= 0' failed in %s", G_STRFUNC, G_OBJECT_TYPE_NAME (gtk_css_gadget_get_owner (GTK_CSS_GADGET (gadget)))); return; } size = gtk_distribute_natural_allocation (size, priv->children->len, sizes); if (size <= 0 || n_expand == 0) return; for (i = 0 ; i < priv->children->len; i++) { GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, i); if (!gtk_box_gadget_child_is_visible (child->object) || !gtk_box_gadget_child_compute_expand (gadget, child)) continue; sizes[i].minimum_size += size / n_expand; /* distribute all pixels, even if there's a remainder */ size -= size / n_expand; n_expand--; } } static void gtk_box_gadget_measure_orientation (GtkCssGadget *gadget, GtkOrientation orientation, gint for_size, gint *minimum, gint *natural, gint *minimum_baseline, gint *natural_baseline) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); gint child_min, child_nat, spacing; guint i, n_visible; *minimum = 0; *natural = 0; spacing = get_spacing (gadget); n_visible = 0; for (i = 0 ; i < priv->children->len; i++) { GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, i); gtk_box_gadget_measure_child (child->object, orientation, for_size, &child_min, &child_nat, NULL, NULL); *minimum += child_min; *natural += child_nat; if (gtk_box_gadget_child_is_visible (child->object)) n_visible++; } *minimum += (MAX (n_visible, 1) - 1) * spacing; *natural += (MAX (n_visible, 1) - 1) * spacing; } static void gtk_box_gadget_measure_opposite (GtkCssGadget *gadget, GtkOrientation orientation, gint for_size, gint *minimum, gint *natural, gint *minimum_baseline, gint *natural_baseline) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); int child_min, child_nat, child_min_baseline, child_nat_baseline; int total_min, above_min, below_min, total_nat, above_nat, below_nat; GtkRequestedSize *sizes; guint i; if (for_size >= 0) { sizes = g_newa (GtkRequestedSize, priv->children->len); gtk_box_gadget_distribute (GTK_BOX_GADGET (gadget), -1, for_size, sizes); } above_min = below_min = above_nat = below_nat = -1; total_min = total_nat = 0; for (i = 0 ; i < priv->children->len; i++) { GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, i); gtk_box_gadget_measure_child (child->object, orientation, for_size >= 0 ? sizes[i].minimum_size : -1, &child_min, &child_nat, &child_min_baseline, &child_nat_baseline); if (child_min_baseline >= 0) { below_min = MAX (below_min, child_min - child_min_baseline); above_min = MAX (above_min, child_min_baseline); below_nat = MAX (below_nat, child_nat - child_nat_baseline); above_nat = MAX (above_nat, child_nat_baseline); } else { total_min = MAX (total_min, child_min); total_nat = MAX (total_nat, child_nat); } } if (above_min >= 0) { total_min = MAX (total_min, above_min + below_min); total_nat = MAX (total_nat, above_nat + below_nat); /* assume GTK_BASELINE_POSITION_CENTER for now */ if (minimum_baseline) *minimum_baseline = above_min + (total_min - (above_min + below_min)) / 2; if (natural_baseline) *natural_baseline = above_nat + (total_nat - (above_nat + below_nat)) / 2; } *minimum = total_min; *natural = total_nat; } static void gtk_box_gadget_get_preferred_size (GtkCssGadget *gadget, GtkOrientation orientation, gint for_size, gint *minimum, gint *natural, gint *minimum_baseline, gint *natural_baseline) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); if (priv->orientation == orientation) gtk_box_gadget_measure_orientation (gadget, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); else gtk_box_gadget_measure_opposite (gadget, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); } static void gtk_box_gadget_allocate_child (GObject *child, GtkOrientation box_orientation, GtkAlign child_align, GtkAllocation *allocation, int baseline, GtkAllocation *out_clip) { if (GTK_IS_WIDGET (child)) { gtk_widget_size_allocate_with_baseline (GTK_WIDGET (child), allocation, baseline); gtk_widget_get_clip (GTK_WIDGET (child), out_clip); } else { GtkAllocation child_allocation; int minimum, natural; int minimum_baseline, natural_baseline; if (box_orientation == GTK_ORIENTATION_HORIZONTAL) { child_allocation.width = allocation->width; child_allocation.x = allocation->x; gtk_css_gadget_get_preferred_size (GTK_CSS_GADGET (child), GTK_ORIENTATION_VERTICAL, allocation->width, &minimum, &natural, &minimum_baseline, &natural_baseline); switch (child_align) { case GTK_ALIGN_FILL: child_allocation.height = allocation->height; child_allocation.y = allocation->y; break; case GTK_ALIGN_START: child_allocation.height = MIN(natural, allocation->height); child_allocation.y = allocation->y; break; case GTK_ALIGN_END: child_allocation.height = MIN(natural, allocation->height); child_allocation.y = allocation->y + allocation->height - child_allocation.height; break; case GTK_ALIGN_BASELINE: if (minimum_baseline >= 0 && baseline >= 0) { child_allocation.height = MIN(natural, allocation->height); child_allocation.y = allocation->y + MAX(0, baseline - minimum_baseline); break; } case GTK_ALIGN_CENTER: child_allocation.height = MIN(natural, allocation->height); child_allocation.y = allocation->y + (allocation->height - child_allocation.height) / 2; break; default: g_assert_not_reached (); } } else { child_allocation.height = allocation->height; child_allocation.y = allocation->y; gtk_css_gadget_get_preferred_size (GTK_CSS_GADGET (child), GTK_ORIENTATION_HORIZONTAL, allocation->height, &minimum, &natural, NULL, NULL); switch (child_align) { case GTK_ALIGN_FILL: child_allocation.width = allocation->width; child_allocation.x = allocation->x; break; case GTK_ALIGN_START: child_allocation.width = MIN(natural, allocation->width); child_allocation.x = allocation->x; break; case GTK_ALIGN_END: child_allocation.width = MIN(natural, allocation->width); child_allocation.x = allocation->x + allocation->width - child_allocation.width; break; case GTK_ALIGN_BASELINE: case GTK_ALIGN_CENTER: child_allocation.width = MIN(natural, allocation->width); child_allocation.x = allocation->x + (allocation->width - child_allocation.width) / 2; break; default: g_assert_not_reached (); } } gtk_css_gadget_allocate (GTK_CSS_GADGET (child), &child_allocation, baseline, out_clip); } } static void gtk_box_gadget_allocate (GtkCssGadget *gadget, const GtkAllocation *allocation, int baseline, GtkAllocation *out_clip) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); GtkRequestedSize *sizes; GtkAllocation child_allocation, child_clip; GtkAlign child_align; gint spacing; guint i; child_allocation = *allocation; sizes = g_newa (GtkRequestedSize, priv->children->len); spacing = get_spacing (gadget); if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) { gtk_box_gadget_distribute (GTK_BOX_GADGET (gadget), allocation->height, allocation->width, sizes); if (priv->allocate_reverse) child_allocation.x = allocation->x + allocation->width; for (i = 0; i < priv->children->len; i++) { guint idx = priv->allocate_reverse ? priv->children->len - 1 - i : i; GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, idx); child_allocation.width = sizes[idx].minimum_size; child_allocation.height = allocation->height; child_allocation.y = allocation->y; if (priv->allocate_reverse) child_allocation.x -= child_allocation.width + spacing; child_align = gtk_box_gadget_child_get_align (GTK_BOX_GADGET (gadget), child); gtk_box_gadget_allocate_child (child->object, priv->orientation, effective_align (child_align, priv->align_reverse), &child_allocation, baseline, &child_clip); if (i == 0) *out_clip = child_clip; else gdk_rectangle_union (out_clip, &child_clip, out_clip); if (!priv->allocate_reverse) child_allocation.x += sizes[idx].minimum_size + spacing; } } else { gtk_box_gadget_distribute (GTK_BOX_GADGET (gadget), allocation->width, allocation->height, sizes); if (priv->allocate_reverse) child_allocation.y = allocation->y + allocation->height; for (i = 0 ; i < priv->children->len; i++) { guint idx = priv->allocate_reverse ? priv->children->len - 1 - i : i; GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, idx); child_allocation.height = sizes[idx].minimum_size; child_allocation.width = allocation->width; child_allocation.x = allocation->x; if (priv->allocate_reverse) child_allocation.y -= child_allocation.height + spacing; child_align = gtk_box_gadget_child_get_align (GTK_BOX_GADGET (gadget), child); gtk_box_gadget_allocate_child (child->object, priv->orientation, effective_align (child_align, priv->align_reverse), &child_allocation, -1, &child_clip); if (i == 0) *out_clip = child_clip; else gdk_rectangle_union (out_clip, &child_clip, out_clip); if (!priv->allocate_reverse) child_allocation.y += sizes[idx].minimum_size + spacing; } } } static gboolean gtk_box_gadget_snapshot (GtkCssGadget *gadget, GtkSnapshot *snapshot, int x, int y, int width, int height) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); GtkWidget *owner = gtk_css_gadget_get_owner (gadget); guint i; for (i = 0; i < priv->children->len; i++) { guint draw_index = priv->draw_reverse ? priv->children->len - 1 - i : i; GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, draw_index); if (GTK_IS_WIDGET (child->object)) gtk_widget_snapshot_child (owner, GTK_WIDGET (child->object), snapshot); else gtk_css_gadget_snapshot (GTK_CSS_GADGET (child->object), snapshot); } if (priv->draw_focus && gtk_widget_has_visible_focus (owner)) return TRUE; return FALSE; } static void gtk_box_gadget_finalize (GObject *object) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (object)); g_array_free (priv->children, TRUE); G_OBJECT_CLASS (gtk_box_gadget_parent_class)->finalize (object); } static void gtk_box_gadget_class_init (GtkBoxGadgetClass *klass) { GtkCssGadgetClass *gadget_class = GTK_CSS_GADGET_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gtk_box_gadget_finalize; gadget_class->get_preferred_size = gtk_box_gadget_get_preferred_size; gadget_class->allocate = gtk_box_gadget_allocate; gadget_class->snapshot = gtk_box_gadget_snapshot; } static void gtk_box_gadget_clear_child (gpointer data) { GtkBoxGadgetChild *child = data; g_object_unref (child->object); } static void gtk_box_gadget_init (GtkBoxGadget *gadget) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->children = g_array_new (FALSE, FALSE, sizeof (GtkBoxGadgetChild)); g_array_set_clear_func (priv->children, gtk_box_gadget_clear_child); } GtkCssGadget * gtk_box_gadget_new_for_node (GtkCssNode *node, GtkWidget *owner) { return g_object_new (GTK_TYPE_BOX_GADGET, "node", node, "owner", owner, NULL); } GtkCssGadget * gtk_box_gadget_new (const char *name, GtkWidget *owner, GtkCssGadget *parent, GtkCssGadget *next_sibling) { GtkCssNode *node; GtkCssGadget *result; node = gtk_css_node_new (); gtk_css_node_set_name (node, g_intern_string (name)); if (parent) gtk_css_node_insert_before (gtk_css_gadget_get_node (parent), node, next_sibling ? gtk_css_gadget_get_node (next_sibling) : NULL); result = gtk_box_gadget_new_for_node (node, owner); g_object_unref (node); return result; } void gtk_box_gadget_set_orientation (GtkBoxGadget *gadget, GtkOrientation orientation) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->orientation = orientation; } void gtk_box_gadget_set_draw_focus (GtkBoxGadget *gadget, gboolean draw_focus) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->draw_focus = draw_focus; } void gtk_box_gadget_set_draw_reverse (GtkBoxGadget *gadget, gboolean draw_reverse) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->draw_reverse = draw_reverse; } void gtk_box_gadget_set_allocate_reverse (GtkBoxGadget *gadget, gboolean allocate_reverse) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->allocate_reverse = allocate_reverse; } void gtk_box_gadget_set_align_reverse (GtkBoxGadget *gadget, gboolean align_reverse) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); priv->align_reverse = align_reverse; } static GtkCssNode * get_css_node (GObject *child) { if (GTK_IS_WIDGET (child)) return gtk_widget_get_css_node (GTK_WIDGET (child)); else return gtk_css_gadget_get_node (GTK_CSS_GADGET (child)); } static void gtk_box_gadget_insert_object (GtkBoxGadget *gadget, int pos, GObject *object, gboolean expand, GtkAlign align) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); GtkBoxGadgetChild child; child.object = g_object_ref (object); child.expand = expand; child.align = align; if (pos < 0 || pos >= priv->children->len) { g_array_append_val (priv->children, child); gtk_css_node_insert_before (gtk_css_gadget_get_node (GTK_CSS_GADGET (gadget)), get_css_node (object), NULL); } else { g_array_insert_val (priv->children, pos, child); gtk_css_node_insert_before (gtk_css_gadget_get_node (GTK_CSS_GADGET (gadget)), get_css_node (object), get_css_node (g_array_index (priv->children, GtkBoxGadgetChild, pos + 1).object)); } } void gtk_box_gadget_insert_widget (GtkBoxGadget *gadget, int pos, GtkWidget *widget) { gtk_box_gadget_insert_object (gadget, pos, G_OBJECT (widget), FALSE, GTK_ALIGN_FILL); } static GtkBoxGadgetChild * gtk_box_gadget_find_object (GtkBoxGadget *gadget, GObject *object, int *position) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); guint i; for (i = 0; i < priv->children->len; i++) { GtkBoxGadgetChild *child = &g_array_index (priv->children, GtkBoxGadgetChild, i); if (child->object == object) { if (position) *position = i; return child; } } return NULL; } static void gtk_box_gadget_remove_object (GtkBoxGadget *gadget, GObject *object) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (gadget); GtkBoxGadgetChild *child; int position; child = gtk_box_gadget_find_object (gadget, object, &position); if (child) { gtk_css_node_set_parent (get_css_node (child->object), NULL); g_array_remove_index (priv->children, position); } } void gtk_box_gadget_remove_widget (GtkBoxGadget *gadget, GtkWidget *widget) { gtk_box_gadget_remove_object (gadget, G_OBJECT (widget)); } void gtk_box_gadget_insert_gadget_before (GtkBoxGadget *gadget, GtkCssGadget *sibling, GtkCssGadget *cssgadget, gboolean expand, GtkAlign align) { /* Insert at the end if no sibling specified */ int pos = -1; if (sibling) gtk_box_gadget_find_object (gadget, G_OBJECT (sibling), &pos); gtk_box_gadget_insert_gadget (gadget, pos, cssgadget, expand, align); } void gtk_box_gadget_insert_gadget_after (GtkBoxGadget *gadget, GtkCssGadget *sibling, GtkCssGadget *cssgadget, gboolean expand, GtkAlign align) { /* Insert at the beginning if no sibling specified */ int pos = 0; if (sibling && gtk_box_gadget_find_object (gadget, G_OBJECT (sibling), &pos)) pos++; gtk_box_gadget_insert_gadget (gadget, pos, cssgadget, expand, align); } void gtk_box_gadget_insert_gadget (GtkBoxGadget *gadget, int pos, GtkCssGadget *cssgadget, gboolean expand, GtkAlign align) { gtk_box_gadget_insert_object (gadget, pos, G_OBJECT (cssgadget), expand, align); } void gtk_box_gadget_remove_gadget (GtkBoxGadget *gadget, GtkCssGadget *cssgadget) { gtk_box_gadget_remove_object (gadget, G_OBJECT (cssgadget)); } void gtk_box_gadget_reverse_children (GtkBoxGadget *gadget) { GtkBoxGadgetPrivate *priv = gtk_box_gadget_get_instance_private (GTK_BOX_GADGET (gadget)); int i, j; gtk_css_node_reverse_children (gtk_css_gadget_get_node (GTK_CSS_GADGET (gadget))); for (i = 0, j = priv->children->len - 1; i < j; i++, j--) { GtkBoxGadgetChild *child1 = &g_array_index (priv->children, GtkBoxGadgetChild, i); GtkBoxGadgetChild *child2 = &g_array_index (priv->children, GtkBoxGadgetChild, j); GtkBoxGadgetChild tmp; tmp = *child1; *child1 = *child2; *child2 = tmp; } } void gtk_box_gadget_set_gadget_expand (GtkBoxGadget *gadget, GObject *object, gboolean expand) { GtkBoxGadgetChild *child; child = gtk_box_gadget_find_object (gadget, object, NULL); if (!child) return; if (child->expand == expand) return; child->expand = expand; gtk_css_gadget_queue_resize (GTK_CSS_GADGET (gadget)); } void gtk_box_gadget_set_gadget_align (GtkBoxGadget *gadget, GObject *object, GtkAlign align) { GtkBoxGadgetChild *child; child = gtk_box_gadget_find_object (gadget, object, NULL); if (!child) return; if (child->align == align) return; child->align = align; gtk_css_gadget_queue_resize (GTK_CSS_GADGET (gadget)); }