Blob Blame History Raw
/* dzl-column-layout.c
 *
 * Copyright (C) 2016 Christian Hergert <chergert@redhat.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "dzl-column-layout.h"

typedef struct
{
  GtkWidget      *widget;
  GtkAllocation   alloc;
  GtkRequisition  req;
  GtkRequisition  min_req;
  gint            priority;
} DzlColumnLayoutChild;

typedef struct
{
  GArray *children;
  gint    column_width;
  gint    column_spacing;
  gint    row_spacing;
  guint   max_columns;
} DzlColumnLayoutPrivate;

#define COLUMN_WIDTH_DEFAULT   500
#define COLUMN_SPACING_DEFAULT 24
#define ROW_SPACING_DEFAULT    24

G_DEFINE_TYPE_WITH_PRIVATE (DzlColumnLayout, dzl_column_layout, GTK_TYPE_CONTAINER)

enum {
  PROP_0,
  PROP_COLUMN_WIDTH,
  PROP_COLUMN_SPACING,
  PROP_MAX_COLUMNS,
  PROP_ROW_SPACING,
  LAST_PROP
};

enum {
  CHILD_PROP_0,
  CHILD_PROP_PRIORITY,
  LAST_CHILD_PROP
};

static GParamSpec *properties [LAST_PROP];
static GParamSpec *child_properties [LAST_CHILD_PROP];

static void
dzl_column_layout_layout (DzlColumnLayout *self,
                          gint             width,
                          gint             height,
                          gint            *tallest_column)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  gint real_tallest_column = 0;
  gint total_height = 0;
  gint n_columns = 0;
  gint border_width;
  gint column;
  guint i;

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (width > 0);
  g_assert (tallest_column != NULL);

  /*
   * We want to layout the children in a series of columns, but try to
   * fill up each column before spilling into the next column.
   *
   * We can determine the number of columns we can support by the width
   * of our allocation, and determine the max-height of each column
   * by dividing the total height of all children by the number of
   * columns. There is the chance that non-uniform sizing will mess up
   * the height a bit here, but in practice it's mostly okay.
   *
   * The order of children is sorted by the priority, so that we know
   * we can allocate them serially as we walk the array.
   *
   * We keep allocating children until we will go over the height of
   * the column.
   */

  border_width = gtk_container_get_border_width (GTK_CONTAINER (self));
  total_height = border_width * 2;

  for (i = 0; i < priv->children->len; i++)
    {
      DzlColumnLayoutChild *child;

      child = &g_array_index (priv->children, DzlColumnLayoutChild, i);

      gtk_widget_get_preferred_height_for_width (child->widget,
                                                 priv->column_width,
                                                 &child->min_req.height,
                                                 &child->req.height);

      if (i != 0)
        total_height += priv->row_spacing;
      total_height += child->req.height;
    }

  if (total_height <= height)
    n_columns = 1;
  else
    n_columns = MAX (1, (width - (border_width * 2)) / (priv->column_width + priv->column_spacing));

  if (priv->max_columns > 0)
    n_columns = MIN (n_columns, (gint)priv->max_columns);

  for (column = 0, i = 0; column < n_columns; column++)
    {
      GtkAllocation alloc;
      gint j = 0;

      alloc.x = border_width + (priv->column_width * column) + (column * priv->column_spacing);
      alloc.y = border_width;
      alloc.width = priv->column_width;
      alloc.height = (height != 0) ? (height - (border_width * 2)) : total_height / n_columns;

      for (; i < priv->children->len; i++)
        {
          DzlColumnLayoutChild *child;
          gint child_height;

          child = &g_array_index (priv->children, DzlColumnLayoutChild, i);

          /*
           * Ignore this child if it is not visible.
           */
          if (!gtk_widget_get_visible (child->widget) ||
              !gtk_widget_get_child_visible (child->widget))
            continue;

          /*
           * If we are discovering height, and this is the last item in the
           * first column, and we only have one column, then we will just
           * make this "vexpand".
           */
          if (priv->max_columns == 1 && i == priv->children->len - 1)
            {
              if (height == 0)
                child_height = child->min_req.height;
              else
                child_height = alloc.height;
            }
          else
            child_height = child->req.height;

          /*
           * If the child requisition is taller than the space we have left in
           * this column, we need to spill over to the next column.
           */
          if ((j != 0) && (child_height > alloc.height) && (column < (n_columns - 1)))
            break;

          child->alloc.x = alloc.x;
          child->alloc.y = alloc.y;
          child->alloc.width = priv->column_width;
          child->alloc.height = child_height;

#if 0
          g_print ("Allocating child to: [%d] %d,%d %dx%d\n",
                   column,
                   child->alloc.x,
                   child->alloc.y,
                   child->alloc.width,
                   child->alloc.height);
#endif

          alloc.y += child_height + priv->row_spacing;
          alloc.height -= child_height + priv->row_spacing;

          if (alloc.y > real_tallest_column)
            real_tallest_column = alloc.y;

          j++;
        }
    }

  real_tallest_column += border_width;

  *tallest_column = real_tallest_column;
}

static GtkSizeRequestMode
dzl_column_layout_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
dzl_column_layout_get_preferred_width (GtkWidget *widget,
                                       gint      *min_width,
                                       gint      *nat_width)
{
  DzlColumnLayout *self = (DzlColumnLayout *)widget;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  gint border_width;
  gint n_columns = 3;

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (min_width != NULL);
  g_assert (nat_width != NULL);

  border_width = gtk_container_get_border_width (GTK_CONTAINER (self));

  /*
   * By default we try to natural size up to 3 columns. Otherwise, we
   * use the max_columns. It would be nice if we could deal with this
   * in a better way, but that is going to take a bunch more solving.
   */

  if (priv->max_columns > 0)
    n_columns = priv->max_columns;

  *nat_width = (priv->column_width * n_columns) + (priv->column_spacing * (n_columns - 1)) + (border_width * 2);
  *min_width = priv->column_width + (border_width * 2);
}

static void
dzl_column_layout_get_preferred_height_for_width (GtkWidget *widget,
                                                  gint       width,
                                                  gint      *min_height,
                                                  gint      *nat_height)
{
  DzlColumnLayout *self = (DzlColumnLayout *)widget;
  gint tallest_column = 0;

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (min_height != NULL);
  g_assert (nat_height != NULL);

  dzl_column_layout_layout (self, width, 0, &tallest_column);

  *min_height = *nat_height = tallest_column;
}

static void
dzl_column_layout_size_allocate (GtkWidget     *widget,
                                 GtkAllocation *allocation)
{
  DzlColumnLayout *self = (DzlColumnLayout *)widget;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  gint tallest_column = 0;
  guint i;

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (allocation != NULL);

  gtk_widget_set_allocation (widget, allocation);

  dzl_column_layout_layout (self, allocation->width, allocation->height, &tallest_column);

  /*
   * If we are on a RTL language, flip all our allocations around so
   * we move from right to left. This is easier than adding all the
   * complexity to directions during layout time.
   */
  if (GTK_TEXT_DIR_RTL == gtk_widget_get_direction (widget))
    {
      for (i = 0; i < priv->children->len; i++)
        {
          DzlColumnLayoutChild *child;

          child = &g_array_index (priv->children, DzlColumnLayoutChild, i);
          child->alloc.x = allocation->x + allocation->width - child->alloc.x - child->alloc.width;
        }
    }

  for (i = 0; i < priv->children->len; i++)
    {
      DzlColumnLayoutChild *child;

      child = &g_array_index (priv->children, DzlColumnLayoutChild, i);
      gtk_widget_size_allocate (child->widget, &child->alloc);
    }
}

static gint
dzl_column_layout_child_compare (gconstpointer a,
                                 gconstpointer b)
{
  const DzlColumnLayoutChild *child_a = a;
  const DzlColumnLayoutChild *child_b = b;

  return child_a->priority - child_b->priority;
}

static void
dzl_column_layout_add (GtkContainer *container,
                       GtkWidget    *widget)
{
  DzlColumnLayout *self = (DzlColumnLayout *)container;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  DzlColumnLayoutChild child = { 0 };

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (GTK_IS_WIDGET (widget));

  child.widget = g_object_ref_sink (widget);
  child.priority = 0;

  g_array_append_val (priv->children, child);
  g_array_sort (priv->children, dzl_column_layout_child_compare);

  gtk_widget_set_parent (widget, GTK_WIDGET (self));
  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static void
dzl_column_layout_remove (GtkContainer *container,
                          GtkWidget    *widget)
{
  DzlColumnLayout *self = (DzlColumnLayout *)container;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  guint i;

  g_assert (GTK_IS_CONTAINER (container));
  g_assert (GTK_IS_WIDGET (widget));

  for (i = 0; i < priv->children->len; i++)
    {
      DzlColumnLayoutChild *child;

      child = &g_array_index (priv->children, DzlColumnLayoutChild, i);

      if (child->widget == widget)
        {
          gtk_widget_unparent (child->widget);
          g_array_remove_index (priv->children, i);
          gtk_widget_queue_resize (GTK_WIDGET (self));
          return;
        }
    }
}

static void
dzl_column_layout_forall (GtkContainer *container,
                          gboolean      include_internals,
                          GtkCallback   callback,
                          gpointer      user_data)
{
  DzlColumnLayout *self = (DzlColumnLayout *)container;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  gint i;

  g_assert (GTK_IS_CONTAINER (container));
  g_assert (callback != NULL);

  /*
   * We walk backwards in the array to be safe against callback destorying
   * the widget (and causing it to be removed).
   */

  for (i = priv->children->len; i > 0; i--)
    {
      DzlColumnLayoutChild *child;

      child = &g_array_index (priv->children, DzlColumnLayoutChild, i - 1);
      callback (child->widget, user_data);
    }
}

static DzlColumnLayoutChild *
dzl_column_layout_find_child (DzlColumnLayout *self,
                              GtkWidget       *widget)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_assert (DZL_IS_COLUMN_LAYOUT (self));
  g_assert (GTK_IS_WIDGET (widget));

  for (guint i = 0; i < priv->children->len; i++)
    {
      DzlColumnLayoutChild *child;

      child = &g_array_index (priv->children, DzlColumnLayoutChild, i);

      if (child->widget == widget)
        return child;
    }

  g_assert_not_reached ();

  return NULL;
}

gint
dzl_column_layout_get_column_width (DzlColumnLayout *self)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0);
  return priv->column_width;
}

void
dzl_column_layout_set_column_width (DzlColumnLayout *self,
                                    gint             column_width)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self));
  g_return_if_fail (column_width >= 0);

  if (priv->column_width != column_width)
    {
      priv->column_width = column_width;
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COLUMN_WIDTH]);
    }
}

gint
dzl_column_layout_get_column_spacing (DzlColumnLayout *self)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0);
  return priv->column_spacing;
}

void
dzl_column_layout_set_column_spacing (DzlColumnLayout *self,
                                      gint             column_spacing)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self));
  g_return_if_fail (column_spacing >= 0);

  if (priv->column_spacing != column_spacing)
    {
      priv->column_spacing = column_spacing;
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COLUMN_SPACING]);
    }
}

gint
dzl_column_layout_get_row_spacing (DzlColumnLayout *self)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);
  g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0);
  return priv->row_spacing;
}

void
dzl_column_layout_set_row_spacing (DzlColumnLayout *self,
                                   gint             row_spacing)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self));
  g_return_if_fail (row_spacing >= 0);

  if (priv->row_spacing != row_spacing)
    {
      priv->row_spacing = row_spacing;
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROW_SPACING]);
    }
}

static void
dzl_column_layout_get_child_property (GtkContainer *container,
                                      GtkWidget    *widget,
                                      guint         prop_id,
                                      GValue       *value,
                                      GParamSpec   *pspec)
{
  DzlColumnLayout *self = (DzlColumnLayout *)container;
  DzlColumnLayoutChild *child = dzl_column_layout_find_child (self, widget);

  switch (prop_id)
    {
    case CHILD_PROP_PRIORITY:
      g_value_set_int (value, child->priority);
      break;

    default:
      GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec);
    }
}

static void
dzl_column_layout_set_child_property (GtkContainer *container,
                                      GtkWidget    *widget,
                                      guint         prop_id,
                                      const GValue *value,
                                      GParamSpec   *pspec)
{
  DzlColumnLayout *self = (DzlColumnLayout *)container;
  DzlColumnLayoutChild *child = dzl_column_layout_find_child (self, widget);

  switch (prop_id)
    {
    case CHILD_PROP_PRIORITY:
      child->priority = g_value_get_int (value);
      gtk_widget_queue_allocate (GTK_WIDGET (self));
      break;

    default:
      GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, prop_id, pspec);
    }
}

static void
dzl_column_layout_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  DzlColumnLayout *self = DZL_COLUMN_LAYOUT(object);

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      g_value_set_int (value, dzl_column_layout_get_column_spacing (self));
      break;

    case PROP_COLUMN_WIDTH:
      g_value_set_int (value, dzl_column_layout_get_column_width (self));
      break;

    case PROP_MAX_COLUMNS:
      g_value_set_uint (value, dzl_column_layout_get_max_columns (self));
      break;

    case PROP_ROW_SPACING:
      g_value_set_int (value, dzl_column_layout_get_row_spacing (self));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
dzl_column_layout_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  DzlColumnLayout *self = DZL_COLUMN_LAYOUT(object);

  switch (prop_id)
    {
    case PROP_COLUMN_SPACING:
      dzl_column_layout_set_column_spacing (self, g_value_get_int (value));
      break;

    case PROP_COLUMN_WIDTH:
      dzl_column_layout_set_column_width (self, g_value_get_int (value));
      break;

    case PROP_MAX_COLUMNS:
      dzl_column_layout_set_max_columns (self, g_value_get_uint (value));
      break;

    case PROP_ROW_SPACING:
      dzl_column_layout_set_row_spacing (self, g_value_get_int (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    }
}

static void
dzl_column_layout_finalize (GObject *object)
{
  DzlColumnLayout *self = (DzlColumnLayout *)object;
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_clear_pointer (&priv->children, g_array_unref);

  G_OBJECT_CLASS (dzl_column_layout_parent_class)->finalize (object);
}

static void
dzl_column_layout_class_init (DzlColumnLayoutClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);

  object_class->finalize = dzl_column_layout_finalize;
  object_class->get_property = dzl_column_layout_get_property;
  object_class->set_property = dzl_column_layout_set_property;

  properties [PROP_COLUMN_SPACING] =
    g_param_spec_int ("column-spacing",
                      "Column Spacing",
                      "The spacing between columns",
                      0,
                      G_MAXINT,
                      COLUMN_SPACING_DEFAULT,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_COLUMN_WIDTH] =
    g_param_spec_int ("column-width",
                      "Column Width",
                      "The width of the columns",
                      0,
                      G_MAXINT,
                      COLUMN_WIDTH_DEFAULT,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_MAX_COLUMNS] =
    g_param_spec_uint ("max-columns",
                       "Max Columns",
                       "Max Columns",
                       0,
                       G_MAXINT,
                       0,
                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  properties [PROP_ROW_SPACING] =
    g_param_spec_int ("row-spacing",
                      "Row Spacing",
                      "The spacing between rows",
                      0,
                      G_MAXINT,
                      ROW_SPACING_DEFAULT,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, LAST_PROP, properties);

  widget_class->get_preferred_height_for_width = dzl_column_layout_get_preferred_height_for_width;
  widget_class->get_preferred_width = dzl_column_layout_get_preferred_width;
  widget_class->get_request_mode = dzl_column_layout_get_request_mode;
  widget_class->size_allocate = dzl_column_layout_size_allocate;

  container_class->add = dzl_column_layout_add;
  container_class->forall = dzl_column_layout_forall;
  container_class->remove = dzl_column_layout_remove;
  container_class->get_child_property = dzl_column_layout_get_child_property;
  container_class->set_child_property = dzl_column_layout_set_child_property;

  child_properties [CHILD_PROP_PRIORITY] =
    g_param_spec_int ("priority",
                      "Priority",
                      "The sort priority of the child",
                      G_MININT,
                      G_MAXINT,
                      0,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_properties);
}

static void
dzl_column_layout_init (DzlColumnLayout *self)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);

  priv->children = g_array_new (FALSE, TRUE, sizeof (DzlColumnLayoutChild));

  priv->column_width = COLUMN_WIDTH_DEFAULT;
  priv->column_spacing = COLUMN_SPACING_DEFAULT;
  priv->row_spacing = ROW_SPACING_DEFAULT;
}

GtkWidget *
dzl_column_layout_new (void)
{
  return g_object_new (DZL_TYPE_COLUMN_LAYOUT, NULL);
}

guint
dzl_column_layout_get_max_columns (DzlColumnLayout *self)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_COLUMN_LAYOUT (self), 0);

  return priv->max_columns;
}

void
dzl_column_layout_set_max_columns (DzlColumnLayout *self,
                                   guint            max_columns)
{
  DzlColumnLayoutPrivate *priv = dzl_column_layout_get_instance_private (self);

  g_return_if_fail (DZL_IS_COLUMN_LAYOUT (self));

  if (priv->max_columns != max_columns)
    {
      priv->max_columns = max_columns;
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_COLUMNS]);
    }
}