/* dzl-multi-paned.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 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 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 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, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#define G_LOG_DOMAIN "dzl-multi-paned"

#include "config.h"

#include "widgets/dzl-multi-paned.h"
#include "util/dzl-util-private.h"

#define HANDLE_WIDTH  10
#define HANDLE_HEIGHT 10

#define IS_HORIZONTAL(o) (o == GTK_ORIENTATION_HORIZONTAL)

/**
 * SECTION:dzlmultipaned
 * @title: DzlMultiPaned
 * @short_description: A widget with multiple adjustable panes
 *
 * This widget is similar to #GtkPaned except that it allows adding more than
 * two children to the widget. For each additional child added to the
 * #DzlMultiPaned, an additional resize grip is added.
 */

typedef struct
{
  /*
   * The child widget in question.
   */
  GtkWidget *widget;

  /*
   * The input only window for resize grip.
   * Has a cursor associated with it.
   */
  GdkWindow *handle;

  /*
   * The position the handle has been dragged to.
   * This is used to adjust size requests.
   */
  gint position;

  /*
   * Cached size requests to avoid extra sizing calls during
   * the layout procedure.
   */
  GtkRequisition min_req;
  GtkRequisition nat_req;

  /*
   * A cached size allocation used during the size_allocate()
   * cycle. This allows us to do a first pass to allocate
   * natural sizes, and then followup when dealing with
   * expanding children.
   */
  GtkAllocation alloc;

  /*
   * If the position field has been set.
   */
  guint position_set : 1;
} DzlMultiPanedChild;

typedef struct
{
  /*
   * A GArray of DzlMultiPanedChild containing everything we need to
   * do size requests, drag operations, resize handles, and temporary
   * space needed in such operations.
   */
  GArray *children;

  /*
   * The gesture used for dragging resize handles.
   *
   * TODO: GtkPaned now uses two gestures, one for mouse and one for touch.
   *       We should do the same as it improved things quite a bit.
   */
  GtkGesturePan *gesture;

  /*
   * For GtkOrientable:orientation.
   */
  GtkOrientation orientation;

  /*
   * This is the child that is currently being dragged. Keep in mind that
   * the drag handle is immediately after the child. So the final visible
   * child has the handle input-only window hidden.
   */
  DzlMultiPanedChild *drag_begin;

  /*
   * The position (width or height) of the child when the drag began.
   * We use the pan delta offset to determine what the size should be
   * by adding (or subtracting) to this value.
   */
  gint drag_begin_position;

  /*
   * If we are dragging a handle in a fashion that would shrink the
   * previous widgets, we need to track how much to subtract from their
   * target allocations. This is set during the drag operation and used
   * in allocation_stage_drag_overflow() to adjust the neighbors.
   */
  gint drag_extra_offset;
} DzlMultiPanedPrivate;

typedef struct
{
  DzlMultiPanedChild **children;
  guint                n_children;
  GtkOrientation       orientation;
  GtkAllocation        top_alloc;
  gint                 avail_width;
  gint                 avail_height;
  gint                 handle_size;
} AllocationState;

typedef void (*AllocationStage) (DzlMultiPaned   *self,
                                 AllocationState *state);

static void allocation_stage_allocate      (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_borders       (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_cache_request (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_drag_overflow (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_expand        (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_handles       (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_minimums      (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_naturals      (DzlMultiPaned   *self,
                                            AllocationState *state);
static void allocation_stage_positions     (DzlMultiPaned   *self,
                                            AllocationState *state);

G_DEFINE_TYPE_EXTENDED (DzlMultiPaned, dzl_multi_paned, GTK_TYPE_CONTAINER, 0,
                        G_ADD_PRIVATE (DzlMultiPaned)
                        G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))

enum {
  PROP_0,
  PROP_ORIENTATION,
  N_PROPS
};

enum {
  CHILD_PROP_0,
  CHILD_PROP_INDEX,
  CHILD_PROP_POSITION,
  N_CHILD_PROPS
};

enum {
  STYLE_PROP_0,
  STYLE_PROP_HANDLE_SIZE,
  N_STYLE_PROPS
};

enum {
  RESIZE_DRAG_BEGIN,
  RESIZE_DRAG_END,
  N_SIGNALS
};

/*
 * TODO: An obvious optimization here would be to move the constant
 *       branches outside the loops.
 */

static GParamSpec *properties [N_PROPS];
static GParamSpec *child_properties [N_CHILD_PROPS];
static GParamSpec *style_properties [N_STYLE_PROPS];
static guint signals [N_SIGNALS];
static AllocationStage allocation_stages[] = {
  allocation_stage_borders,
  allocation_stage_cache_request,
  allocation_stage_minimums,
  allocation_stage_handles,
  allocation_stage_positions,
  allocation_stage_drag_overflow,
  allocation_stage_naturals,
  allocation_stage_expand,
  allocation_stage_allocate,
};

static void
dzl_multi_paned_reset_positions (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      child->position = -1;
      child->position_set = FALSE;

      gtk_container_child_notify_by_pspec (GTK_CONTAINER (self),
                                           child->widget,
                                           child_properties [CHILD_PROP_POSITION]);
    }

  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static DzlMultiPanedChild *
dzl_multi_paned_get_next_visible_child (DzlMultiPaned      *self,
                                        DzlMultiPanedChild *child)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (child != NULL);
  g_assert (priv->children != NULL);
  g_assert (priv->children->len > 0);

  i = child - ((DzlMultiPanedChild *)(gpointer)priv->children->data);

  for (++i; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *next = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (gtk_widget_get_visible (next->widget))
        return next;
    }

  return NULL;
}

static gboolean
dzl_multi_paned_is_last_visible_child (DzlMultiPaned      *self,
                                       DzlMultiPanedChild *child)
{
  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (child != NULL);

  return !dzl_multi_paned_get_next_visible_child (self, child);
}

static void
dzl_multi_paned_get_handle_rect (DzlMultiPaned      *self,
                                 DzlMultiPanedChild *child,
                                 GdkRectangle       *handle_rect)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkAllocation alloc;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (child != NULL);
  g_assert (handle_rect != NULL);

  handle_rect->x = -1;
  handle_rect->y = -1;
  handle_rect->width = 0;
  handle_rect->height = 0;

  if (!gtk_widget_get_visible (child->widget) ||
      !gtk_widget_get_realized (child->widget))
    return;

  if (dzl_multi_paned_is_last_visible_child (self, child))
    return;

  gtk_widget_get_allocation (child->widget, &alloc);

  if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      handle_rect->x = alloc.x + alloc.width - (HANDLE_WIDTH / 2);
      handle_rect->width = HANDLE_WIDTH;
      handle_rect->y = alloc.y;
      handle_rect->height = alloc.height;
    }
  else
    {
      handle_rect->x = alloc.x;
      handle_rect->width = alloc.width;
      handle_rect->y = alloc.y + alloc.height - (HANDLE_HEIGHT / 2);
      handle_rect->height = HANDLE_HEIGHT;
    }
}

static void
dzl_multi_paned_create_child_handle (DzlMultiPaned      *self,
                                     DzlMultiPanedChild *child)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GdkWindowAttr attributes = { 0 };
  GdkDisplay *display;
  GdkWindow *parent;
  const char *cursor_name;
  GdkRectangle handle_rect;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (child != NULL);
  g_assert (child->handle == NULL);

  display = gtk_widget_get_display (GTK_WIDGET (self));
  parent = gtk_widget_get_window (GTK_WIDGET (self));

  cursor_name = (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
                ? "col-resize"
                : "row-resize";

  dzl_multi_paned_get_handle_rect (self, child, &handle_rect);

  attributes.window_type = GDK_WINDOW_CHILD;
  attributes.wclass = GDK_INPUT_ONLY;
  attributes.x = handle_rect.x;
  attributes.y = -handle_rect.y;
  attributes.width = handle_rect.width;
  attributes.height = handle_rect.height;
  attributes.visual = gtk_widget_get_visual (GTK_WIDGET (self));
  attributes.event_mask = (GDK_BUTTON_PRESS_MASK |
                           GDK_BUTTON_RELEASE_MASK |
                           GDK_ENTER_NOTIFY_MASK |
                           GDK_LEAVE_NOTIFY_MASK |
                           GDK_POINTER_MOTION_MASK);
  attributes.cursor = gdk_cursor_new_from_name (display, cursor_name);

  child->handle = gdk_window_new (parent, &attributes, GDK_WA_CURSOR);
  gtk_widget_register_window (GTK_WIDGET (self), child->handle);

  g_clear_object (&attributes.cursor);
}

static gint
dzl_multi_paned_calc_handle_size (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  gint visible_children = 0;
  gint handle_size = 1;
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  gtk_widget_style_get (GTK_WIDGET (self), "handle-size", &handle_size, NULL);

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (gtk_widget_get_visible (child->widget))
        visible_children++;
    }

  return MAX (0, (visible_children - 1) * handle_size);
}

static void
dzl_multi_paned_destroy_child_handle (DzlMultiPaned      *self,
                                      DzlMultiPanedChild *child)
{
  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (child != NULL);

  if (child->handle != NULL)
    {
      gtk_widget_unregister_window (GTK_WIDGET (self), child->handle);
      gdk_window_destroy (child->handle);
      child->handle = NULL;
    }
}

static void
dzl_multi_paned_update_child_handles (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkWidget *widget = GTK_WIDGET (self);

  if (gtk_widget_get_realized (widget))
    {
      GdkCursor *cursor;
      guint i;

      if (gtk_widget_is_sensitive (widget))
        cursor = gdk_cursor_new_from_name (gtk_widget_get_display (widget),
                                           priv->orientation == GTK_ORIENTATION_HORIZONTAL
                                           ? "col-resize"
                                           : "row-resize");
      else
        cursor = NULL;

      for (i = 0; i < priv->children->len; i++)
        {
          DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

          gdk_window_set_cursor (child->handle, cursor);
        }

      if (cursor)
        g_object_unref (cursor);
    }
}

static DzlMultiPanedChild *
dzl_multi_paned_get_child (DzlMultiPaned *self,
                           GtkWidget     *widget)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));

  for (guint i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

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

  g_assert_not_reached ();

  return NULL;
}

static gint
dzl_multi_paned_get_child_index (DzlMultiPaned *self,
                                 GtkWidget     *widget)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));

  for (guint i = 0; i < priv->children->len; i++)
    {
      const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (ele->widget == widget)
        return i;
    }

  return -1;
}

static void
dzl_multi_paned_set_child_index (DzlMultiPaned *self,
                                 GtkWidget     *widget,
                                 gint           index)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));
  g_assert (index >= -1);
  g_assert (priv->children->len > 0);

  if (index < 0)
    index = priv->children->len - 1;

  index = CLAMP (index, 0, priv->children->len - 1);

  for (guint i = 0; i < priv->children->len; i++)
    {
      const DzlMultiPanedChild *ele = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (ele->widget == widget)
        {
          DzlMultiPanedChild copy = { 0 };

          copy.widget = ele->widget;
          copy.handle = ele->handle;
          copy.position = -1;
          copy.position_set = FALSE;

          g_array_remove_index (priv->children, i);
          g_array_insert_val (priv->children, index, copy);
          gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget,
                                               child_properties [CHILD_PROP_INDEX]);
          gtk_widget_queue_resize (GTK_WIDGET (self));

          break;
        }
    }
}

static gint
dzl_multi_paned_get_child_position (DzlMultiPaned *self,
                                    GtkWidget     *widget)
{
  DzlMultiPanedChild *child;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));

  child = dzl_multi_paned_get_child (self, widget);

  return child->position;
}

static void
dzl_multi_paned_set_child_position (DzlMultiPaned *self,
                                    GtkWidget     *widget,
                                    gint           position)
{
  DzlMultiPanedChild *child;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));
  g_assert (position >= -1);

  child = dzl_multi_paned_get_child (self, widget);

  if (child->position != position)
    {
      child->position = position;
      child->position_set = (position != -1);
      gtk_container_child_notify_by_pspec (GTK_CONTAINER (self), widget,
                                           child_properties [CHILD_PROP_POSITION]);
      gtk_widget_queue_resize (GTK_WIDGET (self));
    }
}

static void
dzl_multi_paned_add (GtkContainer *container,
                     GtkWidget    *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)container;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  DzlMultiPanedChild child = { 0 };

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));

  child.widget = g_object_ref_sink (widget);
  child.position = -1;

  if (gtk_widget_get_realized (GTK_WIDGET (self)))
    dzl_multi_paned_create_child_handle (self, &child);

  gtk_widget_set_parent (widget, GTK_WIDGET (self));

  g_array_append_val (priv->children, child);

  dzl_multi_paned_reset_positions (self);

  gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED);

  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static void
dzl_multi_paned_remove (GtkContainer *container,
                        GtkWidget    *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)container;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (widget));

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (child->widget == widget)
        {
          dzl_multi_paned_destroy_child_handle (self, child);

          g_array_remove_index (priv->children, i);
          child = NULL;

          gtk_widget_unparent (widget);
          g_object_unref (widget);

          break;
        }
    }

  dzl_multi_paned_reset_positions (self);

  gtk_gesture_set_state (GTK_GESTURE (priv->gesture), GTK_EVENT_SEQUENCE_DENIED);

  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static void
dzl_multi_paned_forall (GtkContainer *container,
                        gboolean      include_internals,
                        GtkCallback   callback,
                        gpointer      user_data)
{
  DzlMultiPaned *self = (DzlMultiPaned *)container;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  gint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (callback != NULL);

  for (i = priv->children->len; i > 0; i--)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i - 1);

      callback (child->widget, user_data);
    }
}

static GtkSizeRequestMode
dzl_multi_paned_get_request_mode (GtkWidget *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_assert (DZL_IS_MULTI_PANED (self));

  return (priv->orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT
                                                           : GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
dzl_multi_paned_get_preferred_height (GtkWidget *widget,
                                      gint      *min_height,
                                      gint      *nat_height)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkBorder borders;
  guint i;
  gint real_min_height = 0;
  gint real_nat_height = 0;

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

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
      gint child_min_height = 0;
      gint child_nat_height = 0;

      if (gtk_widget_get_visible (child->widget))
        {
          gtk_widget_get_preferred_height (child->widget, &child_min_height, &child_nat_height);

          if (priv->orientation == GTK_ORIENTATION_VERTICAL)
            {
              real_min_height += child_min_height;
              real_nat_height += child_nat_height;
            }
          else
            {
              real_min_height = MAX (real_min_height, child_min_height);
              real_nat_height = MAX (real_nat_height, child_nat_height);
            }
        }
    }

  if (priv->orientation == GTK_ORIENTATION_VERTICAL)
    {
      gint handle_size = dzl_multi_paned_calc_handle_size (self);

      real_min_height += handle_size;
      real_nat_height += handle_size;
    }

  *min_height = real_min_height;
  *nat_height = real_nat_height;

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  *min_height += borders.top + borders.bottom;
  *nat_height += borders.top + borders.bottom;
}

static void
dzl_multi_paned_get_child_preferred_height_for_width (DzlMultiPaned      *self,
                                                      DzlMultiPanedChild *children,
                                                      gint                n_children,
                                                      gint                width,
                                                      gint               *min_height,
                                                      gint               *nat_height)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  DzlMultiPanedChild *child = children;
  gint child_min_height = 0;
  gint child_nat_height = 0;
  gint neighbor_min_height = 0;
  gint neighbor_nat_height = 0;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (n_children == 0 || children != NULL);
  g_assert (min_height != NULL);
  g_assert (nat_height != NULL);

  *min_height = 0;
  *nat_height = 0;

  if (n_children == 0)
    return;

  if (gtk_widget_get_visible (child->widget))
    gtk_widget_get_preferred_height_for_width (child->widget,
                                               width,
                                               &child_min_height,
                                               &child_nat_height);

  dzl_multi_paned_get_child_preferred_height_for_width (self,
                                                        children + 1,
                                                        n_children - 1,
                                                        width,
                                                        &neighbor_min_height,
                                                        &neighbor_nat_height);

  if (priv->orientation == GTK_ORIENTATION_VERTICAL)
    {
      *min_height = child_min_height + neighbor_min_height;
      *nat_height = child_nat_height + neighbor_nat_height;
    }
  else
    {
      *min_height = MAX (child_min_height, neighbor_min_height);
      *nat_height = MAX (child_nat_height, neighbor_nat_height);
    }
}

static void
dzl_multi_paned_get_preferred_height_for_width (GtkWidget *widget,
                                                gint       width,
                                                gint      *min_height,
                                                gint      *nat_height)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkBorder borders;

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

  *min_height = 0;
  *nat_height = 0;

  dzl_multi_paned_get_child_preferred_height_for_width (self,
                                                        (DzlMultiPanedChild *)(gpointer)priv->children->data,
                                                        priv->children->len,
                                                        width,
                                                        min_height,
                                                        nat_height);

  if (priv->orientation == GTK_ORIENTATION_VERTICAL)
    {
      gint handle_size = dzl_multi_paned_calc_handle_size (self);

      *min_height += handle_size;
      *nat_height += handle_size;
    }

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  *min_height += borders.top + borders.bottom;
  *nat_height += borders.top + borders.bottom;
}

static void
dzl_multi_paned_get_preferred_width (GtkWidget *widget,
                                     gint      *min_width,
                                     gint      *nat_width)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkBorder borders;
  guint i;
  gint real_min_width = 0;
  gint real_nat_width = 0;

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

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
      gint child_min_width = 0;
      gint child_nat_width = 0;

      if (gtk_widget_get_visible (child->widget))
        {
          gtk_widget_get_preferred_width (child->widget, &child_min_width, &child_nat_width);

          if (priv->orientation == GTK_ORIENTATION_VERTICAL)
            {
              real_min_width = MAX (real_min_width, child_min_width);
              real_nat_width = MAX (real_nat_width, child_nat_width);
            }
          else
            {
              real_min_width += child_min_width;
              real_nat_width += child_nat_width;
            }
        }
    }

  if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      gint handle_size = dzl_multi_paned_calc_handle_size (self);

      real_min_width += handle_size;
      real_nat_width += handle_size;
    }

  *min_width = real_min_width;
  *nat_width = real_nat_width;

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  *min_width += borders.left + borders.right;
  *nat_width += borders.left + borders.right;
}

static void
dzl_multi_paned_get_child_preferred_width_for_height (DzlMultiPaned      *self,
                                                      DzlMultiPanedChild *children,
                                                      gint                n_children,
                                                      gint                height,
                                                      gint               *min_width,
                                                      gint               *nat_width)
{
  DzlMultiPanedChild *child = children;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  gint child_min_width = 0;
  gint child_nat_width = 0;
  gint neighbor_min_width = 0;
  gint neighbor_nat_width = 0;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (n_children == 0 || children != NULL);
  g_assert (min_width != NULL);
  g_assert (nat_width != NULL);

  *min_width = 0;
  *nat_width = 0;

  if (n_children == 0)
    return;

  if (gtk_widget_get_visible (child->widget))
    gtk_widget_get_preferred_width_for_height (child->widget,
                                               height,
                                               &child_min_width,
                                               &child_nat_width);

  dzl_multi_paned_get_child_preferred_width_for_height (self,
                                                        children + 1,
                                                        n_children - 1,
                                                        height,
                                                        &neighbor_min_width,
                                                        &neighbor_nat_width);

  if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      *min_width = child_min_width + neighbor_min_width;
      *nat_width = child_nat_width + neighbor_nat_width;
    }
  else
    {
      *min_width = MAX (child_min_width, neighbor_min_width);
      *nat_width = MAX (child_nat_width, neighbor_nat_width);
    }
}

static void
dzl_multi_paned_get_preferred_width_for_height (GtkWidget *widget,
                                                gint       height,
                                                gint      *min_width,
                                                gint      *nat_width)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkBorder borders;

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

  dzl_multi_paned_get_child_preferred_width_for_height (self,
                                                        (DzlMultiPanedChild *)(gpointer)priv->children->data,
                                                        priv->children->len,
                                                        height,
                                                        min_width,
                                                        nat_width);

  if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      gint handle_size = dzl_multi_paned_calc_handle_size (self);

      *min_width += handle_size;
      *nat_width += handle_size;
    }

  style_context = gtk_widget_get_style_context (widget);
  dzl_gtk_style_context_get_borders (style_context, &borders);

  *min_width += borders.left + borders.right;
  *nat_width += borders.left + borders.right;
}

static void
allocation_stage_handles (DzlMultiPaned   *self,
                          AllocationState *state)
{
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  /*
   * Push each child allocation forward by the sum handle widths up to
   * their position in the paned.
   */

  for (i = 1; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      if (IS_HORIZONTAL (state->orientation))
        child->alloc.x += (i * state->handle_size);
      else
        child->alloc.y += (i * state->handle_size);
    }

  if (IS_HORIZONTAL (state->orientation))
    state->avail_width -= (state->n_children - 1) * state->handle_size;
  else
    state->avail_height -= (state->n_children - 1) * state->handle_size;
}

static void
allocation_stage_minimums (DzlMultiPaned   *self,
                           AllocationState *state)
{
  gint next_x;
  gint next_y;
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  next_x = state->top_alloc.x;
  next_y = state->top_alloc.y;

  for (i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      if (IS_HORIZONTAL (state->orientation))
        {
          child->alloc.x = next_x;
          child->alloc.y = state->top_alloc.y;
          child->alloc.width = child->min_req.width;
          child->alloc.height = state->top_alloc.height;

          next_x = child->alloc.x + child->alloc.width;

          state->avail_width -= child->alloc.width;
        }
      else
        {
          child->alloc.x = state->top_alloc.x;
          child->alloc.y = next_y;
          child->alloc.width = state->top_alloc.width;
          child->alloc.height = child->min_req.height;

          next_y = child->alloc.y + child->alloc.height;

          state->avail_height -= child->alloc.height;
        }
    }
}

static void
allocation_stage_naturals (DzlMultiPaned   *self,
                           AllocationState *state)
{
  gint x_adjust = 0;
  gint y_adjust = 0;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  for (guint i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      child->alloc.x += x_adjust;
      child->alloc.y += y_adjust;

      if (!child->position_set)
        {
          if (IS_HORIZONTAL (state->orientation))
            {
              if (child->nat_req.width > child->alloc.width)
                {
                  gint adjust = MIN (state->avail_width, child->nat_req.width - child->alloc.width);

                  child->alloc.width += adjust;
                  state->avail_width -= adjust;
                  x_adjust += adjust;
                }
            }
          else
            {
              if (child->nat_req.height > child->alloc.height)
                {
                  gint adjust = MIN (state->avail_height, child->nat_req.height - child->alloc.height);

                  child->alloc.height += adjust;
                  state->avail_height -= adjust;
                  y_adjust += adjust;
                }
            }
        }
    }
}

static void
allocation_stage_borders (DzlMultiPaned   *self,
                          AllocationState *state)
{
  GtkStyleContext *style_context;
  GtkBorder borders;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  /*
   * This subtracts the border+padding from the allocation area so the
   * children are guaranteed to fall within that area.
   */

  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
  dzl_gtk_style_context_get_borders (style_context, &borders);

  state->top_alloc.x += borders.left;
  state->top_alloc.y += borders.right;
  state->top_alloc.width -= (borders.left + borders.right);
  state->top_alloc.height -= (borders.top + borders.bottom);

  if (state->top_alloc.width < 0)
    state->top_alloc.width = 0;

  if (state->top_alloc.height < 0)
    state->top_alloc.height = 0;

  state->avail_width = state->top_alloc.width;
  state->avail_height = state->top_alloc.height;
}

static void
allocation_stage_cache_request (DzlMultiPaned   *self,
                                AllocationState *state)
{
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  for (i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      if (IS_HORIZONTAL (state->orientation))
        gtk_widget_get_preferred_width_for_height (child->widget,
                                                   state->avail_height,
                                                   &child->min_req.width,
                                                   &child->nat_req.width);
      else
        gtk_widget_get_preferred_height_for_width (child->widget,
                                                   state->avail_width,
                                                   &child->min_req.height,
                                                   &child->nat_req.height);
    }
}

static void
allocation_stage_positions (DzlMultiPaned   *self,
                            AllocationState *state)
{
  gint x_adjust = 0;
  gint y_adjust = 0;
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  /*
   * Child may have a position set, which happens when dragging the input
   * window (handle) to resize the child. If so, we want to try to allocate
   * extra space above the minimum size.
   */

  for (i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      child->alloc.x += x_adjust;
      child->alloc.y += y_adjust;

      if (child->position_set)
        {
          if (IS_HORIZONTAL (state->orientation))
            {
              if (child->position > child->alloc.width)
                {
                  gint adjust = MIN (state->avail_width, child->position - child->alloc.width);

                  child->alloc.width += adjust;
                  state->avail_width -= adjust;
                  x_adjust += adjust;
                }
            }
          else
            {
              if (child->position > child->alloc.height)
                {
                  gint adjust = MIN (state->avail_height, child->position - child->alloc.height);

                  child->alloc.height += adjust;
                  state->avail_height -= adjust;
                  y_adjust += adjust;
                }
            }
        }
    }
}

static void
allocation_stage_drag_overflow (DzlMultiPaned   *self,
                                AllocationState *state)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint drag_index;
  gint j;
  gint drag_overflow;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  if (priv->drag_begin == NULL)
    return;

  drag_overflow = ABS (priv->drag_extra_offset);

  for (drag_index = 0; drag_index < state->n_children; drag_index++)
    if (state->children [drag_index] == priv->drag_begin)
      break;

  if (drag_index == 0 ||
      drag_index >= state->n_children ||
      state->children [drag_index] != priv->drag_begin)
    return;

  /*
   * If the user is dragging and we have run out of room in the drag
   * child, then we need to start stealing space from the previous
   * items.
   *
   * This works our way back to the beginning from the drag child
   * stealing available space and giving it to the child *AFTER* the
   * drag item. This is because the drag handle is after the drag
   * child, so logically to the user, its drag_index+1.
   */

  for (j = (int)drag_index; j >= 0 && drag_overflow > 0; j--)
    {
      DzlMultiPanedChild *child = state->children [j];
      guint k;
      gint adjust = 0;

      if (IS_HORIZONTAL (state->orientation))
        {
          if (child->alloc.width > child->min_req.width)
            {
              if (drag_overflow > (child->alloc.width - child->min_req.width))
                adjust = child->alloc.width - child->min_req.width;
              else
                adjust = drag_overflow;
              drag_overflow -= adjust;
              child->alloc.width -= adjust;
              state->children [drag_index + 1]->alloc.width += adjust;
            }
        }
      else
        {
          if (child->alloc.height > child->min_req.height)
            {
              if (drag_overflow > (child->alloc.height - child->min_req.height))
                adjust = child->alloc.height - child->min_req.height;
              else
                adjust = drag_overflow;
              drag_overflow -= adjust;
              child->alloc.height -= adjust;
              state->children [drag_index + 1]->alloc.height += adjust;
            }
        }

      /*
       * Now walk back forward and adjust x/y offsets for all of the
       * children that will have just shifted.
       */

      for (k = j + 1; k <= drag_index + 1; k++)
        {
          DzlMultiPanedChild *neighbor = state->children [k];

          if (IS_HORIZONTAL (state->orientation))
            neighbor->alloc.x -= adjust;
          else
            neighbor->alloc.y -= adjust;
        }
    }
}

static gint
sort_children_horizontal (const DzlMultiPanedChild * const *first,
                          const DzlMultiPanedChild * const *second)
{
  return (*first)->alloc.width - (*second)->alloc.width;
}

static gint
sort_children_vertical (const DzlMultiPanedChild * const *first,
                        const DzlMultiPanedChild * const *second)
{
  return (*first)->alloc.height - (*second)->alloc.height;
}

static void
allocation_stage_expand (DzlMultiPaned   *self,
                         AllocationState *state)
{
  g_autoptr(GPtrArray) expanding = NULL;
  gint x_adjust = 0;
  gint y_adjust = 0;
  gint adjust;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  if (state->n_children == 1)
    {
      DzlMultiPanedChild *child = state->children [0];

      /*
       * Special case for single child, just expand to the
       * available space. Ideally we would have much shorter
       * allocation stages in this case.
       */

      if (gtk_widget_compute_expand (child->widget, state->orientation))
        {
          if (IS_HORIZONTAL (state->orientation))
            child->alloc.width = state->top_alloc.width;
          else
            child->alloc.height = state->top_alloc.height;
        }

      return;
    }

  /*
   * First, we need to collect all of the children who are expanding
   * in the direction matching our orientation. After that, we will
   * sort them by their allocation size so that we can divy out extra
   * allocation starting from the smallest. This will give us an effect
   * of homogeneous size when all children are set to expand and enough
   * space is available.
   */

  expanding = g_ptr_array_new ();

  for (guint i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      if (!child->position_set)
        {
          if (gtk_widget_compute_expand (child->widget, state->orientation))
            g_ptr_array_add (expanding, child);
        }
    }

  if (expanding->len == 0)
    goto fill_last;

  /* Now sort, smallest first, based on size in given orientation. */
  if (IS_HORIZONTAL (state->orientation))
    g_ptr_array_sort (expanding, (GCompareFunc) sort_children_horizontal);
  else
    g_ptr_array_sort (expanding, (GCompareFunc) sort_children_vertical);

  /*
   * While we have additional space available to hand out, allocate
   * as much space as it takes to get to the next item in the array.
   * If we run out of additional space, that is okay (as long as we
   * dont hand out more than we have).
   */

  g_assert (expanding->len > 0);

  for (guint i = 0; i < expanding->len - 1; i++)
    {
      DzlMultiPanedChild *child = g_ptr_array_index (expanding, i);
      DzlMultiPanedChild *next = g_ptr_array_index (expanding, i + 1);

      if (IS_HORIZONTAL (state->orientation))
        {
          guint j;

          g_assert (next->alloc.width >= child->alloc.width);

          adjust = next->alloc.width - child->alloc.width;
          if (adjust > state->avail_width)
            adjust = state->avail_width;

          child->alloc.width += adjust;
          state->avail_width -= adjust;

          /* Adjust X of children following */
          for (j = 0; j < state->n_children; j++)
            if (state->children[j] == child)
              break;
          for (++j; j < state->n_children; j++)
            state->children[j]->alloc.x += adjust;

          g_assert (state->avail_width >= 0);

          if (state->avail_width == 0)
            break;
        }
      else
        {
          guint j;

          g_assert (next->alloc.height >= child->alloc.height);

          adjust = next->alloc.height - child->alloc.height;
          if (adjust > state->avail_height)
            adjust = state->avail_height;

          child->alloc.height += adjust;
          state->avail_height -= adjust;

          /* Adjust Y of children following */
          for (j = 0; j < state->n_children; j++)
            if (state->children[j] == child)
              break;
          for (++j; j < state->n_children; j++)
            state->children[j]->alloc.y += adjust;

          g_assert (state->avail_height >= 0);

          if (state->avail_height == 0)
            break;
        }
    }

  /*
   * If we still have more space we can divy out, then do the normal
   * expansion case now that we are starting with evenly sized
   * children.
   */

  if (IS_HORIZONTAL (state->orientation))
    adjust = state->avail_width / expanding->len;
  else
    adjust = state->avail_height / expanding->len;

  for (guint i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      child->alloc.x += x_adjust;
      child->alloc.y += y_adjust;

      if (!child->position_set)
        {
          if (gtk_widget_compute_expand (child->widget, state->orientation))
            {
              if (IS_HORIZONTAL (state->orientation))
                {
                  child->alloc.width += adjust;
                  state->avail_width -= adjust;
                  x_adjust += adjust;
                }
              else
                {
                  child->alloc.height += adjust;
                  state->avail_height -= adjust;
                  y_adjust += adjust;
                }
            }
        }
    }

fill_last:

  if (IS_HORIZONTAL (state->orientation))
    {
      if (state->avail_width > 0)
        {
          state->children [state->n_children - 1]->alloc.width += state->avail_width;
          state->avail_width = 0;
        }
    }
  else
    {
      if (state->avail_height > 0)
        {
          state->children [state->n_children - 1]->alloc.height += state->avail_height;
          state->avail_height = 0;
        }
    }
}

static void
allocation_stage_allocate (DzlMultiPaned   *self,
                           AllocationState *state)
{
  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (state != NULL);
  g_assert (state->children != NULL);
  g_assert (state->n_children > 0);

  for (guint i = 0; i < state->n_children; i++)
    {
      DzlMultiPanedChild *child = state->children [i];

      gtk_widget_size_allocate (child->widget, &child->alloc);

      if (child->handle != NULL)
        {
          if (state->n_children != (i + 1))
            {
              if (IS_HORIZONTAL (state->orientation))
                {
                  gdk_window_move_resize (child->handle,
                                          child->alloc.x + child->alloc.width - (HANDLE_WIDTH / 2),
                                          child->alloc.y,
                                          HANDLE_WIDTH,
                                          child->alloc.height);
                }
              else
                {
                  gdk_window_move_resize (child->handle,
                                          child->alloc.x,
                                          child->alloc.y + child->alloc.height - (HANDLE_HEIGHT / 2),
                                          child->alloc.width,
                                          HANDLE_HEIGHT);
                }

              gdk_window_show (child->handle);
            }
          else
            {
              gdk_window_hide (child->handle);
            }
        }
    }
}

static void
dzl_multi_paned_size_allocate (GtkWidget     *widget,
                               GtkAllocation *allocation)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  AllocationState state = { 0 };
  g_autoptr(GPtrArray) children = NULL;

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

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->size_allocate (widget, allocation);

  if (priv->children->len == 0)
    return;

  children = g_ptr_array_new ();

  for (guint i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      child->alloc.x = 0;
      child->alloc.y = 0;
      child->alloc.width = 0;
      child->alloc.height = 0;

      if (child->widget != NULL &&
          gtk_widget_get_child_visible (child->widget) &&
          gtk_widget_get_visible (child->widget))
        g_ptr_array_add (children, child);
      else if (child->handle)
        gdk_window_hide (child->handle);
    }

  state.children = (DzlMultiPanedChild **)children->pdata;
  state.n_children = children->len;

  if (state.n_children == 0)
    return;

  gtk_widget_style_get (GTK_WIDGET (self),
                        "handle-size", &state.handle_size,
                        NULL);

  state.orientation = priv->orientation;
  state.top_alloc = *allocation;
  state.avail_width = allocation->width;
  state.avail_height = allocation->height;

  for (guint i = 0; i < G_N_ELEMENTS (allocation_stages); i++)
    allocation_stages [i] (self, &state);

  gtk_widget_queue_draw (GTK_WIDGET (self));
}

static void
dzl_multi_paned_realize (GtkWidget *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->realize (widget);

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      dzl_multi_paned_create_child_handle (self, child);
    }
}

static void
dzl_multi_paned_unrealize (GtkWidget *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      dzl_multi_paned_destroy_child_handle (self, child);
    }

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unrealize (widget);
}

static void
dzl_multi_paned_map (GtkWidget *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->map (widget);

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      gdk_window_show (child->handle);
    }
}

static void
dzl_multi_paned_unmap (GtkWidget *widget)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      gdk_window_hide (child->handle);
    }

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->unmap (widget);
}

static gboolean
dzl_multi_paned_draw (GtkWidget *widget,
                      cairo_t   *cr)
{
  DzlMultiPaned *self = (DzlMultiPaned *)widget;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkStyleContext *style_context;
  GtkAllocation alloc;
  GtkBorder margin;
  GtkBorder borders;
  GtkStateFlags state;
  gint handle_size = 1;
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (cr != NULL);

  gtk_widget_get_allocation (widget, &alloc);

  alloc.x = 0;
  alloc.y = 0;

  style_context = gtk_widget_get_style_context (widget);
  state = gtk_style_context_get_state (style_context);

  dzl_gtk_style_context_get_borders (style_context, &borders);

  gtk_style_context_get_margin (style_context, state, &margin);
  dzl_gtk_allocation_subtract_border (&alloc, &margin);

  gtk_render_background (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);

  gtk_widget_style_get (widget, "handle-size", &handle_size, NULL);

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (!gtk_widget_get_realized (child->widget) ||
          !gtk_widget_get_visible (child->widget))
        continue;

      gtk_container_propagate_draw (GTK_CONTAINER (self), child->widget, cr);
    }

  if (priv->children->len > 0)
    {
      gtk_style_context_save (style_context);
      gtk_style_context_add_class (style_context, "handle");

      for (i = 0; i < priv->children->len; i++)
        {
          DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
          GtkAllocation child_alloc;

          if (!gtk_widget_get_visible (child->widget) ||
              !gtk_widget_get_child_visible (child->widget))
            continue;

          if (dzl_multi_paned_is_last_visible_child (self, child))
            break;

          gtk_widget_get_allocation (child->widget, &child_alloc);
          gtk_widget_translate_coordinates (child->widget, widget, 0, 0, &child_alloc.x, &child_alloc.y);

          if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
            gtk_render_handle (style_context,
                               cr,
                               child_alloc.x + child_alloc.width,
                               borders.top,
                               handle_size,
                               child_alloc.height);
          else
            gtk_render_handle (style_context,
                               cr,
                               borders.left,
                               child_alloc.y + child_alloc.height,
                               child_alloc.width,
                               handle_size);

        }

      gtk_style_context_restore (style_context);
    }

  gtk_render_frame (style_context, cr, alloc.x, alloc.y, alloc.width, alloc.height);

  return FALSE;
}

static void
dzl_multi_paned_pan_gesture_drag_begin (DzlMultiPaned *self,
                                        gdouble        x,
                                        gdouble        y,
                                        GtkGesturePan *gesture)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GdkEventSequence *sequence;
  const GdkEvent *event;
  guint i;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_GESTURE_PAN (gesture));
  g_assert (gesture == priv->gesture);

  sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
  event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);

  priv->drag_begin = NULL;
  priv->drag_begin_position = 0;
  priv->drag_extra_offset = 0;

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (child->handle == event->any.window)
        {
          priv->drag_begin = child;
          break;
        }
    }

  if (priv->drag_begin == NULL)
    {
      gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
      return;
    }

  for (i = 0; i < priv->children->len; i++)
    {
      DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

      if (child->handle == event->any.window)
        break;

      /*
       * We want to make any child before the drag child "sticky" so that it
       * will no longer have expand adjustments while we perform the drag
       * operation.
       */
      if (gtk_widget_get_child_visible (child->widget) &&
          gtk_widget_get_visible (child->widget))
        {
          child->position_set = TRUE;
          child->position = IS_HORIZONTAL (priv->orientation)
            ? child->alloc.width
            : child->alloc.height;
        }
    }

  if (IS_HORIZONTAL (priv->orientation))
    priv->drag_begin_position = priv->drag_begin->alloc.width;
  else
    priv->drag_begin_position = priv->drag_begin->alloc.height;

  gtk_gesture_pan_set_orientation (gesture, priv->orientation);
  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);

  g_signal_emit (self, signals [RESIZE_DRAG_BEGIN], 0, priv->drag_begin->widget);
}

static void
dzl_multi_paned_pan_gesture_drag_end (DzlMultiPaned *self,
                                      gdouble        x,
                                      gdouble        y,
                                      GtkGesturePan *gesture)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GdkEventSequence *sequence;
  GtkEventSequenceState state;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_GESTURE_PAN (gesture));
  g_assert (gesture == priv->gesture);

  sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
  state = gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence);

  if (state != GTK_EVENT_SEQUENCE_CLAIMED)
    goto cleanup;

  g_assert (priv->drag_begin != NULL);

  g_signal_emit (self, signals [RESIZE_DRAG_END], 0, priv->drag_begin->widget);

cleanup:
  priv->drag_begin = NULL;
  priv->drag_begin_position = 0;
  priv->drag_extra_offset = 0;
}

static void
dzl_multi_paned_pan_gesture_pan (DzlMultiPaned   *self,
                                 GtkPanDirection  direction,
                                 gdouble          offset,
                                 GtkGesturePan   *gesture)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkAllocation alloc;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_GESTURE_PAN (gesture));
  g_assert (gesture == priv->gesture);
  g_assert (priv->drag_begin != NULL);

  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);

  if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      if (direction == GTK_PAN_DIRECTION_LEFT)
        offset = -offset;
    }
  else
    {
      g_assert (priv->orientation == GTK_ORIENTATION_VERTICAL);

      if (direction == GTK_PAN_DIRECTION_UP)
        offset = -offset;
    }

  if ((priv->drag_begin_position + offset) < 0)
    priv->drag_extra_offset = (priv->drag_begin_position + offset);
  else
    priv->drag_extra_offset = 0;

  priv->drag_begin->position = MAX (0, priv->drag_begin_position + offset);
  priv->drag_begin->position_set = TRUE;

  gtk_widget_queue_allocate (GTK_WIDGET (self));
}

static void
dzl_multi_paned_create_pan_gesture (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkGesture *gesture;

  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (priv->gesture == NULL);

  gesture = gtk_gesture_pan_new (GTK_WIDGET (self), GTK_ORIENTATION_HORIZONTAL);
  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE);
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE);

  g_signal_connect_object (gesture,
                           "drag-begin",
                           G_CALLBACK (dzl_multi_paned_pan_gesture_drag_begin),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (gesture,
                           "drag-end",
                           G_CALLBACK (dzl_multi_paned_pan_gesture_drag_end),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (gesture,
                           "pan",
                           G_CALLBACK (dzl_multi_paned_pan_gesture_pan),
                           self,
                           G_CONNECT_SWAPPED);

  priv->gesture = GTK_GESTURE_PAN (gesture);
}

static void
dzl_multi_paned_resize_drag_begin (DzlMultiPaned *self,
                                   GtkWidget     *child)
{
  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (child));

}

static void
dzl_multi_paned_resize_drag_end (DzlMultiPaned *self,
                                 GtkWidget     *child)
{
  g_assert (DZL_IS_MULTI_PANED (self));
  g_assert (GTK_IS_WIDGET (child));

}

static void
dzl_multi_paned_get_child_property (GtkContainer *container,
                                    GtkWidget    *widget,
                                    guint         prop_id,
                                    GValue       *value,
                                    GParamSpec   *pspec)
{
  DzlMultiPaned *self = DZL_MULTI_PANED (container);

  switch (prop_id)
    {
    case CHILD_PROP_INDEX:
      g_value_set_int (value, dzl_multi_paned_get_child_index (self, widget));
      break;

    case CHILD_PROP_POSITION:
      g_value_set_int (value, dzl_multi_paned_get_child_position (self, widget));
      break;

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

static void
dzl_multi_paned_set_child_property (GtkContainer *container,
                                    GtkWidget    *widget,
                                    guint         prop_id,
                                    const GValue *value,
                                    GParamSpec   *pspec)
{
  DzlMultiPaned *self = DZL_MULTI_PANED (container);

  switch (prop_id)
    {
    case CHILD_PROP_INDEX:
      dzl_multi_paned_set_child_index (self, widget, g_value_get_int (value));
      break;

    case CHILD_PROP_POSITION:
      dzl_multi_paned_set_child_position (self, widget, g_value_get_int (value));
      break;

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

static void
dzl_multi_paned_finalize (GObject *object)
{
  DzlMultiPaned *self = (DzlMultiPaned *)object;
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_assert (priv->children->len == 0);

  g_clear_pointer (&priv->children, g_array_unref);
  g_clear_object (&priv->gesture);

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

static void
dzl_multi_paned_get_property (GObject    *object,
                              guint       prop_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  DzlMultiPaned *self = DZL_MULTI_PANED (object);
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ORIENTATION:
      g_value_set_enum (value, priv->orientation);
      break;

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

static void
dzl_multi_paned_set_property (GObject      *object,
                              guint         prop_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  DzlMultiPaned *self = DZL_MULTI_PANED (object);
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  switch (prop_id)
    {
    case PROP_ORIENTATION:
      priv->orientation = g_value_get_enum (value);
      for (guint i = 0; i < priv->children->len; i++)
        {
          DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);
          child->position_set = FALSE;
        }
      dzl_multi_paned_update_child_handles (self);
      gtk_widget_queue_resize (GTK_WIDGET (self));
      break;

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

static void
dzl_multi_paned_state_flags_changed (GtkWidget     *widget,
                                     GtkStateFlags  previous_state)
{
  dzl_multi_paned_update_child_handles (DZL_MULTI_PANED (widget));

  GTK_WIDGET_CLASS (dzl_multi_paned_parent_class)->state_flags_changed (widget, previous_state);
}

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

  object_class->get_property = dzl_multi_paned_get_property;
  object_class->set_property = dzl_multi_paned_set_property;
  object_class->finalize = dzl_multi_paned_finalize;

  widget_class->get_request_mode = dzl_multi_paned_get_request_mode;
  widget_class->get_preferred_width = dzl_multi_paned_get_preferred_width;
  widget_class->get_preferred_height = dzl_multi_paned_get_preferred_height;
  widget_class->get_preferred_width_for_height = dzl_multi_paned_get_preferred_width_for_height;
  widget_class->get_preferred_height_for_width = dzl_multi_paned_get_preferred_height_for_width;
  widget_class->size_allocate = dzl_multi_paned_size_allocate;
  widget_class->realize = dzl_multi_paned_realize;
  widget_class->unrealize = dzl_multi_paned_unrealize;
  widget_class->map = dzl_multi_paned_map;
  widget_class->unmap = dzl_multi_paned_unmap;
  widget_class->draw = dzl_multi_paned_draw;
  widget_class->state_flags_changed = dzl_multi_paned_state_flags_changed;

  container_class->add = dzl_multi_paned_add;
  container_class->remove = dzl_multi_paned_remove;
  container_class->get_child_property = dzl_multi_paned_get_child_property;
  container_class->set_child_property = dzl_multi_paned_set_child_property;
  container_class->forall = dzl_multi_paned_forall;

  klass->resize_drag_begin = dzl_multi_paned_resize_drag_begin;
  klass->resize_drag_end = dzl_multi_paned_resize_drag_end;

  gtk_widget_class_set_css_name (widget_class, "dzlmultipaned");

  properties [PROP_ORIENTATION] =
    g_param_spec_enum ("orientation",
                       "Orientation",
                       "Orientation",
                       GTK_TYPE_ORIENTATION,
                       GTK_ORIENTATION_VERTICAL,
                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  child_properties [CHILD_PROP_INDEX] =
    g_param_spec_int ("index",
                      "Index",
                      "The index of the child",
                      -1,
                      G_MAXINT,
                      -1,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  child_properties [CHILD_PROP_POSITION] =
    g_param_spec_int ("position",
                      "Position",
                      "Position",
                      -1,
                      G_MAXINT,
                      0,
                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  gtk_container_class_install_child_properties (container_class, N_CHILD_PROPS, child_properties);

  style_properties [STYLE_PROP_HANDLE_SIZE] =
    g_param_spec_int ("handle-size",
                      "Handle Size",
                      "Width of the resize handle",
                      0,
                      G_MAXINT,
                      1,
                      (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
  gtk_widget_class_install_style_property (widget_class, style_properties [STYLE_PROP_HANDLE_SIZE]);

  signals [RESIZE_DRAG_BEGIN] =
    g_signal_new ("resize-drag-begin",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (DzlMultiPanedClass, resize_drag_begin),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1, GTK_TYPE_WIDGET);

  signals [RESIZE_DRAG_END] =
    g_signal_new ("resize-drag-end",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (DzlMultiPanedClass, resize_drag_end),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1, GTK_TYPE_WIDGET);
}

static void
dzl_multi_paned_init (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);

  priv->orientation = GTK_ORIENTATION_VERTICAL;
  priv->children = g_array_new (FALSE, TRUE, sizeof (DzlMultiPanedChild));

  dzl_multi_paned_create_pan_gesture (self);
}

GtkWidget *
dzl_multi_paned_new (void)
{
  return g_object_new (DZL_TYPE_MULTI_PANED, NULL);
}

guint
dzl_multi_paned_get_n_children (DzlMultiPaned *self)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_MULTI_PANED (self), 0);

  return priv->children ? priv->children->len : 0;
}

/**
 * dzl_multi_paned_get_nth_child:
 * @self: a #DzlMultiPaned
 *
 * Gets the @nth child of the #DzlMultiPaned.
 *
 * It is an error to call this function with a value >= the result of
 * dzl_multi_paned_get_nth_child().
 *
 * The index starts from 0.
 *
 * Returns: (transfer none): A #GtkWidget
 */
GtkWidget *
dzl_multi_paned_get_nth_child (DzlMultiPaned *self,
                               guint          nth)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL);
  g_return_val_if_fail (nth < priv->children->len, NULL);

  return g_array_index (priv->children, DzlMultiPanedChild, nth).widget;
}

/**
 * dzl_multi_paned_get_at_point:
 * @self: a #DzlMultiPaned
 * @x: x coordinate
 * @y: y coordinate
 *
 * Locates the widget at position x,y within widget.
 *
 * @x and @y should be relative to @self.
 *
 * Returns: (transfer none) (nullable): a #GtkWidget or %NULL
 *
 * Since: 3.28
 */
GtkWidget *
dzl_multi_paned_get_at_point (DzlMultiPaned *self,
                              gint           x,
                              gint           y)
{
  DzlMultiPanedPrivate *priv = dzl_multi_paned_get_instance_private (self);
  GtkAllocation alloc;

  g_return_val_if_fail (DZL_IS_MULTI_PANED (self), NULL);

  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);

  if (IS_HORIZONTAL (priv->orientation))
    {
      for (guint i = 0; i < priv->children->len; i++)
        {
          const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

          if (x >= child->alloc.x && x < (child->alloc.x + child->alloc.width))
            return child->widget;
        }
    }
  else
    {
      for (guint i = 0; i < priv->children->len; i++)
        {
          const DzlMultiPanedChild *child = &g_array_index (priv->children, DzlMultiPanedChild, i);

          if (y >= child->alloc.y && y < (child->alloc.y + child->alloc.height))
            return child->widget;
        }
    }

  return NULL;
}
