[libadwaita/wip/exalm/wrapbox] Draft: AdwWrapLayout/Box




commit 8786823e5ef13ffb9e7184d23bc90b957ca113fa
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Fri Aug 6 12:54:36 2021 +0500

    Draft: AdwWrapLayout/Box

 src/adw-wrap-box.c    | 465 ++++++++++++++++++++++++++++++++++
 src/adw-wrap-box.h    |  58 +++++
 src/adw-wrap-layout.c | 673 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/adw-wrap-layout.h |  39 +++
 src/adwaita.h         |   2 +
 src/meson.build       |   4 +
 6 files changed, 1241 insertions(+)
---
diff --git a/src/adw-wrap-box.c b/src/adw-wrap-box.c
new file mode 100644
index 00000000..3d312d83
--- /dev/null
+++ b/src/adw-wrap-box.c
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-wrap-box.h"
+
+#include "adw-wrap-layout.h"
+
+/**
+ * AdwWrapBox:
+ *
+ * TODO
+ *
+ * ## CSS nodes
+ *
+ * `AdwWrapBox` uses a single CSS node with name `wrapbox`.
+ *
+ * ## Accessibility
+ *
+ * `AdwWrapBox` uses the `GTK_ACCESSIBLE_ROLE_GROUP` role.
+ *
+ * Since: 1.0
+ */
+
+struct _AdwWrapBox
+{
+  GtkWidget parent_instance;
+};
+
+enum {
+  PROP_0,
+  PROP_SPACING,
+  PROP_LINE_SPACING,
+
+  /* Overridden properties */
+  PROP_ORIENTATION,
+
+  LAST_PROP = PROP_LINE_SPACING + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void adw_wrap_box_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwWrapBox, adw_wrap_box, GTK_TYPE_WIDGET,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_wrap_box_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static GtkOrientation
+get_orientation (AdwWrapBox *self)
+{
+  GtkLayoutManager *layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+
+  return gtk_orientable_get_orientation (GTK_ORIENTABLE (layout));
+}
+
+static void
+set_orientation (AdwWrapBox     *self,
+                 GtkOrientation  orientation)
+{
+  GtkLayoutManager *layout = gtk_widget_get_layout_manager (GTK_WIDGET (self));
+
+  if (orientation == get_orientation (self))
+    return;
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (layout), orientation);
+
+  g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+adw_wrap_box_compute_expand (GtkWidget *widget,
+                             gboolean  *hexpand_p,
+                             gboolean  *vexpand_p)
+{
+  GtkWidget *w;
+  gboolean hexpand = FALSE;
+  gboolean vexpand = FALSE;
+
+  for (w = gtk_widget_get_first_child (widget);
+       w != NULL;
+       w = gtk_widget_get_next_sibling (w)) {
+    hexpand = hexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_HORIZONTAL);
+    vexpand = vexpand || gtk_widget_compute_expand (w, GTK_ORIENTATION_VERTICAL);
+  }
+
+  *hexpand_p = hexpand;
+  *vexpand_p = vexpand;
+}
+
+static void
+adw_wrap_box_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  AdwWrapBox *self = ADW_WRAP_BOX (object);
+
+  switch (prop_id) {
+  case PROP_SPACING:
+    g_value_set_int (value, adw_wrap_box_get_spacing (self));
+    break;
+  case PROP_LINE_SPACING:
+    g_value_set_int (value, adw_wrap_box_get_line_spacing (self));
+    break;
+  case PROP_ORIENTATION:
+    g_value_set_enum (value, get_orientation (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_wrap_box_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  AdwWrapBox *self = ADW_WRAP_BOX (object);
+
+  switch (prop_id) {
+  case PROP_SPACING:
+    adw_wrap_box_set_spacing (self, g_value_get_int (value));
+    break;
+  case PROP_LINE_SPACING:
+    adw_wrap_box_set_line_spacing (self, g_value_get_int (value));
+    break;
+  case PROP_ORIENTATION:
+    set_orientation (self, g_value_get_enum (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_wrap_box_dispose (GObject *object)
+{
+  GtkWidget *child;
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
+    gtk_widget_unparent (child);
+
+  G_OBJECT_CLASS (adw_wrap_box_parent_class)->dispose (object);
+}
+
+static void
+adw_wrap_box_class_init (AdwWrapBoxClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = adw_wrap_box_get_property;
+  object_class->set_property = adw_wrap_box_set_property;
+  object_class->dispose = adw_wrap_box_dispose;
+
+  widget_class->compute_expand = adw_wrap_box_compute_expand;
+
+  g_object_class_override_property (object_class,
+                                    PROP_ORIENTATION,
+                                    "orientation");
+
+  /**
+   * AdwWrapBox:spacing: (attributes org.gtk.Property.get=adw_wrap_box_get_spacing 
org.gtk.Property.set=adw_wrap_box_set_spacing)
+   *
+   * The spacing between widgets on the same line.
+   *
+   * Since: 1.0
+   */
+  props[PROP_SPACING] =
+    g_param_spec_int ("spacing",
+                      "Spacing",
+                      "The spacing between widgets on the same line",
+                      0, G_MAXINT, 0,
+                      G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwWrapBox:line-spacing: (attributes org.gtk.Property.get=adw_wrap_box_get_line_spacing 
org.gtk.Property.set=adw_wrap_box_set_line_spacing)
+   *
+   * The spacing between lines.
+   *
+   * Since: 1.0
+   */
+  props[PROP_LINE_SPACING] =
+    g_param_spec_int ("line-spacing",
+                      "Line spacing",
+                      "The spacing between lines",
+                      0, G_MAXINT, 0,
+                      G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, ADW_TYPE_WRAP_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "wrapbox");
+  gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
+}
+
+static void
+adw_wrap_box_init (AdwWrapBox *self)
+{
+}
+
+static void
+adw_wrap_box_buildable_add_child (GtkBuildable *buildable,
+                                  GtkBuilder   *builder,
+                                  GObject      *child,
+                                  const char   *type)
+{
+  if (GTK_IS_WIDGET (child))
+    adw_wrap_box_append (ADW_WRAP_BOX (buildable), GTK_WIDGET (child));
+  else
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_wrap_box_buildable_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+  iface->add_child = adw_wrap_box_buildable_add_child;
+}
+
+/**
+ * adw_wrap_box_new:
+ *
+ * Creates a new `AdwWrapBox`.
+ *
+ * Returns: the newly created `AdwWrapBox`
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_wrap_box_new (void)
+{
+  return g_object_new (ADW_TYPE_WRAP_BOX, NULL);
+}
+
+/**
+ * adw_wrap_box_get_spacing: (attributes org.gtk.Method.get_property=spacing)
+ * @self: a `AdwWrapBox`
+ *
+ * Gets spacing between widgets on the same line.
+ *
+ * Returns: spacing between widgets on the same line
+ *
+ * Since: 1.0
+ */
+int
+adw_wrap_box_get_spacing (AdwWrapBox *self)
+{
+  AdwWrapLayout *layout;
+
+  g_return_val_if_fail (ADW_IS_WRAP_BOX (self), 0);
+
+  layout = ADW_WRAP_LAYOUT (gtk_widget_get_layout_manager (GTK_WIDGET (self)));
+
+  return adw_wrap_layout_get_spacing (layout);
+}
+
+/**
+ * adw_wrap_box_set_spacing: (attributes org.gtk.Method.set_property=spacing)
+ * @self: a `AdwWrapBox`
+ * @spacing: the spacing
+ *
+ * Sets the spacing between widgets on the same line.
+ *
+ * Since: 1.0
+ */
+void
+adw_wrap_box_set_spacing (AdwWrapBox *self,
+                          int         spacing)
+{
+  AdwWrapLayout *layout;
+
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+
+  if (spacing < 0)
+    spacing = 0;
+
+  layout = ADW_WRAP_LAYOUT (gtk_widget_get_layout_manager (GTK_WIDGET (self)));
+
+  if (spacing == adw_wrap_layout_get_spacing (layout))
+    return;
+
+  adw_wrap_layout_set_spacing (layout, spacing);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+/**
+ * adw_wrap_box_get_line_spacing: (attributes org.gtk.Method.get_property=line-spacing)
+ * @self: a `AdwWrapBox`
+ *
+ * Gets the spacing between lines.
+ *
+ * Returns: the line spacing
+ *
+ * Since: 1.0
+ */
+int
+adw_wrap_box_get_line_spacing (AdwWrapBox *self)
+{
+  AdwWrapLayout *layout;
+
+  g_return_val_if_fail (ADW_IS_WRAP_BOX (self), 0);
+
+  layout = ADW_WRAP_LAYOUT (gtk_widget_get_layout_manager (GTK_WIDGET (self)));
+
+  return adw_wrap_layout_get_line_spacing (layout);
+}
+
+/**
+ * adw_wrap_box_set_line_spacing: (attributes org.gtk.Method.set_property=line-spacing)
+ * @self: a `AdwWrapBox`
+ * @line_spacing: the line spacing
+ *
+ * Sets the spacing between lines.
+ *
+ * Since: 1.0
+ */
+void
+adw_wrap_box_set_line_spacing (AdwWrapBox *self,
+                               int         line_spacing)
+{
+  AdwWrapLayout *layout;
+
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+
+  if (line_spacing < 0)
+    line_spacing = 0;
+
+  layout = ADW_WRAP_LAYOUT (gtk_widget_get_layout_manager (GTK_WIDGET (self)));
+
+  if (line_spacing == adw_wrap_layout_get_line_spacing (layout))
+    return;
+
+  adw_wrap_layout_set_line_spacing (layout, line_spacing);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LINE_SPACING]);
+}
+
+/**
+ * adw_wrap_box_insert_child_after:
+ * @self: a `AdwWrapBox`
+ * @child: the widget to insert
+ * @sibling: (nullable): the sibling after which to insert @child
+ *
+ * Inserts @child in the position after @sibling in the list of @self children.
+ *
+ * If @sibling is `NULL`, inserts @child at the first position.
+ */
+void
+adw_wrap_box_insert_child_after (AdwWrapBox *self,
+                                 GtkWidget  *child,
+                                 GtkWidget  *sibling)
+{
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == NULL);
+
+  if (sibling) {
+    g_return_if_fail (GTK_IS_WIDGET (sibling));
+    g_return_if_fail (gtk_widget_get_parent (sibling) == GTK_WIDGET (self));
+  }
+
+  if (child == sibling)
+    return;
+
+  gtk_widget_insert_after (child, GTK_WIDGET (self), sibling);
+}
+
+/**
+ * adw_wrap_box_reorder_child_after:
+ * @self: a `AdwWrapBox`
+ * @child: the widget to move, must be a child of @self
+ * @sibling: (nullable): the sibling to move @child after
+ *
+ * Moves @child to the position after @sibling in the list of @self children.
+ *
+ * If @sibling is `NULL`, moves @child to the first position.
+ */
+void
+adw_wrap_box_reorder_child_after (AdwWrapBox *self,
+                                  GtkWidget  *child,
+                                  GtkWidget  *sibling)
+{
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == GTK_WIDGET (self));
+
+  if (sibling) {
+    g_return_if_fail (GTK_IS_WIDGET (sibling));
+    g_return_if_fail (gtk_widget_get_parent (sibling) == GTK_WIDGET (self));
+  }
+
+  if (child == sibling)
+    return;
+
+  gtk_widget_insert_after (child, GTK_WIDGET (self), sibling);
+}
+
+/**
+ * adw_wrap_box_append:
+ * @self: a `AdwWrapBox`
+ * @child: the widget to append
+ *
+ * Adds @child as the last child to @self.
+ */
+void
+adw_wrap_box_append (AdwWrapBox *self,
+                     GtkWidget  *child)
+{
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == NULL);
+
+  gtk_widget_insert_before (child, GTK_WIDGET (self), NULL);
+}
+
+/**
+ * adw_wrap_box_prepend:
+ * @self: a `AdwWrapBox`
+ * @child: the widget to prepend
+ *
+ * Adds @child as the first child to @self.
+ */
+void
+adw_wrap_box_prepend (AdwWrapBox *self,
+                      GtkWidget  *child)
+{
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == NULL);
+
+  gtk_widget_insert_after (child, GTK_WIDGET (self), NULL);
+}
+
+/**
+ * adw_wrap_box_remove:
+ * @self: a `AdwWrapBox`
+ * @child: the child to remove
+ *
+ * Removes a child widget from @self.
+ *
+ * The child must have been added before with [method@Adw.WrapBox.append],
+ * [method@Adw.WrapBox.prepend], or [method@Adw.WrapBox.insert_child_after].
+ */
+void
+adw_wrap_box_remove (AdwWrapBox *self,
+                     GtkWidget  *child)
+{
+  g_return_if_fail (ADW_IS_WRAP_BOX (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+  g_return_if_fail (gtk_widget_get_parent (child) == GTK_WIDGET (self));
+
+  gtk_widget_unparent (child);
+}
+
diff --git a/src/adw-wrap-box.h b/src/adw-wrap-box.h
new file mode 100644
index 00000000..872ea48e
--- /dev/null
+++ b/src/adw-wrap-box.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_WRAP_BOX (adw_wrap_box_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwWrapBox, adw_wrap_box, ADW, WRAP_BOX, GtkWidget)
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_wrap_box_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_ALL
+int  adw_wrap_box_get_spacing (AdwWrapBox *self);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_set_spacing (AdwWrapBox *self,
+                               int         spacing);
+
+ADW_AVAILABLE_IN_ALL
+int  adw_wrap_box_get_line_spacing (AdwWrapBox *self);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_set_line_spacing (AdwWrapBox *self,
+                                    int         line_spacing);
+
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_insert_child_after  (AdwWrapBox *self,
+                                       GtkWidget  *child,
+                                       GtkWidget  *sibling);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_reorder_child_after (AdwWrapBox *self,
+                                       GtkWidget  *child,
+                                       GtkWidget  *sibling);
+
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_append  (AdwWrapBox *self,
+                           GtkWidget  *child);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_prepend (AdwWrapBox *self,
+                           GtkWidget  *child);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_box_remove  (AdwWrapBox *self,
+                           GtkWidget  *child);
+
+G_END_DECLS
diff --git a/src/adw-wrap-layout.c b/src/adw-wrap-layout.c
new file mode 100644
index 00000000..c57eae36
--- /dev/null
+++ b/src/adw-wrap-layout.c
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-wrap-layout.h"
+
+#include <math.h>
+
+/**
+ * AdwWrapLayout:
+ *
+ * `AdwWrapLayout` is something I couldn't describe because I'm bad at docs.
+ *
+ * `AdwWrapLayout` is similar to `GtkBoxLayout` but can wrap lines when the
+ * widgets cannot fit otherwise. Unlike `GtkFlowBox`, the children aren't
+ * arranged into a grid and behave more like words in a wrapped label.
+ *
+ * Since: 1.0
+ */
+
+struct _AdwWrapLayout
+{
+  GtkLayoutManager parent_instance;
+
+  int spacing;
+  int line_spacing;
+  GtkOrientation orientation;
+};
+
+enum {
+  PROP_0,
+  PROP_SPACING,
+  PROP_LINE_SPACING,
+
+  /* Overridden properties */
+  PROP_ORIENTATION,
+
+  LAST_PROP = PROP_LINE_SPACING + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (AdwWrapLayout, adw_wrap_layout, GTK_TYPE_LAYOUT_MANAGER,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static void
+set_orientation (AdwWrapLayout  *self,
+                 GtkOrientation  orientation)
+{
+  if (self->orientation == orientation)
+    return;
+
+  self->orientation = orientation;
+
+  gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+  g_object_notify (G_OBJECT (self), "orientation");
+}
+
+typedef struct _AllocationData AllocationData;
+
+struct _AllocationData {
+  // Provided values
+  int minimum_size;
+  int natural_size;
+  gboolean expand;
+
+  // Computed values
+  int allocated_size;
+
+  // Context
+  union {
+    GtkWidget *widget;
+    struct {
+      AllocationData *children;
+      int n_children;
+    } line;
+  } data;
+};
+
+static int
+count_line_children (AdwWrapLayout  *self,
+                     int             for_size,
+                     AllocationData *child_data,
+                     int             n_children,
+                     int            *unused_space)
+{
+  int remaining_space = for_size + self->spacing;
+  int n_line_children = 0;
+
+  /* Count how many widgets can fit into this line */
+  while (true) {
+    int delta;
+
+    if (n_line_children >= n_children)
+      break;
+
+    delta = child_data[n_line_children].natural_size + self->spacing; // TODO policy
+
+    // FIXME should set explicit overflow flag, this is dirty
+    if (remaining_space - delta < 0)
+      break;
+
+    remaining_space -= delta;
+    n_line_children++;
+  }
+
+  if (unused_space)
+    *unused_space = remaining_space;
+
+  return n_line_children;
+}
+
+static int
+count_lines (AdwWrapLayout  *self,
+             int             for_size,
+             AllocationData *child_data,
+             int             n_children)
+{
+  int n_lines = 0;
+
+  while (n_children > 0) {
+    int unused_space;
+    int n_line_children = count_line_children (self, for_size, child_data,
+                                               n_children, &unused_space);
+
+    if (n_line_children == 0)
+      n_line_children++;
+
+    n_children -= n_line_children;
+    child_data = &child_data[n_line_children];
+    n_lines++;
+  }
+
+  return n_lines;
+}
+
+static void
+box_allocate (AllocationData *child_data,
+              int             n_children,
+              int             for_size,
+              int             spacing)
+{
+  int extra_space;
+  int n_expand = 0;
+  int size_given_to_child = 0;
+  int n_extra_widgets = 0;
+  int children_minimum_size = 0;
+  int i;
+  GtkRequestedSize *sizes = g_newa (GtkRequestedSize, n_children);
+
+  for (i = 0; i < n_children; i++) {
+    if (child_data[i].expand)
+      n_expand++;
+
+    children_minimum_size += child_data[i].minimum_size;
+  }
+
+  extra_space = for_size - (n_children - 1) * spacing;
+  g_assert (extra_space >= 0);
+
+  for (i = 0; i < n_children; i++) {
+    sizes[i].minimum_size = child_data[i].minimum_size;
+    sizes[i].natural_size = child_data[i].natural_size;
+  }
+
+  /* Bring children up to size first */
+  extra_space -= children_minimum_size;
+  extra_space = MAX (0, extra_space);
+  extra_space = gtk_distribute_natural_allocation (extra_space, n_children, sizes);
+
+  /* Calculate space which hasn't been distributed yet,
+   * and is available for expanding children.
+   */
+  if (n_expand > 0) {
+    size_given_to_child = extra_space / n_expand;
+    n_extra_widgets = extra_space % n_expand;
+  }
+
+  /* Allocate sizes */
+  for (i = 0; i < n_children; i++) {
+    int allocated_size = sizes[i].minimum_size;
+
+    if (child_data[i].expand) {
+      allocated_size += size_given_to_child;
+
+      if (n_extra_widgets > 0) {
+        allocated_size++;
+        n_extra_widgets--;
+      }
+    }
+
+    child_data[i].allocated_size = allocated_size;
+  }
+}
+
+static int
+compute_line (AdwWrapLayout  *self,
+              int             for_size,
+              AllocationData *child_data,
+              int             n_children)
+{
+  int n_line_children, remaining_space;
+
+  g_assert (n_children > 0);
+
+  /* Count how many widgets can fit into this line */
+  n_line_children = count_line_children (self, for_size, child_data,
+                                         n_children, &remaining_space);
+
+  /* Even one widget doesn't fit. Since we can't have a line with 0 widgets,
+   * we take the first one and allocate it out of bounds. Since this only
+   * happens with for_size == -1 or when allocating less than minimum width,
+   * it's acceptable. */
+  if (n_line_children == 0) {
+    child_data[0].allocated_size = MAX (for_size, child_data[0].minimum_size);
+
+    return 1;
+  }
+
+  /* All widgets fit, we can calculate their exact sizes within the line. */
+  box_allocate (child_data, n_line_children, for_size, self->spacing);
+
+  return n_line_children;
+}
+
+static void
+compute_sizes (AdwWrapLayout   *self,
+               GtkWidget       *widget,
+               int              for_size,
+               AllocationData **child_allocation,
+               AllocationData **line_allocation,
+               int             *n_lines)
+{
+  AllocationData *child_data, *line_data, *line_start;
+  GtkWidget *child;
+  int n_visible_children = 0;
+  int i = 0, j;
+  GtkOrientation opposite_orientation;
+
+  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+    opposite_orientation = GTK_ORIENTATION_VERTICAL;
+  else
+    opposite_orientation = GTK_ORIENTATION_HORIZONTAL;
+
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child)) {
+    if (!gtk_widget_should_layout (child))
+      continue;
+
+    n_visible_children++;
+  }
+
+  child_data = g_new0 (AllocationData, n_visible_children);
+
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child)) {
+    if (!gtk_widget_should_layout (child))
+      continue;
+
+    gtk_widget_measure (child, self->orientation, -1,
+                        &child_data[i].minimum_size,
+                        &child_data[i].natural_size,
+                        NULL, NULL);
+
+    child_data[i].expand = gtk_widget_compute_expand (child, self->orientation);
+    child_data[i].data.widget = child;
+    i++;
+  }
+
+  *n_lines = count_lines (self, for_size, child_data, n_visible_children);
+  line_data = g_new0 (AllocationData, *n_lines);
+  line_start = child_data;
+
+  for (i = 0; i < *n_lines; i++) {
+    int line_min = 0, line_nat = 0;
+    int n_line_children;
+    gboolean expand = FALSE;
+
+    n_line_children = compute_line (self, for_size, line_start, n_visible_children);
+
+    g_assert (n_line_children > 0);
+
+    for (j = 0; j < n_line_children; j++) {
+      int child_min = 0, child_nat = 0;
+
+      gtk_widget_measure (line_start[j].data.widget,
+                          opposite_orientation,
+                          line_start[j].allocated_size,
+                          &child_min, &child_nat, NULL, NULL);
+
+      expand = expand || gtk_widget_compute_expand (line_start[j].data.widget,
+                                                    opposite_orientation);
+
+      line_min = MAX (line_min, child_min);
+      line_nat = MAX (line_nat, child_nat);
+    }
+
+    line_data[i].minimum_size = line_min;
+    line_data[i].natural_size = line_nat;
+    line_data[i].expand = expand;
+    line_data[i].data.line.children = line_start;
+    line_data[i].data.line.n_children = n_line_children;
+
+    n_visible_children -= n_line_children;
+    line_start = &line_start[n_line_children];
+  }
+
+  *child_allocation = child_data;
+  *line_allocation = line_data;
+}
+
+static void
+adw_wrap_layout_measure (GtkLayoutManager *manager,
+                         GtkWidget        *widget,
+                         GtkOrientation    orientation,
+                         int               for_size,
+                         int              *minimum,
+                         int              *natural,
+                         int              *minimum_baseline,
+                         int              *natural_baseline)
+{
+  AdwWrapLayout *self = ADW_WRAP_LAYOUT (manager);
+  GtkWidget *child;
+  int min = 0, nat = 0;
+
+  if (self->orientation == orientation) {
+    min -= self->spacing;
+
+    for (child = gtk_widget_get_first_child (widget);
+         child != NULL;
+         child = gtk_widget_get_next_sibling (child)) {
+      int child_min, child_nat;
+
+      if (!gtk_widget_should_layout (child))
+        continue;
+
+      gtk_widget_measure (child, orientation, -1,
+                          &child_min, &child_nat, NULL, NULL);
+
+      min = MAX (min, child_min);
+      nat += child_nat + self->spacing;
+    }
+  } else {
+    g_autofree AllocationData *child_data = NULL;
+    g_autofree AllocationData *line_data = NULL;
+    int i, n_lines;
+
+    compute_sizes (self, widget, for_size, &child_data, &line_data, &n_lines);
+
+    for (i = 0; i < n_lines; i++) {
+      min += line_data[i].minimum_size;
+      nat += line_data[i].natural_size;
+    }
+
+    min += self->line_spacing * (n_lines - 1);
+    nat += self->line_spacing * (n_lines - 1);
+  }
+
+  if (minimum)
+    *minimum = min;
+  if (natural)
+    *natural = nat;
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+allocate_line (AdwWrapLayout  *self,
+               int             width,
+               gboolean        is_rtl,
+               gboolean        horiz,
+               AllocationData *line_child_data,
+               int             n_children,
+               int             line_size,
+               int             line_offset)
+{
+  int i, widget_offset = 0;
+
+  if (is_rtl && horiz)
+    widget_offset = width + self->spacing;
+
+  for (i = 0; i < n_children; i++) {
+    GtkWidget *widget = line_child_data[i].data.widget;
+    int widget_size = line_child_data[i].allocated_size;
+    GskTransform *transform;
+    int x, y, w, h;
+
+    if (is_rtl && horiz)
+      widget_offset -= widget_size + self->spacing;
+
+    if (horiz) {
+      x = widget_offset;
+      y = line_offset;
+      w = widget_size;
+      h = line_size;
+    } else {
+      x = line_offset;
+      y = widget_offset;
+      w = line_size;
+      h = widget_size;
+    }
+
+    transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (x, y));
+    gtk_widget_allocate (widget, w, h, -1, transform);
+
+    if (!is_rtl || !horiz)
+      widget_offset += widget_size + self->spacing;
+  }
+}
+
+static void
+adw_wrap_layout_allocate (GtkLayoutManager *manager,
+                          GtkWidget        *widget,
+                          int               width,
+                          int               height,
+                          int               baseline)
+{
+  AdwWrapLayout *self = ADW_WRAP_LAYOUT (manager);
+  g_autofree AllocationData *child_data = NULL;
+  g_autofree AllocationData *line_data = NULL;
+  int n_lines;
+  gboolean horiz = self->orientation == GTK_ORIENTATION_HORIZONTAL;
+  gboolean is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+  int i, line_pos = 0;
+
+  if (is_rtl && !horiz)
+    line_pos = width + self->line_spacing;
+
+  compute_sizes (self, widget, horiz ? width : height,
+                 &child_data, &line_data, &n_lines);
+  box_allocate (line_data, n_lines,
+                horiz ? height : width, self->line_spacing);
+
+  for (i = 0; i < n_lines; i++) {
+    if (is_rtl && !horiz)
+      line_pos -= line_data[i].allocated_size + self->line_spacing;
+
+    allocate_line (self, width, is_rtl, horiz,
+                   line_data[i].data.line.children,
+                   line_data[i].data.line.n_children,
+                   line_data[i].allocated_size, line_pos);
+
+    if (!is_rtl || horiz)
+      line_pos += line_data[i].allocated_size + self->line_spacing;
+  }
+}
+
+static GtkSizeRequestMode
+adw_wrap_layout_get_request_mode (GtkLayoutManager *manager,
+                                  GtkWidget        *widget)
+{
+  AdwWrapLayout *self = ADW_WRAP_LAYOUT (manager);
+
+  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+    return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+
+  return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+}
+
+static void
+adw_wrap_layout_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  AdwWrapLayout *self = ADW_WRAP_LAYOUT (object);
+
+  switch (prop_id) {
+  case PROP_SPACING:
+    g_value_set_int (value, adw_wrap_layout_get_spacing (self));
+    break;
+  case PROP_LINE_SPACING:
+    g_value_set_int (value, adw_wrap_layout_get_line_spacing (self));
+    break;
+  case PROP_ORIENTATION:
+    g_value_set_enum (value, self->orientation);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_wrap_layout_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  AdwWrapLayout *self = ADW_WRAP_LAYOUT (object);
+
+  switch (prop_id) {
+  case PROP_SPACING:
+    adw_wrap_layout_set_spacing (self, g_value_get_int (value));
+    break;
+  case PROP_LINE_SPACING:
+    adw_wrap_layout_set_line_spacing (self, g_value_get_int (value));
+    break;
+  case PROP_ORIENTATION:
+    set_orientation (self, g_value_get_enum (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_wrap_layout_class_init (AdwWrapLayoutClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+
+  object_class->get_property = adw_wrap_layout_get_property;
+  object_class->set_property = adw_wrap_layout_set_property;
+
+  layout_manager_class->measure = adw_wrap_layout_measure;
+  layout_manager_class->allocate = adw_wrap_layout_allocate;
+  layout_manager_class->get_request_mode = adw_wrap_layout_get_request_mode;
+
+  g_object_class_override_property (object_class,
+                                    PROP_ORIENTATION,
+                                    "orientation");
+
+  /**
+   * AdwWrapLayout:spacing: (attributes org.gtk.Property.get=adw_wrap_layout_get_spacing 
org.gtk.Property.set=adw_wrap_layout_set_spacing)
+   *
+   * The spacing between widgets on the same line.
+   *
+   * Since: 1.0
+   */
+  props[PROP_SPACING] =
+    g_param_spec_int ("spacing",
+                      "Spacing",
+                      "The spacing between widgets on the same line",
+                      0, G_MAXINT, 0,
+                      G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwWrapLayout:line-spacing: (attributes org.gtk.Property.get=adw_wrap_layout_get_line_spacing 
org.gtk.Property.set=adw_wrap_layout_set_line_spacing)
+   *
+   * The spacing between lines.
+   *
+   * Since: 1.0
+   */
+  props[PROP_LINE_SPACING] =
+    g_param_spec_int ("line-spacing",
+                      "Line spacing",
+                      "The spacing between lines",
+                      0, G_MAXINT, 0,
+                      G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+adw_wrap_layout_init (AdwWrapLayout *self)
+{
+}
+
+/**
+ * adw_wrap_layout_new:
+ *
+ * Creates a new `AdwWrapLayout`.
+ *
+ * Returns: the newly created `AdwWrapLayout`
+ *
+ * Since: 1.0
+ */
+GtkLayoutManager *
+adw_wrap_layout_new (void)
+{
+  return g_object_new (ADW_TYPE_WRAP_LAYOUT, NULL);
+}
+
+/**
+ * adw_wrap_layout_get_spacing: (attributes org.gtk.Method.get_property=spacing)
+ * @self: a `AdwWrapLayout`
+ *
+ * Gets spacing between widgets on the same line.
+ *
+ * Returns: spacing between widgets on the same line
+ *
+ * Since: 1.0
+ */
+int
+adw_wrap_layout_get_spacing (AdwWrapLayout *self)
+{
+  g_return_val_if_fail (ADW_IS_WRAP_LAYOUT (self), 0);
+
+  return self->spacing;
+}
+
+/**
+ * adw_wrap_layout_set_spacing: (attributes org.gtk.Method.set_property=spacing)
+ * @self: a `AdwWrapLayout`
+ * @spacing: the spacing
+ *
+ * Sets the spacing between widgets on the same line.
+ *
+ * Since: 1.0
+ */
+void
+adw_wrap_layout_set_spacing (AdwWrapLayout *self,
+                             int            spacing)
+{
+  g_return_if_fail (ADW_IS_WRAP_LAYOUT (self));
+
+  if (spacing < 0)
+    spacing = 0;
+
+  if (spacing == self->spacing)
+    return;
+
+  self->spacing = spacing;
+
+  gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+/**
+ * adw_wrap_layout_get_line_spacing: (attributes org.gtk.Method.get_property=line-spacing)
+ * @self: a `AdwWrapLayout`
+ *
+ * Gets the spacing between lines.
+ *
+ * Returns: the line spacing
+ *
+ * Since: 1.0
+ */
+int
+adw_wrap_layout_get_line_spacing (AdwWrapLayout *self)
+{
+  g_return_val_if_fail (ADW_IS_WRAP_LAYOUT (self), 0);
+
+  return self->line_spacing;
+}
+
+/**
+ * adw_wrap_layout_set_line_spacing: (attributes org.gtk.Method.set_property=line-spacing)
+ * @self: a `AdwWrapLayout`
+ * @line_spacing: the line spacing
+ *
+ * Sets the spacing between lines.
+ *
+ * Since: 1.0
+ */
+void
+adw_wrap_layout_set_line_spacing (AdwWrapLayout *self,
+                                  int            line_spacing)
+{
+  g_return_if_fail (ADW_IS_WRAP_LAYOUT (self));
+
+  if (line_spacing < 0)
+    line_spacing = 0;
+
+  if (line_spacing == self->line_spacing)
+    return;
+
+  self->line_spacing = line_spacing;
+
+  gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LINE_SPACING]);
+}
diff --git a/src/adw-wrap-layout.h b/src/adw-wrap-layout.h
new file mode 100644
index 00000000..0ecfacd0
--- /dev/null
+++ b/src/adw-wrap-layout.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_WRAP_LAYOUT (adw_wrap_layout_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwWrapLayout, adw_wrap_layout, ADW, WRAP_LAYOUT, GtkLayoutManager)
+
+ADW_AVAILABLE_IN_ALL
+GtkLayoutManager *adw_wrap_layout_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_ALL
+int  adw_wrap_layout_get_spacing (AdwWrapLayout *self);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_layout_set_spacing (AdwWrapLayout *self,
+                                  int            spacing);
+
+ADW_AVAILABLE_IN_ALL
+int  adw_wrap_layout_get_line_spacing (AdwWrapLayout *self);
+ADW_AVAILABLE_IN_ALL
+void adw_wrap_layout_set_line_spacing (AdwWrapLayout *self,
+                                       int            line_spacing);
+
+G_END_DECLS
diff --git a/src/adwaita.h b/src/adwaita.h
index 9879d538..d8914392 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -61,6 +61,8 @@ G_BEGIN_DECLS
 #include "adw-view-switcher-title.h"
 #include "adw-window.h"
 #include "adw-window-title.h"
+#include "adw-wrap-box.h"
+#include "adw-wrap-layout.h"
 
 #undef _ADWAITA_INSIDE
 
diff --git a/src/meson.build b/src/meson.build
index d0b1c90c..aec3ef73 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -107,6 +107,8 @@ src_headers = [
   'adw-view-switcher-title.h',
   'adw-window.h',
   'adw-window-title.h',
+  'adw-wrap-box.h',
+  'adw-wrap-layout.h',
 ]
 
 sed = find_program('sed', required: true)
@@ -171,6 +173,8 @@ src_sources = [
   'adw-window.c',
   'adw-window-mixin.c',
   'adw-window-title.c',
+  'adw-wrap-box.c',
+  'adw-wrap-layout.c',
 ]
 
 libadwaita_public_headers += files(src_headers)


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