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 + * + * 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 . + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include + +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 + * + * 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 . + * + * 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 + * + * 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 . + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include + +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 + * + * 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 . + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#include "config.h" + +#include + +#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 + * + * 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 . + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include + +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 + * + * 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 . + * + * 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',