[libadwaita/wip/exalm/wrapbox] Draft: AdwWrapLayout/Box
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/wrapbox] Draft: AdwWrapLayout/Box
- Date: Fri, 6 Aug 2021 07:54:48 +0000 (UTC)
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]