[sysprof/wip/gtk4-port] egg: bring multi-paned ported to GTK 4



commit e14c2616034fdfcc5b400a0c27eaf5224d4b9b56
Author: Christian Hergert <chergert redhat com>
Date:   Fri Oct 1 14:49:04 2021 -0700

    egg: bring multi-paned ported to GTK 4
    
    This is out of libpanel, which already has it but with the dock stuff
    removed (more like DzlMultiPaned). We probably still need some work to
    handle natural allocation from children.

 src/libsysprof-ui/egg-handle-private.h  |  35 ++
 src/libsysprof-ui/egg-handle.c          | 145 ++++++++
 src/libsysprof-ui/egg-paned-private.h   |  48 +++
 src/libsysprof-ui/egg-paned.c           | 593 ++++++++++++++++++++++++++++++++
 src/libsysprof-ui/egg-resizer-private.h |  40 +++
 src/libsysprof-ui/egg-resizer.c         | 488 ++++++++++++++++++++++++++
 src/libsysprof-ui/meson.build           |   4 +
 7 files changed, 1353 insertions(+)
---
diff --git a/src/libsysprof-ui/egg-handle-private.h b/src/libsysprof-ui/egg-handle-private.h
new file mode 100644
index 00000000..9c004aa0
--- /dev/null
+++ b/src/libsysprof-ui/egg-handle-private.h
@@ -0,0 +1,35 @@
+/* egg-handle.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_HANDLE (egg_handle_get_type())
+
+G_DECLARE_FINAL_TYPE (EggHandle, egg_handle, EGG, HANDLE, GtkWidget)
+
+GtkWidget *egg_handle_new          (GtkPositionType  position);
+void       egg_handle_set_position (EggHandle       *self,
+                                    GtkPositionType  position);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/egg-handle.c b/src/libsysprof-ui/egg-handle.c
new file mode 100644
index 00000000..5872abb3
--- /dev/null
+++ b/src/libsysprof-ui/egg-handle.c
@@ -0,0 +1,145 @@
+/* egg-handle.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include "egg-handle-private.h"
+
+#define EXTRA_SIZE 8
+
+struct _EggHandle
+{
+  GtkWidget        parent_instance;
+  GtkWidget       *separator;
+  GtkPositionType  position : 3;
+};
+
+G_DEFINE_TYPE (EggHandle, egg_handle, GTK_TYPE_WIDGET)
+
+static gboolean
+egg_handle_contains (GtkWidget *widget,
+                     double     x,
+                     double     y)
+{
+  EggHandle *self = (EggHandle *)widget;
+  graphene_rect_t area;
+
+  g_assert (EGG_IS_HANDLE (self));
+
+  if (!gtk_widget_compute_bounds (GTK_WIDGET (self->separator),
+                                  GTK_WIDGET (self),
+                                  &area))
+      return FALSE;
+
+  switch (self->position)
+    {
+    case GTK_POS_LEFT:
+      area.origin.x -= EXTRA_SIZE;
+      area.size.width = EXTRA_SIZE;
+      break;
+
+    case GTK_POS_RIGHT:
+      area.size.width = EXTRA_SIZE;
+      break;
+
+    case GTK_POS_TOP:
+      area.origin.y -= EXTRA_SIZE;
+      area.size.height = EXTRA_SIZE;
+      break;
+
+    case GTK_POS_BOTTOM:
+      area.size.height = EXTRA_SIZE;
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+
+  return graphene_rect_contains_point (&area, &GRAPHENE_POINT_INIT (x, y));
+}
+
+static void
+egg_handle_dispose (GObject *object)
+{
+  EggHandle *self = (EggHandle *)object;
+
+  g_clear_pointer (&self->separator, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (egg_handle_parent_class)->dispose (object);
+}
+
+static void
+egg_handle_class_init (EggHandleClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = egg_handle_dispose;
+
+  widget_class->contains = egg_handle_contains;
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+}
+
+static void
+egg_handle_init (EggHandle *self)
+{
+  self->separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+  gtk_widget_set_parent (GTK_WIDGET (self->separator), GTK_WIDGET (self));
+}
+
+void
+egg_handle_set_position (EggHandle       *self,
+                         GtkPositionType  position)
+{
+  g_return_if_fail (EGG_IS_HANDLE (self));
+
+  self->position = position;
+
+  switch (position)
+    {
+    case GTK_POS_LEFT:
+    case GTK_POS_RIGHT:
+      gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "col-resize");
+      gtk_orientable_set_orientation (GTK_ORIENTABLE (self->separator), GTK_ORIENTATION_VERTICAL);
+      break;
+
+    case GTK_POS_TOP:
+    case GTK_POS_BOTTOM:
+      gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "row-resize");
+      gtk_orientable_set_orientation (GTK_ORIENTABLE (self->separator), GTK_ORIENTATION_HORIZONTAL);
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+}
+
+GtkWidget *
+egg_handle_new (GtkPositionType position)
+{
+  EggHandle *self;
+
+  self = g_object_new (EGG_TYPE_HANDLE, NULL);
+  egg_handle_set_position (self, position);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/libsysprof-ui/egg-paned-private.h b/src/libsysprof-ui/egg-paned-private.h
new file mode 100644
index 00000000..b673c1a4
--- /dev/null
+++ b/src/libsysprof-ui/egg-paned-private.h
@@ -0,0 +1,48 @@
+/* egg-paned.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_PANED (egg_paned_get_type())
+
+G_DECLARE_FINAL_TYPE (EggPaned, egg_paned, EGG, PANED, GtkWidget)
+
+GtkWidget *egg_paned_new            (void);
+void       egg_paned_append         (EggPaned  *self,
+                                     GtkWidget *child);
+void       egg_paned_prepend        (EggPaned  *self,
+                                     GtkWidget *child);
+void       egg_paned_insert         (EggPaned  *self,
+                                     int        position,
+                                     GtkWidget *child);
+void       egg_paned_insert_after   (EggPaned  *self,
+                                     GtkWidget *child,
+                                     GtkWidget *sibling);
+void       egg_paned_remove         (EggPaned  *self,
+                                     GtkWidget *child);
+guint      egg_paned_get_n_children (EggPaned  *self);
+GtkWidget *egg_paned_get_nth_child  (EggPaned  *self,
+                                     guint      nth);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/egg-paned.c b/src/libsysprof-ui/egg-paned.c
new file mode 100644
index 00000000..25b0d82c
--- /dev/null
+++ b/src/libsysprof-ui/egg-paned.c
@@ -0,0 +1,593 @@
+/* egg-paned.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "egg-paned-private.h"
+#include "egg-resizer-private.h"
+
+struct _EggPaned
+{
+  GtkWidget      parent_instance;
+  GtkOrientation orientation;
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EggPaned, egg_paned, GTK_TYPE_WIDGET,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+enum {
+  PROP_0,
+  N_PROPS,
+
+  PROP_ORIENTATION,
+};
+
+static void
+update_orientation (GtkWidget      *widget,
+                    GtkOrientation  orientation)
+{
+  if (orientation == GTK_ORIENTATION_HORIZONTAL)
+    {
+      gtk_widget_remove_css_class (widget, "vertical");
+      gtk_widget_add_css_class (widget, "horizontal");
+    }
+  else
+    {
+      gtk_widget_remove_css_class (widget, "horizontal");
+      gtk_widget_add_css_class (widget, "vertical");
+    }
+
+  gtk_accessible_update_property (GTK_ACCESSIBLE (widget),
+                                  GTK_ACCESSIBLE_PROPERTY_ORIENTATION, orientation,
+                                  -1);
+}
+
+/**
+ * egg_paned_new:
+ *
+ * Create a new #EggPaned.
+ *
+ * Returns: (transfer full): a newly created #EggPaned
+ */
+GtkWidget *
+egg_paned_new (void)
+{
+  return g_object_new (EGG_TYPE_PANED, NULL);
+}
+
+static void
+egg_paned_set_orientation (EggPaned       *self,
+                           GtkOrientation  orientation)
+{
+  GtkPositionType pos;
+
+  g_assert (EGG_IS_PANED (self));
+  g_assert (orientation == GTK_ORIENTATION_HORIZONTAL ||
+            orientation == GTK_ORIENTATION_VERTICAL);
+
+  if (self->orientation == orientation)
+    return;
+
+  self->orientation = orientation;
+
+  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+    pos = GTK_POS_LEFT;
+  else
+    pos = GTK_POS_TOP;
+
+  for (GtkWidget *child = gtk_widget_get_last_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_prev_sibling (child))
+    {
+      g_assert (EGG_IS_RESIZER (child));
+
+      egg_resizer_set_position (EGG_RESIZER (child), pos);
+    }
+
+  update_orientation (GTK_WIDGET (self), self->orientation);
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+  g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+egg_paned_measure (GtkWidget      *widget,
+                   GtkOrientation  orientation,
+                   int             for_size,
+                   int            *minimum,
+                   int            *natural,
+                   int            *minimum_baseline,
+                   int            *natural_baseline)
+{
+  EggPaned *self = (EggPaned *)widget;
+
+  g_assert (EGG_IS_PANED (self));
+
+  *minimum = 0;
+  *natural = 0;
+  *minimum_baseline = -1;
+  *natural_baseline = -1;
+
+  for (GtkWidget *child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      int child_min, child_nat;
+
+      gtk_widget_measure (child, orientation, for_size, &child_min, &child_nat, NULL, NULL);
+
+      if (orientation == self->orientation)
+        {
+          *minimum += child_min;
+          *natural += child_nat;
+        }
+      else
+        {
+          *minimum = MAX (*minimum, child_min);
+          *natural = MAX (*natural, child_nat);
+        }
+    }
+}
+
+typedef struct
+{
+  GtkWidget      *widget;
+  GtkRequisition  min_request;
+  GtkRequisition  nat_request;
+  GtkAllocation   alloc;
+} ChildAllocation;
+
+static void
+egg_paned_size_allocate (GtkWidget *widget,
+                         int        width,
+                         int        height,
+                         int        baseline)
+{
+  EggPaned *self = (EggPaned *)widget;
+  ChildAllocation *allocs;
+  ChildAllocation *last_alloc = NULL;
+  GtkOrientation orientation;
+  guint n_children = 0;
+  guint n_expand = 0;
+  guint i;
+  int extra_width = width;
+  int extra_height = height;
+  int expand_width;
+  int expand_height;
+  int x, y;
+
+  g_assert (EGG_IS_PANED (self));
+
+  GTK_WIDGET_CLASS (egg_paned_parent_class)->size_allocate (widget, width, height, baseline);
+
+  n_children = egg_paned_get_n_children (self);
+
+  if (n_children == 1)
+    {
+      GtkWidget *child = gtk_widget_get_first_child (widget);
+      GtkAllocation alloc = { 0, 0, width, height };
+
+      if (gtk_widget_get_visible (child))
+        {
+          gtk_widget_size_allocate (child, &alloc, -1);
+          return;
+        }
+    }
+
+  orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (self));
+  allocs = g_newa (ChildAllocation, n_children);
+  memset (allocs, 0, sizeof *allocs * n_children);
+
+  /* Give min size to each of the children */
+  i = 0;
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child), i++)
+    {
+      ChildAllocation *child_alloc = &allocs[i];
+
+      if (!gtk_widget_get_visible (child))
+        continue;
+
+      gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, height,
+                          &child_alloc->min_request.width,
+                          &child_alloc->nat_request.width,
+                          NULL, NULL);
+      gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, width,
+                          &child_alloc->min_request.height,
+                          &child_alloc->nat_request.height,
+                          NULL, NULL);
+
+      child_alloc->alloc.width = child_alloc->min_request.width;
+      child_alloc->alloc.height = child_alloc->min_request.height;
+
+      n_expand += gtk_widget_compute_expand (child, orientation);
+
+      if (orientation == GTK_ORIENTATION_HORIZONTAL)
+        {
+          extra_width -= child_alloc->alloc.width;
+          child_alloc->alloc.height = height;
+        }
+      else
+        {
+          extra_height -= child_alloc->alloc.height;
+          child_alloc->alloc.width = width;
+        }
+    }
+
+  /* Now try to distribute extra space for natural size */
+  i = 0;
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child), i++)
+    {
+      ChildAllocation *child_alloc = &allocs[i];
+
+      if (!gtk_widget_get_visible (child))
+        continue;
+
+      if (orientation == GTK_ORIENTATION_HORIZONTAL)
+        {
+          int taken = MIN (extra_width, child_alloc->nat_request.width - child_alloc->alloc.width);
+
+          if (taken > 0)
+            {
+              child_alloc->alloc.width += taken;
+              extra_width -= taken;
+            }
+        }
+      else
+        {
+          int taken = MIN (extra_height, child_alloc->nat_request.height - child_alloc->alloc.height);
+
+          if (taken > 0)
+            {
+              child_alloc->alloc.height += taken;
+              extra_height -= taken;
+            }
+        }
+
+      last_alloc = child_alloc;
+    }
+
+  /* Now give extra space for those that expand */
+  expand_width = n_expand ? extra_width / n_expand : 0;
+  expand_height = n_expand ? extra_height / n_expand : 0;
+  i = n_children;
+  for (GtkWidget *child = gtk_widget_get_last_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_prev_sibling (child), i--)
+    {
+      ChildAllocation *child_alloc = &allocs[i-1];
+
+      if (!gtk_widget_get_visible (child))
+        continue;
+
+      if (!gtk_widget_compute_expand (child, orientation))
+        continue;
+
+      if (orientation == GTK_ORIENTATION_HORIZONTAL)
+        {
+          child_alloc->alloc.width += expand_width;
+          extra_width -= expand_width;
+        }
+      else
+        {
+          child_alloc->alloc.height += expand_height;
+          extra_height -= expand_height;
+        }
+    }
+
+  /* Give any leftover to the last visible child */
+  if (last_alloc)
+    {
+      if (orientation == GTK_ORIENTATION_HORIZONTAL)
+        last_alloc->alloc.width += extra_width;
+      else
+        last_alloc->alloc.height += extra_height;
+    }
+
+  i = 0;
+  x = 0;
+  y = 0;
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child), i++)
+    {
+      ChildAllocation *child_alloc = &allocs[i];
+
+      child_alloc->alloc.x = x;
+      child_alloc->alloc.y = y;
+
+      if (orientation == GTK_ORIENTATION_HORIZONTAL)
+        x += child_alloc->alloc.width;
+      else
+        y += child_alloc->alloc.height;
+
+      gtk_widget_size_allocate (child, &child_alloc->alloc, -1);
+    }
+}
+
+static void
+egg_paned_dispose (GObject *object)
+{
+  EggPaned *self = (EggPaned *)object;
+  GtkWidget *child;
+
+  child = gtk_widget_get_first_child (GTK_WIDGET (self));
+  while (child)
+    {
+      GtkWidget *next = gtk_widget_get_next_sibling (child);
+      gtk_widget_unparent (child);
+      child = next;
+    }
+
+  G_OBJECT_CLASS (egg_paned_parent_class)->dispose (object);
+}
+
+static void
+egg_paned_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  EggPaned *self = EGG_PANED (object);
+
+  switch (prop_id)
+    {
+    case PROP_ORIENTATION:
+      g_value_set_enum (value, self->orientation);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_paned_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  EggPaned *self = EGG_PANED (object);
+
+  switch (prop_id)
+    {
+    case PROP_ORIENTATION:
+      egg_paned_set_orientation (self, g_value_get_enum (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_paned_class_init (EggPanedClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = egg_paned_dispose;
+  object_class->get_property = egg_paned_get_property;
+  object_class->set_property = egg_paned_set_property;
+
+  widget_class->measure = egg_paned_measure;
+  widget_class->size_allocate = egg_paned_size_allocate;
+
+  g_object_class_override_property (object_class, PROP_ORIENTATION, "orientation");
+
+  gtk_widget_class_set_css_name (widget_class, "eggpaned");
+}
+
+static void
+egg_paned_init (EggPaned *self)
+{
+  self->orientation = GTK_ORIENTATION_HORIZONTAL;
+
+  update_orientation (GTK_WIDGET (self), self->orientation);
+}
+
+static void
+egg_paned_update_handles (EggPaned *self)
+{
+  GtkWidget *child;
+
+  g_assert (EGG_IS_PANED (self));
+
+  for (child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      GtkWidget *handle;
+
+      g_assert (EGG_IS_RESIZER (child));
+
+      if ((handle = egg_resizer_get_handle (EGG_RESIZER (child))))
+        gtk_widget_show (handle);
+    }
+
+  if ((child = gtk_widget_get_last_child (GTK_WIDGET (self))))
+    {
+      GtkWidget *handle = egg_resizer_get_handle (EGG_RESIZER (child));
+      gtk_widget_hide (handle);
+    }
+}
+
+void
+egg_paned_remove (EggPaned  *self,
+                  GtkWidget *child)
+{
+  GtkWidget *resizer;
+
+  g_return_if_fail (EGG_IS_PANED (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+
+  resizer = gtk_widget_get_ancestor (child, EGG_TYPE_RESIZER);
+  g_return_if_fail (resizer != NULL &&
+                    gtk_widget_get_parent (resizer) == GTK_WIDGET (self));
+  gtk_widget_unparent (resizer);
+  egg_paned_update_handles (self);
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+void
+egg_paned_insert (EggPaned  *self,
+                  int        position,
+                  GtkWidget *child)
+{
+  GtkPositionType pos;
+  GtkWidget *resizer;
+
+  g_return_if_fail (EGG_IS_PANED (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == NULL);
+
+  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+    pos = GTK_POS_LEFT;
+  else
+    pos = GTK_POS_TOP;
+
+  resizer = egg_resizer_new (pos);
+  egg_resizer_set_child (EGG_RESIZER (resizer), child);
+
+  if (position < 0)
+    gtk_widget_insert_before (GTK_WIDGET (resizer), GTK_WIDGET (self), NULL);
+  else if (position == 0)
+    gtk_widget_insert_after (GTK_WIDGET (resizer), GTK_WIDGET (self), NULL);
+  else
+    {
+      GtkWidget *sibling = gtk_widget_get_first_child (GTK_WIDGET (self));
+
+      for (int i = position; i > 0 && sibling != NULL; i--)
+        sibling = gtk_widget_get_next_sibling (sibling);
+
+      gtk_widget_insert_before (GTK_WIDGET (resizer), GTK_WIDGET (self), sibling);
+    }
+
+  egg_paned_update_handles (self);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+void
+egg_paned_append (EggPaned  *self,
+                  GtkWidget *child)
+{
+  egg_paned_insert (self, -1, child);
+}
+
+void
+egg_paned_prepend (EggPaned  *self,
+                   GtkWidget *child)
+{
+  egg_paned_insert (self, 0, child);
+}
+
+void
+egg_paned_insert_after (EggPaned  *self,
+                        GtkWidget *child,
+                        GtkWidget *sibling)
+{
+  int position = 0;
+
+  g_return_if_fail (EGG_IS_PANED (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (!sibling || GTK_IS_WIDGET (sibling));
+
+  if (sibling == NULL)
+    {
+      egg_paned_prepend (self, child);
+      return;
+    }
+
+  /* TODO: We should reverse insert() to call this */
+
+  for (GtkWidget *ancestor = gtk_widget_get_first_child (GTK_WIDGET (self));
+       ancestor != NULL;
+       ancestor = gtk_widget_get_next_sibling (ancestor))
+    {
+      position++;
+
+      if (sibling == ancestor || gtk_widget_is_ancestor (sibling, ancestor))
+        break;
+    }
+
+  egg_paned_insert (self, position, child);
+}
+
+guint
+egg_paned_get_n_children (EggPaned *self)
+{
+  guint count = 0;
+
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    count++;
+
+  return count;
+}
+
+GtkWidget *
+egg_paned_get_nth_child (EggPaned *self,
+                         guint     nth)
+{
+  g_return_val_if_fail (EGG_IS_PANED (self), NULL);
+
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      g_assert (EGG_IS_RESIZER (child));
+
+      if (nth == 0)
+        return egg_resizer_get_child (EGG_RESIZER (child));
+
+      nth--;
+    }
+
+  return NULL;
+}
+
+static void
+egg_paned_add_child (GtkBuildable *buildable,
+                     GtkBuilder   *builder,
+                     GObject      *child,
+                     const char   *type)
+{
+  if (GTK_IS_WIDGET (child))
+    egg_paned_append (EGG_PANED (buildable), GTK_WIDGET (child));
+  else
+    g_warning ("Cannot add child of type %s to %s",
+               G_OBJECT_TYPE_NAME (child),
+               G_OBJECT_TYPE_NAME (buildable));
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->add_child = egg_paned_add_child;
+}
diff --git a/src/libsysprof-ui/egg-resizer-private.h b/src/libsysprof-ui/egg-resizer-private.h
new file mode 100644
index 00000000..58db56c3
--- /dev/null
+++ b/src/libsysprof-ui/egg-resizer-private.h
@@ -0,0 +1,40 @@
+/* egg-resizer.h
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_RESIZER (egg_resizer_get_type())
+
+G_DECLARE_FINAL_TYPE (EggResizer, egg_resizer, EGG, RESIZER, GtkWidget)
+
+GtkWidget       *egg_resizer_new          (GtkPositionType  position);
+GtkPositionType  egg_resizer_get_position (EggResizer      *self);
+void             egg_resizer_set_position (EggResizer      *self,
+                                           GtkPositionType  position);
+GtkWidget       *egg_resizer_get_child    (EggResizer      *self);
+void             egg_resizer_set_child    (EggResizer      *self,
+                                           GtkWidget       *child);
+GtkWidget       *egg_resizer_get_handle   (EggResizer      *self);
+
+G_END_DECLS
diff --git a/src/libsysprof-ui/egg-resizer.c b/src/libsysprof-ui/egg-resizer.c
new file mode 100644
index 00000000..6918439c
--- /dev/null
+++ b/src/libsysprof-ui/egg-resizer.c
@@ -0,0 +1,488 @@
+/* egg-resizer.c
+ *
+ * Copyright 2021 Christian Hergert <chergert redhat com>
+ *
+ * This file 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 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include "egg-handle-private.h"
+#include "egg-resizer-private.h"
+
+#define HANDLE_SIZE 8
+
+struct _EggResizer
+{
+  GtkWidget        parent_instance;
+
+  EggHandle       *handle;
+  GtkWidget       *child;
+
+  double           drag_orig_size;
+  double           drag_position;
+
+  GtkPositionType  position : 3;
+};
+
+G_DEFINE_TYPE (EggResizer, egg_resizer, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_CHILD,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+egg_resizer_drag_begin_cb (EggResizer   *self,
+                             double          start_x,
+                             double          start_y,
+                             GtkGestureDrag *drag)
+{
+  GtkAllocation child_alloc;
+  GtkAllocation handle_alloc;
+
+  g_assert (EGG_IS_RESIZER (self));
+  g_assert (GTK_IS_GESTURE_DRAG (drag));
+
+  if (self->child == NULL)
+    return;
+
+  switch (self->position)
+    {
+    case GTK_POS_LEFT:
+      if (start_x > gtk_widget_get_width (GTK_WIDGET (self)) - HANDLE_SIZE)
+        goto start_drag;
+      break;
+
+    case GTK_POS_RIGHT:
+      if (start_x <= HANDLE_SIZE)
+        goto start_drag;
+      break;
+
+    case GTK_POS_TOP:
+      if (start_y > gtk_widget_get_height (GTK_WIDGET (self)) - HANDLE_SIZE)
+        goto start_drag;
+      break;
+
+    case GTK_POS_BOTTOM:
+      if (start_y <= HANDLE_SIZE)
+        goto start_drag;
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+
+deny_sequence:
+  gtk_gesture_set_state (GTK_GESTURE (drag),
+                         GTK_EVENT_SEQUENCE_DENIED);
+
+  return;
+
+start_drag:
+
+  gtk_widget_get_allocation (self->child, &child_alloc);
+  gtk_widget_get_allocation (GTK_WIDGET (self->handle), &handle_alloc);
+
+  if (self->position == GTK_POS_LEFT ||
+      self->position == GTK_POS_RIGHT)
+    {
+      self->drag_orig_size = child_alloc.width + handle_alloc.width;
+      gtk_widget_set_hexpand (self->child, FALSE);
+    }
+  else
+    {
+      self->drag_orig_size = child_alloc.height + handle_alloc.height;
+      gtk_widget_set_vexpand (self->child, FALSE);
+    }
+
+  self->drag_position = self->drag_orig_size;
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+egg_resizer_drag_update_cb (EggResizer   *self,
+                              double          offset_x,
+                              double          offset_y,
+                              GtkGestureDrag *drag)
+{
+  g_assert (EGG_IS_RESIZER (self));
+  g_assert (GTK_IS_GESTURE_DRAG (drag));
+
+  if (self->position == GTK_POS_LEFT)
+    self->drag_position = self->drag_orig_size + offset_x;
+  else if (self->position == GTK_POS_RIGHT)
+    self->drag_position = gtk_widget_get_width (GTK_WIDGET (self)) - offset_x;
+  else if (self->position == GTK_POS_TOP)
+    self->drag_position = self->drag_orig_size + offset_y;
+  else if (self->position == GTK_POS_BOTTOM)
+    self->drag_position = gtk_widget_get_height (GTK_WIDGET (self)) - offset_y;
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+egg_resizer_drag_end_cb (EggResizer   *self,
+                           double          offset_x,
+                           double          offset_y,
+                           GtkGestureDrag *drag)
+{
+  g_assert (EGG_IS_RESIZER (self));
+  g_assert (GTK_IS_GESTURE_DRAG (drag));
+}
+
+GtkWidget *
+egg_resizer_new (GtkPositionType position)
+{
+  EggResizer *self;
+
+  self = g_object_new (EGG_TYPE_RESIZER, NULL);
+  self->position = position;
+  self->handle = EGG_HANDLE (egg_handle_new (position));
+  gtk_widget_set_parent (GTK_WIDGET (self->handle), GTK_WIDGET (self));
+
+  return GTK_WIDGET (self);
+}
+
+static void
+egg_resizer_measure (GtkWidget      *widget,
+                       GtkOrientation  orientation,
+                       int             for_size,
+                       int            *minimum,
+                       int            *natural,
+                       int            *minimum_baseline,
+                       int            *natural_baseline)
+{
+  EggResizer *self = (EggResizer *)widget;
+
+  g_assert (EGG_IS_RESIZER (self));
+
+  *minimum = 0;
+  *natural = 0;
+  *minimum_baseline = -1;
+  *natural_baseline = -1;
+
+  if (self->child != NULL)
+    gtk_widget_measure (self->child,
+                        orientation,
+                        for_size,
+                        minimum, natural,
+                        NULL, NULL);
+
+  if ((orientation == GTK_ORIENTATION_HORIZONTAL &&
+       (self->position == GTK_POS_LEFT ||
+        self->position == GTK_POS_RIGHT)) ||
+      (orientation == GTK_ORIENTATION_VERTICAL &&
+       (self->position == GTK_POS_TOP ||
+        self->position == GTK_POS_BOTTOM)))
+    {
+      int handle_min, handle_nat;
+
+      if (self->drag_position > *minimum)
+        *natural = self->drag_position;
+      else if (self->drag_position < *minimum)
+        *natural = *minimum;
+
+      if (gtk_widget_get_visible (GTK_WIDGET (self->handle)))
+        {
+          gtk_widget_measure (GTK_WIDGET (self->handle),
+                              orientation, for_size,
+                              &handle_min, &handle_nat,
+                              NULL, NULL);
+
+          *minimum += handle_min;
+          *natural += handle_nat;
+        }
+    }
+}
+
+static void
+egg_resizer_size_allocate (GtkWidget *widget,
+                             int        width,
+                             int        height,
+                             int        baseline)
+{
+  EggResizer *self = (EggResizer *)widget;
+  GtkOrientation orientation;
+  GtkAllocation child_alloc;
+  GtkAllocation handle_alloc;
+  int handle_min = 0, handle_nat = 0;
+
+  g_assert (EGG_IS_RESIZER (self));
+
+  if (self->position == GTK_POS_LEFT ||
+      self->position == GTK_POS_RIGHT)
+    orientation = GTK_ORIENTATION_HORIZONTAL;
+  else
+    orientation = GTK_ORIENTATION_VERTICAL;
+
+  if (gtk_widget_get_visible (GTK_WIDGET (self->handle)))
+    gtk_widget_measure (GTK_WIDGET (self->handle),
+                        orientation,
+                        -1,
+                        &handle_min, &handle_nat,
+                        NULL, NULL);
+
+  switch (self->position)
+    {
+    case GTK_POS_LEFT:
+      handle_alloc.x = width - handle_min;
+      handle_alloc.width = handle_min;
+      handle_alloc.y = 0;
+      handle_alloc.height = height;
+      child_alloc.x = 0;
+      child_alloc.y = 0;
+      child_alloc.width = width - handle_min;
+      child_alloc.height = height;
+      break;
+
+    case GTK_POS_RIGHT:
+      handle_alloc.x = 0;
+      handle_alloc.width = handle_min;
+      handle_alloc.y = 0;
+      handle_alloc.height = height;
+      child_alloc.x = handle_min;
+      child_alloc.y = 0;
+      child_alloc.width = width - handle_min;
+      child_alloc.height = height;
+      break;
+
+    case GTK_POS_TOP:
+      handle_alloc.x = 0;
+      handle_alloc.width = width;
+      handle_alloc.y = height - handle_min;
+      handle_alloc.height = handle_min;
+      child_alloc.x = 0;
+      child_alloc.y = 0;
+      child_alloc.width = width;
+      child_alloc.height = height - handle_min;
+      break;
+
+    case GTK_POS_BOTTOM:
+      handle_alloc.x = 0;
+      handle_alloc.width = width;
+      handle_alloc.y = 0;
+      handle_alloc.height = handle_min;
+      child_alloc.x = 0;
+      child_alloc.y = handle_min;
+      child_alloc.width = width;
+      child_alloc.height = height - handle_min;
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  if (gtk_widget_get_mapped (GTK_WIDGET (self->handle)))
+    gtk_widget_size_allocate (GTK_WIDGET (self->handle), &handle_alloc, -1);
+
+  if (self->child != NULL &&
+      gtk_widget_get_mapped (self->child))
+    gtk_widget_size_allocate (self->child, &child_alloc, -1);
+}
+
+static void
+egg_resizer_compute_expand (GtkWidget *widget,
+                              gboolean  *hexpand,
+                              gboolean  *vexpand)
+{
+  EggResizer *self = EGG_RESIZER (widget);
+
+  if (self->child != NULL)
+    {
+      *hexpand = gtk_widget_compute_expand (self->child, GTK_ORIENTATION_HORIZONTAL);
+      *vexpand = gtk_widget_compute_expand (self->child, GTK_ORIENTATION_VERTICAL);
+    }
+  else
+    {
+      *hexpand = FALSE;
+      *vexpand = FALSE;
+    }
+}
+
+static void
+egg_resizer_dispose (GObject *object)
+{
+  EggResizer *self = (EggResizer *)object;
+
+  g_clear_pointer ((GtkWidget **)&self->handle, gtk_widget_unparent);
+  g_clear_pointer ((GtkWidget **)&self->child, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (egg_resizer_parent_class)->dispose (object);
+}
+
+static void
+egg_resizer_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  EggResizer *self = EGG_RESIZER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILD:
+      g_value_set_object (value, egg_resizer_get_child (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_resizer_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  EggResizer *self = EGG_RESIZER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILD:
+      egg_resizer_set_child (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_resizer_class_init (EggResizerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = egg_resizer_dispose;
+  object_class->get_property = egg_resizer_get_property;
+  object_class->set_property = egg_resizer_set_property;
+
+  widget_class->compute_expand = egg_resizer_compute_expand;
+  widget_class->measure = egg_resizer_measure;
+  widget_class->size_allocate = egg_resizer_size_allocate;
+
+  properties [PROP_CHILD] =
+    g_param_spec_object ("child",
+                         "Child",
+                         "Child",
+                         GTK_TYPE_WIDGET,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "eggresizer");
+}
+
+static void
+egg_resizer_init (EggResizer *self)
+{
+  GtkGesture *gesture;
+
+  gesture = gtk_gesture_drag_new ();
+  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE);
+  g_signal_connect_object (gesture,
+                           "drag-begin",
+                           G_CALLBACK (egg_resizer_drag_begin_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (gesture,
+                           "drag-update",
+                           G_CALLBACK (egg_resizer_drag_update_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (gesture,
+                           "drag-end",
+                           G_CALLBACK (egg_resizer_drag_end_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
+}
+
+/**
+ * egg_resizer_get_child:
+ * @self: a #EggResizer
+ *
+ * Gets the child widget of the resizer.
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL
+ */
+GtkWidget *
+egg_resizer_get_child (EggResizer *self)
+{
+  g_return_val_if_fail (EGG_IS_RESIZER (self), NULL);
+
+  return self->child;
+}
+
+void
+egg_resizer_set_child (EggResizer *self,
+                         GtkWidget    *child)
+{
+  g_return_if_fail (EGG_IS_RESIZER (self));
+  g_return_if_fail (!child || GTK_IS_WIDGET (child));
+
+  if (child == self->child)
+    return;
+
+  g_clear_pointer (&self->child, gtk_widget_unparent);
+
+  self->child = child;
+
+  if (self->child != NULL)
+    gtk_widget_insert_before (self->child,
+                              GTK_WIDGET (self),
+                              GTK_WIDGET (self->handle));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILD]);
+}
+
+GtkPositionType
+egg_resizer_get_position (EggResizer *self)
+{
+  g_return_val_if_fail (EGG_IS_RESIZER (self), 0);
+
+  return self->position;
+}
+
+void
+egg_resizer_set_position (EggResizer      *self,
+                          GtkPositionType  position)
+{
+  g_return_if_fail (EGG_IS_RESIZER (self));
+
+  if (position != self->position)
+    {
+      self->position = position;
+
+      egg_handle_set_position (self->handle, position);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+    }
+}
+
+GtkWidget *
+egg_resizer_get_handle (EggResizer *self)
+{
+  g_return_val_if_fail (EGG_IS_RESIZER (self), NULL);
+
+  return GTK_WIDGET (self->handle);
+}
diff --git a/src/libsysprof-ui/meson.build b/src/libsysprof-ui/meson.build
index f55b0cd6..956a7b5f 100644
--- a/src/libsysprof-ui/meson.build
+++ b/src/libsysprof-ui/meson.build
@@ -13,7 +13,11 @@ libsysprof_ui_public_sources = [
 ]
 
 libsysprof_ui_private_sources = [
+  'egg-handle.c',
+  'egg-paned.c',
+  'egg-resizer.c',
   'egg-three-grid.c',
+
   'pointcache.c',
   'rectangles.c',
   'sysprof-aid.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]