diff options
author | Matthias Clasen <mclasen@redhat.com> | 2019-07-02 14:01:01 +0000 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2019-07-02 14:01:01 +0000 |
commit | b2f15a622db1c74f86ef0e10ebbc0cb8a8bc5f91 (patch) | |
tree | ff70b2a1e25b92a78c0f448d62653eb6b4e5428b | |
parent | 33bd7051f24862f80cb8149a79090df350b28855 (diff) | |
parent | 04aaf02881acee3e603835ac1062f6525de096c8 (diff) | |
download | gtk+-b2f15a622db1c74f86ef0e10ebbc0cb8a8bc5f91.tar.gz |
Merge branch 'wip/ebassi/constraint-layout' into 'master'
Add constraint-based layout manager
Closes #1090
See merge request GNOME/gtk!973
60 files changed, 13696 insertions, 16 deletions
diff --git a/demos/constraint-editor/constraint-editor-application.c b/demos/constraint-editor/constraint-editor-application.c new file mode 100644 index 0000000000..09b5d64270 --- /dev/null +++ b/demos/constraint-editor/constraint-editor-application.c @@ -0,0 +1,94 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#include "config.h" + +#include "constraint-editor-application.h" +#include "constraint-editor-window.h" + +struct _ConstraintEditorApplication +{ + GtkApplication parent_instance; +}; + +G_DEFINE_TYPE(ConstraintEditorApplication, constraint_editor_application, GTK_TYPE_APPLICATION); + +static void +constraint_editor_application_init (ConstraintEditorApplication *app) +{ +} + +static void +quit_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + g_application_quit (G_APPLICATION (data)); +} + +static GActionEntry app_entries[] = +{ + { "quit", quit_activated, NULL, NULL, NULL } +}; + +static void +constraint_editor_application_startup (GApplication *app) +{ + const char *quit_accels[2] = { "<Ctrl>Q", NULL }; + GtkCssProvider *provider; + + G_APPLICATION_CLASS (constraint_editor_application_parent_class)->startup (app); + + g_action_map_add_action_entries (G_ACTION_MAP (app), + app_entries, G_N_ELEMENTS (app_entries), + app); + gtk_application_set_accels_for_action (GTK_APPLICATION (app), "app.quit", quit_accels); + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, "/org/gtk/gtk4/constraint-editor/constraint-editor.css"); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +static void +constraint_editor_application_activate (GApplication *app) +{ + ConstraintEditorWindow *win; + + win = constraint_editor_window_new (CONSTRAINT_EDITOR_APPLICATION (app)); + gtk_window_present (GTK_WINDOW (win)); +} + +static void +constraint_editor_application_class_init (ConstraintEditorApplicationClass *class) +{ + GApplicationClass *application_class = G_APPLICATION_CLASS (class); + + application_class->startup = constraint_editor_application_startup; + application_class->activate = constraint_editor_application_activate; +} + +ConstraintEditorApplication * +constraint_editor_application_new (void) +{ + return g_object_new (CONSTRAINT_EDITOR_APPLICATION_TYPE, + "application-id", "org.gtk.gtk4.ConstraintEditor", + NULL); +} diff --git a/demos/constraint-editor/constraint-editor-application.h b/demos/constraint-editor/constraint-editor-application.h new file mode 100644 index 0000000000..c7d9fd3048 --- /dev/null +++ b/demos/constraint-editor/constraint-editor-application.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#define CONSTRAINT_EDITOR_APPLICATION_TYPE (constraint_editor_application_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintEditorApplication, constraint_editor_application, CONSTRAINT, EDITOR_APPLICATION, GtkApplication) + +ConstraintEditorApplication *constraint_editor_application_new (void); diff --git a/demos/constraint-editor/constraint-editor-window.c b/demos/constraint-editor/constraint-editor-window.c new file mode 100644 index 0000000000..a8fa33464d --- /dev/null +++ b/demos/constraint-editor/constraint-editor-window.c @@ -0,0 +1,354 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#include "config.h" + +#include "constraint-editor-window.h" +#include "constraint-view.h" +#include "constraint-editor.h" +#include "guide-editor.h" + +struct _ConstraintEditorWindow +{ + GtkApplicationWindow parent_instance; + + GtkWidget *paned; + GtkWidget *view; + GtkWidget *list; +}; + +G_DEFINE_TYPE(ConstraintEditorWindow, constraint_editor_window, GTK_TYPE_APPLICATION_WINDOW); + +gboolean +constraint_editor_window_load (ConstraintEditorWindow *self, + GFile *file) +{ + GBytes *bytes; + + bytes = g_file_load_bytes (file, NULL, NULL, NULL); + if (bytes == NULL) + return FALSE; + + if (!g_utf8_validate (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL)) + { + g_bytes_unref (bytes); + return FALSE; + } + +#if 0 + + gtk_text_buffer_get_end_iter (self->text_buffer, &end); + gtk_text_buffer_insert (self->text_buffer, + &end, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes)); +#endif + + g_bytes_unref (bytes); + + return TRUE; +} + +static void +constraint_editor_window_finalize (GObject *object) +{ + //ConstraintEditorWindow *self = (ConstraintEditorWindow *)object; + + G_OBJECT_CLASS (constraint_editor_window_parent_class)->finalize (object); +} + +static int child_counter; +static int guide_counter; + +static void +add_child (ConstraintEditorWindow *win) +{ + char *name; + + child_counter++; + name = g_strdup_printf ("Child %d", child_counter); + constraint_view_add_child (CONSTRAINT_VIEW (win->view), name); + g_free (name); +} + +static void +add_guide (ConstraintEditorWindow *win) +{ + char *name; + GtkConstraintGuide *guide; + + guide_counter++; + name = g_strdup_printf ("Guide %d", guide_counter); + guide = g_object_new (GTK_TYPE_CONSTRAINT_GUIDE, NULL); + g_object_set_data_full (G_OBJECT (guide), "name", name, g_free); + + constraint_view_add_guide (CONSTRAINT_VIEW (win->view), guide); +} + +static void +constraint_editor_done (ConstraintEditor *editor, + GtkConstraint *constraint, + ConstraintEditorWindow *win) +{ + GtkConstraint *old_constraint; + + g_object_get (editor, "constraint", &old_constraint, NULL); + + if (old_constraint) + constraint_view_remove_constraint (CONSTRAINT_VIEW (win->view), old_constraint); + + constraint_view_add_constraint (CONSTRAINT_VIEW (win->view), constraint); + + g_clear_object (&old_constraint); + + gtk_widget_destroy (gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW)); +} + +static void +edit_constraint (ConstraintEditorWindow *win, + GtkConstraint *constraint) +{ + GtkWidget *window; + ConstraintEditor *editor; + GListModel *model; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (win)); + gtk_window_set_resizable (GTK_WINDOW (window), FALSE); + if (constraint) + gtk_window_set_title (GTK_WINDOW (window), "Edit Constraint"); + else + gtk_window_set_title (GTK_WINDOW (window), "Create Constraint"); + + model = constraint_view_get_model (CONSTRAINT_VIEW (win->view)); + + editor = constraint_editor_new (model, constraint); + + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (editor)); + + g_signal_connect (editor, "done", G_CALLBACK (constraint_editor_done), win); + + gtk_widget_show (window); +} + +static void +add_constraint (ConstraintEditorWindow *win) +{ + edit_constraint (win, NULL); +} + +static void +guide_editor_done (GuideEditor *editor, + GtkConstraintGuide *guide, + ConstraintEditorWindow *win) +{ + constraint_view_guide_changed (CONSTRAINT_VIEW (win->view), guide); + gtk_widget_destroy (gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW)); +} + +static void +edit_guide (ConstraintEditorWindow *win, + GtkConstraintGuide *guide) +{ + GtkWidget *window; + GuideEditor *editor; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_resizable (GTK_WINDOW (window), FALSE); + gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (win)); + gtk_window_set_title (GTK_WINDOW (window), "Edit Guide"); + + editor = guide_editor_new (guide); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (editor)); + + g_signal_connect (editor, "done", G_CALLBACK (guide_editor_done), win); + gtk_widget_show (window); +} + +static void +row_activated (GtkListBox *list, + GtkListBoxRow *row, + ConstraintEditorWindow *win) +{ + GObject *item; + + item = G_OBJECT (g_object_get_data (G_OBJECT (row), "item")); + + if (GTK_IS_CONSTRAINT (item)) + edit_constraint (win, GTK_CONSTRAINT (item)); + else if (GTK_IS_CONSTRAINT_GUIDE (item)) + edit_guide (win, GTK_CONSTRAINT_GUIDE (item)); +} + +static void +constraint_editor_window_class_init (ConstraintEditorWindowClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + g_type_ensure (CONSTRAINT_VIEW_TYPE); + + object_class->finalize = constraint_editor_window_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gtk/gtk4/constraint-editor/constraint-editor-window.ui"); + + gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, paned); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, view); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, list); + + gtk_widget_class_bind_template_callback (widget_class, add_child); + gtk_widget_class_bind_template_callback (widget_class, add_guide); + gtk_widget_class_bind_template_callback (widget_class, add_constraint); + gtk_widget_class_bind_template_callback (widget_class, row_activated); +} + +static void +row_edit (GtkButton *button, + ConstraintEditorWindow *win) +{ + GtkWidget *row; + GObject *item; + + row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW); + item = (GObject *)g_object_get_data (G_OBJECT (row), "item"); + if (GTK_IS_CONSTRAINT (item)) + edit_constraint (win, GTK_CONSTRAINT (item)); + else if (GTK_IS_CONSTRAINT_GUIDE (item)) + edit_guide (win, GTK_CONSTRAINT_GUIDE (item)); +} + +static void +mark_constraints_invalid (ConstraintEditorWindow *win, + gpointer removed) +{ + GtkWidget *child; + GObject *item; + + for (child = gtk_widget_get_first_child (win->list); + child; + child = gtk_widget_get_next_sibling (child)) + { + item = (GObject *)g_object_get_data (G_OBJECT (child), "item"); + if (GTK_IS_CONSTRAINT (item)) + { + GtkConstraint *constraint = GTK_CONSTRAINT (item); + + if (gtk_constraint_get_target (constraint) == (GtkConstraintTarget *)removed || + gtk_constraint_get_source (constraint) == (GtkConstraintTarget *)removed) + { + GtkWidget *button; + button = (GtkWidget *)g_object_get_data (G_OBJECT (child), "edit"); + gtk_button_set_icon_name (GTK_BUTTON (button), "dialog-warning-symbolic"); + gtk_widget_set_tooltip_text (button, "Constraint is invalid"); + } + } + } +} + +static void +row_delete (GtkButton *button, + ConstraintEditorWindow *win) +{ + GtkWidget *row; + GObject *item; + + row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW); + item = (GObject *)g_object_get_data (G_OBJECT (row), "item"); + if (GTK_IS_CONSTRAINT (item)) + constraint_view_remove_constraint (CONSTRAINT_VIEW (win->view), + GTK_CONSTRAINT (item)); + else if (GTK_IS_CONSTRAINT_GUIDE (item)) + { + mark_constraints_invalid (win, item); + constraint_view_remove_guide (CONSTRAINT_VIEW (win->view), + GTK_CONSTRAINT_GUIDE (item)); + } + else if (GTK_IS_WIDGET (item)) + { + mark_constraints_invalid (win, item); + constraint_view_remove_child (CONSTRAINT_VIEW (win->view), + GTK_WIDGET (item)); + } +} + +static GtkWidget * +create_widget_func (gpointer item, + gpointer user_data) +{ + ConstraintEditorWindow *win = user_data; + const char *name; + GtkWidget *row, *box, *label, *button; + + name = (const char *)g_object_get_data (G_OBJECT (item), "name"); + + row = gtk_list_box_row_new (); + g_object_set_data (G_OBJECT (row), "item", item); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + label = gtk_label_new (name); + g_object_set (label, + "margin", 10, + NULL); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_set_hexpand (label, TRUE); + gtk_container_add (GTK_CONTAINER (row), box); + gtk_container_add (GTK_CONTAINER (box), label); + + if (GTK_IS_CONSTRAINT (item) || GTK_IS_CONSTRAINT_GUIDE (item)) + { + button = gtk_button_new_from_icon_name ("document-edit-symbolic"); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + g_signal_connect (button, "clicked", G_CALLBACK (row_edit), win); + g_object_set_data (G_OBJECT (row), "edit", button); + gtk_container_add (GTK_CONTAINER (box), button); + button = gtk_button_new_from_icon_name ("edit-delete-symbolic"); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + g_signal_connect (button, "clicked", G_CALLBACK (row_delete), win); + gtk_container_add (GTK_CONTAINER (box), button); + } + else if (GTK_IS_WIDGET (item)) + { + button = gtk_button_new_from_icon_name ("edit-delete-symbolic"); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + g_signal_connect (button, "clicked", G_CALLBACK (row_delete), win); + gtk_container_add (GTK_CONTAINER (box), button); + } + + return row; +} + +static void +constraint_editor_window_init (ConstraintEditorWindow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_bind_model (GTK_LIST_BOX (self->list), + constraint_view_get_model (CONSTRAINT_VIEW (self->view)), + create_widget_func, + self, + NULL); +} + +ConstraintEditorWindow * +constraint_editor_window_new (ConstraintEditorApplication *application) +{ + return g_object_new (CONSTRAINT_EDITOR_WINDOW_TYPE, + "application", application, + NULL); +} diff --git a/demos/constraint-editor/constraint-editor-window.h b/demos/constraint-editor/constraint-editor-window.h new file mode 100644 index 0000000000..9096c6e71d --- /dev/null +++ b/demos/constraint-editor/constraint-editor-window.h @@ -0,0 +1,34 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "constraint-editor-application.h" + + +#define CONSTRAINT_EDITOR_WINDOW_TYPE (constraint_editor_window_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintEditorWindow, constraint_editor_window, CONSTRAINT, EDITOR_WINDOW, GtkApplicationWindow) + +ConstraintEditorWindow * constraint_editor_window_new (ConstraintEditorApplication *application); + +gboolean constraint_editor_window_load (ConstraintEditorWindow *self, + GFile *file); diff --git a/demos/constraint-editor/constraint-editor-window.ui b/demos/constraint-editor/constraint-editor-window.ui new file mode 100644 index 0000000000..c2c9a009ec --- /dev/null +++ b/demos/constraint-editor/constraint-editor-window.ui @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ConstraintEditorWindow" parent="GtkApplicationWindow"> + <style> + <class name="devel"/> + </style> + <property name="title" translatable="yes">GTK Constraint Editor</property> + <property name="default-width">1024</property> + <property name="default-height">768</property> + <child type="titlebar"> + <object class="GtkHeaderBar" id="header"> + <property name="title" translatable="yes">GTK Constraint Editor</property> + <property name="show-title-buttons">1</property> + </object> + </child> + <child> + <object class="GtkPaned" id="paned"> + <property name="orientation">horizontal</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="orientation">horizontal</property> + <child> + <object class="GtkButton"> + <property name="label">Add Child</property> + <signal name="clicked" handler="add_child" swapped="yes"/> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="label">Add Guide</property> + <signal name="clicked" handler="add_guide" swapped="yes"/> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="label">Add Constraint</property> + <signal name="clicked" handler="add_constraint" swapped="yes"/> + </object> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="hscrollbar-policy">never</property> + <property name="vscrollbar-policy">automatic</property> + <property name="vexpand">1</property> + <child> + <object class="GtkListBox" id="list"> + <property name="show-separators">1</property> + <property name="selection-mode">none</property> + <signal name="row-activated" handler="row_activated"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="ConstraintView" id="view"> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/demos/constraint-editor/constraint-editor.c b/demos/constraint-editor/constraint-editor.c new file mode 100644 index 0000000000..5a85b35ecf --- /dev/null +++ b/demos/constraint-editor/constraint-editor.c @@ -0,0 +1,583 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#include "config.h" + +#include "constraint-editor.h" + +struct _ConstraintEditor +{ + GtkWidget parent_instance; + + GtkWidget *grid; + GtkWidget *name; + GtkWidget *target; + GtkWidget *target_attr; + GtkWidget *relation; + GtkWidget *source; + GtkWidget *source_attr; + GtkWidget *multiplier; + GtkWidget *constant; + GtkWidget *strength; + GtkWidget *preview; + GtkWidget *button; + + GtkConstraint *constraint; + GListModel *model; + + gboolean constructed; +}; + +enum { + PROP_MODEL = 1, + PROP_CONSTRAINT, + LAST_PROP +}; + +static GParamSpec *pspecs[LAST_PROP]; + +enum { + DONE, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE(ConstraintEditor, constraint_editor, GTK_TYPE_WIDGET); + +static const char * +get_target_name (GtkConstraintTarget *target) +{ + if (target == NULL) + return "super"; + else + return (const char *)g_object_get_data (G_OBJECT (target), "name"); +} + +static void +constraint_target_combo (GListModel *model, + GtkWidget *combo, + gboolean is_source) +{ + int i; + + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "super", "Super"); + + if (model) + { + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + GObject *item = g_list_model_get_object (model, i); + const char *name; + + if (GTK_IS_CONSTRAINT (item)) + continue; + + name = get_target_name (GTK_CONSTRAINT_TARGET (item)); + + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), name, name); + g_object_unref (item); + } + } +} + +static void +constraint_attribute_combo (GtkWidget *combo, + gboolean is_source) +{ + if (is_source) + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "none", "None"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "left", "Left"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "right", "Right"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "top", "Top"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "bottom", "Bottom"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "start", "Start"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "end", "End"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "width", "Width"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "height", "Height"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "center-x", "Center X"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "center-y", "Center Y"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "baseline", "Baseline"); +} + +static void +constraint_relation_combo (GtkWidget *combo) +{ + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "le", "≤"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "eq", "="); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "ge", "≥"); +} + +static void +constraint_strength_combo (GtkWidget *combo) +{ + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "weak", "Weak"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "medium", "Medium"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "strong", "Strong"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "required", "Required"); +} + +static gpointer +get_target (GListModel *model, + const char *id) +{ + int i; + + if (strcmp ("super", id) == 0) + return NULL; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + GObject *item = g_list_model_get_object (model, i); + const char *name; + if (GTK_IS_CONSTRAINT (item)) + continue; + name = (const char *)g_object_get_data (item, "name"); + g_object_unref (item); + if (strcmp (name, id) == 0) + return item; + } + + return NULL; +} + +static GtkConstraintAttribute +get_target_attr (const char *id) +{ + GtkConstraintAttribute attr; + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_ATTRIBUTE); + GEnumValue *value = g_enum_get_value_by_nick (class, id); + attr = value->value; + g_type_class_unref (class); + + return attr; +} + +static const char * +get_attr_nick (GtkConstraintAttribute attr) +{ + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_ATTRIBUTE); + GEnumValue *value = g_enum_get_value (class, attr); + const char *nick = value->value_nick; + g_type_class_unref (class); + + return nick; +} + +static GtkConstraintRelation +get_relation (const char *id) +{ + GtkConstraintRelation relation; + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_RELATION); + GEnumValue *value = g_enum_get_value_by_nick (class, id); + relation = value->value; + g_type_class_unref (class); + + return relation; +} + +static const char * +get_relation_nick (GtkConstraintRelation relation) +{ + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_RELATION); + GEnumValue *value = g_enum_get_value (class, relation); + const char *nick = value->value_nick; + g_type_class_unref (class); + + return nick; +} + +static GtkConstraintStrength +get_strength (const char *id) +{ + GtkConstraintStrength strength; + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_STRENGTH); + GEnumValue *value = g_enum_get_value_by_nick (class, id); + strength = value->value; + g_type_class_unref (class); + + return strength; +} + +static const char * +get_strength_nick (GtkConstraintStrength strength) +{ + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_STRENGTH); + GEnumValue *value = g_enum_get_value (class, strength); + const char *nick = value->value_nick; + g_type_class_unref (class); + + return nick; +} + +static void +create_constraint (GtkButton *button, + ConstraintEditor *editor) +{ + const char *id; + gpointer target; + GtkConstraintAttribute target_attr; + gpointer source; + GtkConstraintAttribute source_attr; + GtkConstraintRelation relation; + double multiplier; + double constant; + int strength; + GtkConstraint *constraint; + const char *name; + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->target)); + target = get_target (editor->model, id); + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->target_attr)); + target_attr = get_target_attr (id); + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source)); + source = get_target (editor->model, id); + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source_attr)); + source_attr = get_target_attr (id); + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->relation)); + relation = get_relation (id); + + multiplier = g_ascii_strtod (gtk_editable_get_text (GTK_EDITABLE (editor->multiplier)), NULL); + + constant = g_ascii_strtod (gtk_editable_get_text (GTK_EDITABLE (editor->constant)), NULL); + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->strength)); + strength = get_strength (id); + + name = gtk_editable_get_text (GTK_EDITABLE (editor->name)); + + constraint = gtk_constraint_new (target, target_attr, + relation, + source, source_attr, + multiplier, + constant, + strength); + g_object_set_data_full (G_OBJECT (constraint), "name", g_strdup (name), g_free); + g_signal_emit (editor, signals[DONE], 0, constraint); + g_object_unref (constraint); +} + +static void +source_attr_changed (ConstraintEditor *editor) +{ + const char *id; + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source_attr)); + if (strcmp (id, "none") == 0) + { + gtk_combo_box_set_active (GTK_COMBO_BOX (editor->source), -1); + gtk_editable_set_text (GTK_EDITABLE (editor->multiplier), ""); + gtk_widget_set_sensitive (editor->source, FALSE); + gtk_widget_set_sensitive (editor->multiplier, FALSE); + } + else + { + gtk_widget_set_sensitive (editor->source, TRUE); + gtk_widget_set_sensitive (editor->multiplier, TRUE); + } +} + +static void +update_preview (ConstraintEditor *editor) +{ + GString *str; + const char *name; + const char *attr; + char *relation; + const char *multiplier; + const char *constant; + double c, m; + + if (!editor->constructed) + return; + + str = g_string_new (""); + + name = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->target)); + attr = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->target_attr)); + relation = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (editor->relation)); + + if (name == NULL) + name = "[ ]"; + + g_string_append_printf (str, "%s.%s %s ", name, attr, relation); + g_free (relation); + + constant = gtk_editable_get_text (GTK_EDITABLE (editor->constant)); + c = g_ascii_strtod (constant, NULL); + + attr = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source_attr)); + if (strcmp (attr, "none") != 0) + { + name = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source)); + multiplier = gtk_editable_get_text (GTK_EDITABLE (editor->multiplier)); + m = g_ascii_strtod (multiplier, NULL); + + if (name == NULL) + name = "[ ]"; + + g_string_append_printf (str, "%s.%s", name, attr); + + if (m != 1.0) + g_string_append_printf (str, " × %g", m); + + if (c > 0.0) + g_string_append_printf (str, " + %g", c); + else if (c < 0.0) + g_string_append_printf (str, " - %g", -c); + } + else + g_string_append_printf (str, "%g", c); + + gtk_label_set_label (GTK_LABEL (editor->preview), str->str); + + g_string_free (str, TRUE); +} + +static void +update_button (ConstraintEditor *editor) +{ + if (gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->target)) != NULL && + gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->source)) != NULL) + gtk_widget_set_sensitive (editor->button, TRUE); + else + gtk_widget_set_sensitive (editor->button, FALSE); +} + +static void +constraint_editor_init (ConstraintEditor *editor) +{ + gtk_widget_init_template (GTK_WIDGET (editor)); +} + +static int constraint_counter; + +static void +constraint_editor_constructed (GObject *object) +{ + ConstraintEditor *editor = CONSTRAINT_EDITOR (object); + + constraint_target_combo (editor->model, editor->target, FALSE); + constraint_attribute_combo (editor->target_attr, FALSE); + constraint_relation_combo (editor->relation); + constraint_target_combo (editor->model, editor->source, TRUE); + constraint_attribute_combo (editor->source_attr, TRUE); + + constraint_strength_combo (editor->strength); + + if (editor->constraint) + { + GtkConstraintTarget *target; + GtkConstraintAttribute attr; + GtkConstraintRelation relation; + GtkConstraintStrength strength; + const char *nick; + char *val; + double multiplier; + double constant; + + nick = (char *)g_object_get_data (G_OBJECT (editor->constraint), "name"); + gtk_editable_set_text (GTK_EDITABLE (editor->name), nick); + + target = gtk_constraint_get_target (editor->constraint); + nick = get_target_name (target); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->target), nick); + + attr = gtk_constraint_get_target_attribute (editor->constraint); + nick = get_attr_nick (attr); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->target_attr), nick); + + target = gtk_constraint_get_source (editor->constraint); + nick = get_target_name (target); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->source), nick); + + attr = gtk_constraint_get_source_attribute (editor->constraint); + nick = get_attr_nick (attr); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->source_attr), nick); + + relation = gtk_constraint_get_relation (editor->constraint); + nick = get_relation_nick (relation); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->relation), nick); + + multiplier = gtk_constraint_get_multiplier (editor->constraint); + val = g_strdup_printf ("%g", multiplier); + gtk_editable_set_text (GTK_EDITABLE (editor->multiplier), val); + g_free (val); + + constant = gtk_constraint_get_constant (editor->constraint); + val = g_strdup_printf ("%g", constant); + gtk_editable_set_text (GTK_EDITABLE (editor->constant), val); + g_free (val); + + strength = gtk_constraint_get_strength (editor->constraint); + nick = get_strength_nick (strength); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->strength), nick); + + gtk_button_set_label (GTK_BUTTON (editor->button), "Apply"); + } + else + { + char *name; + + constraint_counter++; + name = g_strdup_printf ("Constraint %d", constraint_counter); + gtk_editable_set_text (GTK_EDITABLE (editor->name), name); + g_free (name); + + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->target_attr), "left"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->source_attr), "left"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->relation), "eq"); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->strength), "required"); + + gtk_editable_set_text (GTK_EDITABLE (editor->multiplier), "1.0"); + gtk_editable_set_text (GTK_EDITABLE (editor->constant), "0.0"); + + gtk_button_set_label (GTK_BUTTON (editor->button), "Create"); + } + + editor->constructed = TRUE; + update_preview (editor); + update_button (editor); +} + +static void +constraint_editor_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + ConstraintEditor *self = CONSTRAINT_EDITOR (object); + + switch (property_id) + { + case PROP_MODEL: + self->model = g_value_dup_object (value); + break; + + case PROP_CONSTRAINT: + self->constraint = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +constraint_editor_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ConstraintEditor *self = CONSTRAINT_EDITOR (object); + + switch (property_id) + { + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + case PROP_CONSTRAINT: + g_value_set_object (value, self->constraint); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +constraint_editor_dispose (GObject *object) +{ + ConstraintEditor *self = (ConstraintEditor *)object; + + g_clear_pointer (&self->grid, gtk_widget_unparent); + g_clear_object (&self->model); + g_clear_object (&self->constraint); + + G_OBJECT_CLASS (constraint_editor_parent_class)->dispose (object); +} + +static void +constraint_editor_class_init (ConstraintEditorClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->constructed = constraint_editor_constructed; + object_class->dispose = constraint_editor_dispose; + object_class->set_property = constraint_editor_set_property; + object_class->get_property = constraint_editor_get_property; + + pspecs[PROP_CONSTRAINT] = + g_param_spec_object ("constraint", "constraint", "constraint", + GTK_TYPE_CONSTRAINT, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_MODEL] = + g_param_spec_object ("model", "model", "model", + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); + + signals[DONE] = + g_signal_new ("done", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, GTK_TYPE_CONSTRAINT); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gtk/gtk4/constraint-editor/constraint-editor.ui"); + + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, grid); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, name); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, target); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, target_attr); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, relation); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, source); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, source_attr); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, multiplier); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, constant); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, strength); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, preview); + gtk_widget_class_bind_template_child (widget_class, ConstraintEditor, button); + + gtk_widget_class_bind_template_callback (widget_class, update_preview); + gtk_widget_class_bind_template_callback (widget_class, update_button); + gtk_widget_class_bind_template_callback (widget_class, create_constraint); + gtk_widget_class_bind_template_callback (widget_class, source_attr_changed); +} + +ConstraintEditor * +constraint_editor_new (GListModel *model, + GtkConstraint *constraint) +{ + return g_object_new (CONSTRAINT_EDITOR_TYPE, + "model", model, + "constraint", constraint, + NULL); +} diff --git a/demos/constraint-editor/constraint-editor.css b/demos/constraint-editor/constraint-editor.css new file mode 100644 index 0000000000..c9538bf268 --- /dev/null +++ b/demos/constraint-editor/constraint-editor.css @@ -0,0 +1,12 @@ +constraintview { + background: black; + color: white; +} + +constraintview .child { + background: red; +} + +constraintview .guide { + background: blue; +} diff --git a/demos/constraint-editor/constraint-editor.gresource.xml b/demos/constraint-editor/constraint-editor.gresource.xml new file mode 100644 index 0000000000..e57964b1a7 --- /dev/null +++ b/demos/constraint-editor/constraint-editor.gresource.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gtk/gtk4/constraint-editor"> + <file preprocess="xml-stripblanks">constraint-editor-window.ui</file> + <file preprocess="xml-stripblanks">constraint-editor.ui</file> + <file preprocess="xml-stripblanks">guide-editor.ui</file> + <file>constraint-editor.css</file> + </gresource> +</gresources> diff --git a/demos/constraint-editor/constraint-editor.h b/demos/constraint-editor/constraint-editor.h new file mode 100644 index 0000000000..c5940e254b --- /dev/null +++ b/demos/constraint-editor/constraint-editor.h @@ -0,0 +1,29 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#define CONSTRAINT_EDITOR_TYPE (constraint_editor_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintEditor, constraint_editor, CONSTRAINT, EDITOR, GtkWidget) + +ConstraintEditor * constraint_editor_new (GListModel *model, + GtkConstraint *constraint); diff --git a/demos/constraint-editor/constraint-editor.ui b/demos/constraint-editor/constraint-editor.ui new file mode 100644 index 0000000000..604757659a --- /dev/null +++ b/demos/constraint-editor/constraint-editor.ui @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ConstraintEditor" parent="GtkWidget"> + <child> + <object class="GtkGrid" id="grid"> + <property name="margin">20</property> + <property name="row-spacing">10</property> + <property name="column-spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="label">Name</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="name"> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Target</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="target"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <signal name="changed" handler="update_button" swapped="yes"/> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="target_attr"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Relation</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="relation"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Source</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="source"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <signal name="changed" handler="update_button" swapped="yes"/> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="source_attr"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <signal name="changed" handler="source_attr_changed" swapped="yes"/> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Multiplier</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="multiplier"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Constant</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">5</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="constant"> + <signal name="changed" handler="update_preview" swapped="yes"/> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">5</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Strength</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">6</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="strength"> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">6</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="preview"> + <property name="xalign">0</property> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">7</property> + <property name="column-span">2</property> + </layout> + <attributes> + <attribute name="scale" value="1.44"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton" id="button"> + <property name="label">Create</property> + <signal name="clicked" handler="create_constraint"/> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">8</property> + </layout> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/demos/constraint-editor/constraint-view-child.c b/demos/constraint-editor/constraint-view-child.c new file mode 100644 index 0000000000..272054a387 --- /dev/null +++ b/demos/constraint-editor/constraint-view-child.c @@ -0,0 +1,93 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#include "constraint-view-child.h" + +struct _ConstraintViewChild +{ + GObject parent_instance; + + char *name; +}; + +enum { + PROP_NAME = 1, + LAST_PROP +}; + +static GParamSpec props[LAST_PROP]; + +G_DEFINE_TYPE (ConstraintViewChild, constraint_view_child, G_TYPE_OBJECT) + +static void +constraint_view_child_init (ConstraintViewChild *child) +{ +} + +static void +constraint_view_child_finalize (GObject *object) +{ + ConstraintViewChild *child = CONSTRAINT_VIEW_CHILD (object); + + g_free (child->name); + + G_OBJECT_CLASS (constraint_view_child_parent_class)->finalize (object); +} + +static void +constraint_view_child_set_property (GObject *object, + +static void +constraint_view_child_class_init (ConstraintViewChildClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = constraint_view_child_finalize; + object_class->get_property = constraint_view_child_get_property; + object_class->set_property = constraint_view_child_set_property; + + props[PROP_NAME] = + g_param_spec_string ("name", "name", "name", + NULL, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +#define CONSTRAINT_VIEW_CHILD_TYPE (constraint_view_get_type ()) + +G_DECLARE_TYPE (ConstraintViewChild, constraint_view_child, CONSTRAINT, VIEW_CHILD, GObject) + +#define CONSTRAINT_VIEW_WIDGET_TYPE (constraint_view_widget_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewWidget, constraint_view_widget, CONSTRAINT, VIEW_WIDGET, ConstraintViewChild) + +ConstraintViewWidget * constraint_view_widget_new (void); + +#define CONSTRAINT_VIEW_GUIDE_TYPE (constraint_view_guide_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewGuide, constraint_view_guide, CONSTRAINT, VIEW_GUIDE, ConstraintViewChild) + +ConstraintViewGuide * constraint_view_guide_new (void); + +#define CONSTRAINT_VIEW_CONSTRAINT_TYPE (constraint_view_constraint_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewConstraint, constraint_view_constraint, CONSTRAINT, VIEW_CONSTRAINT, ConstraintViewChild) + +ConstraintViewGuide * constraint_view_constraint_new (void); diff --git a/demos/constraint-editor/constraint-view-child.h b/demos/constraint-editor/constraint-view-child.h new file mode 100644 index 0000000000..6f2120ef0b --- /dev/null +++ b/demos/constraint-editor/constraint-view-child.h @@ -0,0 +1,44 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#define CONSTRAINT_VIEW_CHILD_TYPE (constraint_view_get_type ()) + +G_DECLARE_TYPE (ConstraintViewChild, constraint_view_child, CONSTRAINT, VIEW_CHILD, GObject) + +#define CONSTRAINT_VIEW_WIDGET_TYPE (constraint_view_widget_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewWidget, constraint_view_widget, CONSTRAINT, VIEW_WIDGET, ConstraintViewChild) + +ConstraintViewWidget * constraint_view_widget_new (void); + +#define CONSTRAINT_VIEW_GUIDE_TYPE (constraint_view_guide_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewGuide, constraint_view_guide, CONSTRAINT, VIEW_GUIDE, ConstraintViewChild) + +ConstraintViewGuide * constraint_view_guide_new (void); + +#define CONSTRAINT_VIEW_CONSTRAINT_TYPE (constraint_view_constraint_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintViewConstraint, constraint_view_constraint, CONSTRAINT, VIEW_CONSTRAINT, ConstraintViewChild) + +ConstraintViewGuide * constraint_view_constraint_new (void); diff --git a/demos/constraint-editor/constraint-view.c b/demos/constraint-editor/constraint-view.c new file mode 100644 index 0000000000..7a4b094df5 --- /dev/null +++ b/demos/constraint-editor/constraint-view.c @@ -0,0 +1,378 @@ +/* Copyright (C) 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <gtk/gtk.h> +#include "constraint-view.h" + +struct _ConstraintView +{ + GtkWidget parent; + + GListStore *store; + + GtkWidget *drag_widget; +}; + +G_DEFINE_TYPE (ConstraintView, constraint_view, GTK_TYPE_WIDGET); + +static void +constraint_view_dispose (GObject *object) +{ + ConstraintView *view = CONSTRAINT_VIEW (object); + GtkWidget *child; + + while ((child = gtk_widget_get_first_child (GTK_WIDGET (view))) != NULL) + gtk_widget_unparent (child); + + g_clear_object (&view->store); + + G_OBJECT_CLASS (constraint_view_parent_class)->dispose (object); +} + +static void +constraint_view_class_init (ConstraintViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = constraint_view_dispose; + + gtk_widget_class_set_css_name (widget_class, "constraintview"); +} + +static void +update_weak_position (ConstraintView *self, + GtkWidget *child, + double x, + double y) +{ + GtkLayoutManager *manager; + GtkConstraint *constraint; + + manager = gtk_widget_get_layout_manager (GTK_WIDGET (self)); + constraint = (GtkConstraint *)g_object_get_data (G_OBJECT (child), "x-constraint"); + if (constraint) + { + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (child), "x-constraint", NULL); + } + constraint = gtk_constraint_new_constant (child, + GTK_CONSTRAINT_ATTRIBUTE_CENTER_X, + GTK_CONSTRAINT_RELATION_EQ, + x, + GTK_CONSTRAINT_STRENGTH_WEAK); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (child), "x-constraint", constraint); + + constraint = (GtkConstraint *)g_object_get_data (G_OBJECT (child), "y-constraint"); + if (constraint) + { + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (child), "y-constraint", NULL); + } + constraint = gtk_constraint_new_constant (child, + GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y, + GTK_CONSTRAINT_RELATION_EQ, + y, + GTK_CONSTRAINT_STRENGTH_WEAK); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (child), "y-constraint", constraint); +} + +static void +drag_begin (GtkGestureDrag *drag, + double start_x, + double start_y, + ConstraintView *self) +{ + GtkWidget *widget; + + widget = gtk_widget_pick (GTK_WIDGET (self), start_x, start_y, GTK_PICK_DEFAULT); + + if (GTK_IS_LABEL (widget)) + { + widget = gtk_widget_get_ancestor (widget, GTK_TYPE_FRAME); + if (widget && + gtk_widget_get_parent (widget) == (GtkWidget *)self) + { + self->drag_widget = widget; + } + } +} + +static void +drag_update (GtkGestureDrag *drag, + double offset_x, + double offset_y, + ConstraintView *self) +{ + double x, y; + + if (!self->drag_widget) + return; + + gtk_gesture_drag_get_start_point (drag, &x, &y); + update_weak_position (self, self->drag_widget, x + offset_x, y + offset_y); +} + +static void +drag_end (GtkGestureDrag *drag, + double offset_x, + double offset_y, + ConstraintView *self) +{ + self->drag_widget = NULL; +} + +static void +constraint_view_init (ConstraintView *self) +{ + GtkEventController *controller; + + gtk_widget_set_layout_manager (GTK_WIDGET (self), + gtk_constraint_layout_new ()); + + self->store = g_list_store_new (G_TYPE_OBJECT); + + controller = (GtkEventController *)gtk_gesture_drag_new (); + g_signal_connect (controller, "drag-begin", G_CALLBACK (drag_begin), self); + g_signal_connect (controller, "drag-update", G_CALLBACK (drag_update), self); + g_signal_connect (controller, "drag-end", G_CALLBACK (drag_end), self); + gtk_widget_add_controller (GTK_WIDGET (self), controller); +} + +ConstraintView * +constraint_view_new (void) +{ + return g_object_new (CONSTRAINT_VIEW_TYPE, NULL); +} + +void +constraint_view_add_child (ConstraintView *view, + const char *name) +{ + GtkWidget *frame; + GtkWidget *label; + + label = gtk_label_new (name); + frame = gtk_frame_new (NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (frame), "child"); + g_object_set_data_full (G_OBJECT (frame), "name", g_strdup (name), g_free); + gtk_container_add (GTK_CONTAINER (frame), label); + gtk_widget_set_parent (frame, GTK_WIDGET (view)); + + update_weak_position (view, frame, 100, 100); + + g_list_store_append (view->store, frame); +} + +void +constraint_view_remove_child (ConstraintView *view, + GtkWidget *child) +{ + int i; + + gtk_widget_unparent (child); + + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (view->store)); i++) + { + if (g_list_model_get_item (G_LIST_MODEL (view->store), i) == (GObject*)child) + { + g_list_store_remove (view->store, i); + break; + } + } +} + +void +constraint_view_add_guide (ConstraintView *view, + GtkConstraintGuide *guide) +{ + GtkLayoutManager *manager; + GtkWidget *frame; + GtkWidget *label; + const char *name; + GtkConstraint *constraint; + + name = (const char *)g_object_get_data (G_OBJECT (guide), "name"); + + label = gtk_label_new (name); + frame = gtk_frame_new (NULL); + gtk_style_context_add_class (gtk_widget_get_style_context (frame), "guide"); + g_object_set_data_full (G_OBJECT (frame), "name", g_strdup (name), g_free); + gtk_container_add (GTK_CONTAINER (frame), label); + gtk_widget_insert_after (frame, GTK_WIDGET (view), NULL); + + g_object_set_data (G_OBJECT (guide), "frame", frame); + g_object_set_data (G_OBJECT (guide), "label", label); + + manager = gtk_widget_get_layout_manager (GTK_WIDGET (view)); + gtk_constraint_layout_add_guide (GTK_CONSTRAINT_LAYOUT (manager), + g_object_ref (guide)); + + constraint = gtk_constraint_new (frame, + GTK_CONSTRAINT_ATTRIBUTE_LEFT, + GTK_CONSTRAINT_RELATION_EQ, + guide, + GTK_CONSTRAINT_ATTRIBUTE_LEFT, + 1.0, 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (guide), "left-constraint", constraint); + + constraint = gtk_constraint_new (frame, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + guide, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (guide), "top-constraint", constraint); + + constraint = gtk_constraint_new (frame, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_RELATION_EQ, + guide, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + 1.0, 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (guide), "width-constraint", constraint); + + constraint = gtk_constraint_new (frame, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + GTK_CONSTRAINT_RELATION_EQ, + guide, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + 1.0, 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + g_object_set_data (G_OBJECT (guide), "height-constraint", constraint); + + update_weak_position (view, frame, 150, 150); + + g_list_store_append (view->store, guide); +} + +void +constraint_view_guide_changed (ConstraintView *view, + GtkConstraintGuide *guide) +{ + GtkWidget *label; + const char *name; + int i; + + name = (const char *)g_object_get_data (G_OBJECT (guide), "name"); + label = (GtkWidget *)g_object_get_data (G_OBJECT (guide), "label"); + gtk_label_set_label (GTK_LABEL (label), name); + + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (view->store)); i++) + { + if (g_list_model_get_item (G_LIST_MODEL (view->store), i) == (GObject*)guide) + { + g_list_model_items_changed (G_LIST_MODEL (view->store), i, 1, 1); + break; + } + } +} + +void +constraint_view_remove_guide (ConstraintView *view, + GtkConstraintGuide *guide) +{ + GtkLayoutManager *manager; + GtkWidget *frame; + GtkConstraint *constraint; + int i; + + manager = gtk_widget_get_layout_manager (GTK_WIDGET (view)); + + constraint = (GtkConstraint*)g_object_get_data (G_OBJECT (guide), "left-constraint"); + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + constraint = (GtkConstraint*)g_object_get_data (G_OBJECT (guide), "top-constraint"); + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + constraint = (GtkConstraint*)g_object_get_data (G_OBJECT (guide), "width-constraint"); + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + constraint = (GtkConstraint*)g_object_get_data (G_OBJECT (guide), "height-constraint"); + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + + frame = (GtkWidget *)g_object_get_data (G_OBJECT (guide), "frame"); + gtk_widget_unparent (frame); + + gtk_constraint_layout_remove_guide (GTK_CONSTRAINT_LAYOUT (manager), + guide); + + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (view->store)); i++) + { + if (g_list_model_get_item (G_LIST_MODEL (view->store), i) == (GObject*)guide) + { + g_list_store_remove (view->store, i); + break; + } + } +} + +void +constraint_view_add_constraint (ConstraintView *view, + GtkConstraint *constraint) +{ + GtkLayoutManager *manager; + + manager = gtk_widget_get_layout_manager (GTK_WIDGET (view)); + gtk_constraint_layout_add_constraint (GTK_CONSTRAINT_LAYOUT (manager), + g_object_ref (constraint)); + + g_list_store_append (view->store, constraint); +} + +void +constraint_view_remove_constraint (ConstraintView *view, + GtkConstraint *constraint) +{ + GtkLayoutManager *manager; + int i; + + manager = gtk_widget_get_layout_manager (GTK_WIDGET (view)); + gtk_constraint_layout_remove_constraint (GTK_CONSTRAINT_LAYOUT (manager), + constraint); + for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (view->store)); i++) + { + if (g_list_model_get_item (G_LIST_MODEL (view->store), i) == (GObject*)constraint) + { + g_list_store_remove (view->store, i); + break; + } + } +} + +GListModel * +constraint_view_get_model (ConstraintView *view) +{ + return G_LIST_MODEL (view->store); +} diff --git a/demos/constraint-editor/constraint-view.h b/demos/constraint-editor/constraint-view.h new file mode 100644 index 0000000000..40e80c8c43 --- /dev/null +++ b/demos/constraint-editor/constraint-view.h @@ -0,0 +1,44 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#define CONSTRAINT_VIEW_TYPE (constraint_view_get_type ()) + +G_DECLARE_FINAL_TYPE (ConstraintView, constraint_view, CONSTRAINT, VIEW, GtkWidget) + +ConstraintView * constraint_view_new (void); + +void constraint_view_add_child (ConstraintView *view, + const char *name); +void constraint_view_remove_child (ConstraintView *view, + GtkWidget *child); +void constraint_view_add_guide (ConstraintView *view, + GtkConstraintGuide *guide); +void constraint_view_remove_guide (ConstraintView *view, + GtkConstraintGuide *guide); +void constraint_view_guide_changed (ConstraintView *view, + GtkConstraintGuide *guide); +void constraint_view_add_constraint (ConstraintView *view, + GtkConstraint *constraint); +void constraint_view_remove_constraint (ConstraintView *view, + GtkConstraint *constraint); +GListModel * constraint_view_get_model (ConstraintView *view); diff --git a/demos/constraint-editor/guide-editor.c b/demos/constraint-editor/guide-editor.c new file mode 100644 index 0000000000..39bd08a157 --- /dev/null +++ b/demos/constraint-editor/guide-editor.c @@ -0,0 +1,387 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#include "config.h" + +#include "guide-editor.h" + +struct _GuideEditor +{ + GtkWidget parent_instance; + + GtkWidget *grid; + GtkWidget *name; + GtkWidget *min_width; + GtkWidget *min_height; + GtkWidget *nat_width; + GtkWidget *nat_height; + GtkWidget *max_width; + GtkWidget *max_height; + GtkWidget *strength; + GtkWidget *button; + + GtkConstraintGuide *guide; + + gboolean constructed; +}; + +enum { + PROP_GUIDE = 1, + LAST_PROP +}; + +static GParamSpec *pspecs[LAST_PROP]; + +enum { + DONE, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL]; + +G_DEFINE_TYPE(GuideEditor, guide_editor, GTK_TYPE_WIDGET); + +static void +guide_strength_combo (GtkWidget *combo) +{ + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "weak", "Weak"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "medium", "Medium"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "strong", "Strong"); + gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (combo), "required", "Required"); +} + +static GtkConstraintStrength +get_strength (const char *id) +{ + GtkConstraintStrength strength; + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_STRENGTH); + GEnumValue *value = g_enum_get_value_by_nick (class, id); + strength = value->value; + g_type_class_unref (class); + + return strength; +} + +const char * +get_strength_nick (GtkConstraintStrength strength) +{ + GEnumClass *class = g_type_class_ref (GTK_TYPE_CONSTRAINT_STRENGTH); + GEnumValue *value = g_enum_get_value (class, strength); + const char *nick = value->value_nick; + g_type_class_unref (class); + + return nick; +} + +static void +create_guide (GtkButton *button, + GuideEditor *editor) +{ + const char *id; + int strength; + const char *name; + int w, h; + GtkConstraintGuide *guide; + + if (editor->guide) + guide = g_object_ref (editor->guide); + else + guide = gtk_constraint_guide_new (); + + name = gtk_editable_get_text (GTK_EDITABLE (editor->name)); + g_object_set_data_full (G_OBJECT (guide), "name", g_strdup (name), g_free); + + w = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->min_width)); + h = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->min_height)); + gtk_constraint_guide_set_min_size (guide, w, h); + + w = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->nat_width)); + h = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->nat_height)); + gtk_constraint_guide_set_nat_size (guide, w, h); + + w = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->max_width)); + h = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (editor->max_height)); + gtk_constraint_guide_set_max_size (guide, w, h); + + id = gtk_combo_box_get_active_id (GTK_COMBO_BOX (editor->strength)); + strength = get_strength (id); + gtk_constraint_guide_set_strength (guide, strength); + + g_signal_emit (editor, signals[DONE], 0, guide); + g_object_unref (guide); +} + +static void +guide_editor_init (GuideEditor *editor) +{ + gtk_widget_init_template (GTK_WIDGET (editor)); +} + +static int guide_counter; + +static int +min_input (GtkSpinButton *spin_button, + double *new_val) +{ + if (strcmp (gtk_editable_get_text (GTK_EDITABLE (spin_button)), "") == 0) + { + *new_val = 0.0; + return TRUE; + } + + return FALSE; +} + +static int +max_input (GtkSpinButton *spin_button, + double *new_val) +{ + if (strcmp (gtk_editable_get_text (GTK_EDITABLE (spin_button)), "") == 0) + { + *new_val = G_MAXINT; + return TRUE; + } + + return FALSE; +} + +static gboolean +min_output (GtkSpinButton *spin_button) +{ + GtkAdjustment *adjustment; + double value; + GtkWidget *box, *text; + + adjustment = gtk_spin_button_get_adjustment (spin_button); + value = gtk_adjustment_get_value (adjustment); + + box = gtk_widget_get_first_child (GTK_WIDGET (spin_button)); + text = gtk_widget_get_first_child (box); + + if (value == 0.0) + { + gtk_editable_set_text (GTK_EDITABLE (spin_button), ""); + gtk_text_set_placeholder_text (GTK_TEXT (text), "unset"); + return TRUE; + } + else + { + gtk_text_set_placeholder_text (GTK_TEXT (text), ""); + return FALSE; + } +} + +static gboolean +max_output (GtkSpinButton *spin_button) +{ + GtkAdjustment *adjustment; + double value; + GtkWidget *box, *text; + + adjustment = gtk_spin_button_get_adjustment (spin_button); + value = gtk_adjustment_get_value (adjustment); + + box = gtk_widget_get_first_child (GTK_WIDGET (spin_button)); + text = gtk_widget_get_first_child (box); + + if (value == (double)G_MAXINT) + { + gtk_editable_set_text (GTK_EDITABLE (spin_button), ""); + gtk_text_set_placeholder_text (GTK_TEXT (text), "unset"); + return TRUE; + } + else + { + gtk_text_set_placeholder_text (GTK_TEXT (text), ""); + return FALSE; + } +} + +static void +guide_editor_constructed (GObject *object) +{ + GuideEditor *editor = GUIDE_EDITOR (object); + + guide_strength_combo (editor->strength); + + g_signal_connect (editor->min_width, "input", G_CALLBACK (min_input), NULL); + g_signal_connect (editor->min_width, "output", G_CALLBACK (min_output), NULL); + + g_signal_connect (editor->min_height, "input", G_CALLBACK (min_input), NULL); + g_signal_connect (editor->min_height, "output", G_CALLBACK (min_output), NULL); + + g_signal_connect (editor->max_width, "input", G_CALLBACK (max_input), NULL); + g_signal_connect (editor->max_width, "output", G_CALLBACK (max_output), NULL); + + g_signal_connect (editor->max_height, "input", G_CALLBACK (max_input), NULL); + g_signal_connect (editor->max_height, "output", G_CALLBACK (max_output), NULL); + + if (editor->guide) + { + GtkConstraintStrength strength; + const char *nick; + int w, h; + + nick = (char *)g_object_get_data (G_OBJECT (editor->guide), "name"); + gtk_editable_set_text (GTK_EDITABLE (editor->name), nick); + + gtk_constraint_guide_get_min_size (editor->guide, &w, &h); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->min_width), w); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->min_height), h); + + gtk_constraint_guide_get_nat_size (editor->guide, &w, &h); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->nat_width), w); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->nat_height), h); + + gtk_constraint_guide_get_max_size (editor->guide, &w, &h); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->max_width), w); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->max_height), h); + + strength = gtk_constraint_guide_get_strength (editor->guide); + nick = get_strength_nick (strength); + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->strength), nick); + + gtk_button_set_label (GTK_BUTTON (editor->button), "Apply"); + } + else + { + char *name; + + guide_counter++; + name = g_strdup_printf ("Guide %d", guide_counter); + gtk_editable_set_text (GTK_EDITABLE (editor->name), name); + g_free (name); + + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->min_width), 0.0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->min_height), 0.0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->nat_width), 0.0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->nat_height), 0.0); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->max_width), G_MAXINT); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (editor->max_height), G_MAXINT); + + gtk_combo_box_set_active_id (GTK_COMBO_BOX (editor->strength), "medium"); + + gtk_button_set_label (GTK_BUTTON (editor->button), "Create"); + } + + editor->constructed = TRUE; +} + +static void +guide_editor_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GuideEditor *self = GUIDE_EDITOR (object); + + switch (property_id) + { + case PROP_GUIDE: + self->guide = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +guide_editor_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GuideEditor *self = GUIDE_EDITOR (object); + + switch (property_id) + { + case PROP_GUIDE: + g_value_set_object (value, self->guide); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +guide_editor_dispose (GObject *object) +{ + GuideEditor *self = (GuideEditor *)object; + + g_clear_pointer (&self->grid, gtk_widget_unparent); + g_clear_object (&self->guide); + + G_OBJECT_CLASS (guide_editor_parent_class)->dispose (object); +} + +static void +guide_editor_class_init (GuideEditorClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->constructed = guide_editor_constructed; + object_class->dispose = guide_editor_dispose; + object_class->set_property = guide_editor_set_property; + object_class->get_property = guide_editor_get_property; + + pspecs[PROP_GUIDE] = + g_param_spec_object ("guide", "guide", "guide", + GTK_TYPE_CONSTRAINT_GUIDE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); + + signals[DONE] = + g_signal_new ("done", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, GTK_TYPE_CONSTRAINT_GUIDE); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gtk/gtk4/constraint-editor/guide-editor.ui"); + + gtk_widget_class_bind_template_child (widget_class, GuideEditor, grid); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, name); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, min_width); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, min_height); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, nat_width); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, nat_height); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, max_width); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, max_height); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, strength); + gtk_widget_class_bind_template_child (widget_class, GuideEditor, button); + + gtk_widget_class_bind_template_callback (widget_class, create_guide); +} + +GuideEditor * +guide_editor_new (GtkConstraintGuide *guide) +{ + return g_object_new (GUIDE_EDITOR_TYPE, + "guide", guide, + NULL); +} diff --git a/demos/constraint-editor/guide-editor.h b/demos/constraint-editor/guide-editor.h new file mode 100644 index 0000000000..d11cb4f3db --- /dev/null +++ b/demos/constraint-editor/guide-editor.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2019 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtk.h> + +#define GUIDE_EDITOR_TYPE (guide_editor_get_type ()) + +G_DECLARE_FINAL_TYPE (GuideEditor, guide_editor, GUIDE, EDITOR, GtkWidget) + +GuideEditor * guide_editor_new (GtkConstraintGuide *guide); diff --git a/demos/constraint-editor/guide-editor.ui b/demos/constraint-editor/guide-editor.ui new file mode 100644 index 0000000000..a788de10bf --- /dev/null +++ b/demos/constraint-editor/guide-editor.ui @@ -0,0 +1,188 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <object class="GtkAdjustment" id="min_width_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <object class="GtkAdjustment" id="min_height_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <object class="GtkAdjustment" id="nat_width_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <object class="GtkAdjustment" id="nat_height_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <object class="GtkAdjustment" id="max_width_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <object class="GtkAdjustment" id="max_height_adj"> + <property name="lower">0</property> + <property name="upper">2147483647</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + <property name="page-size">0</property> + </object> + <template class="GuideEditor" parent="GtkWidget"> + <child> + <object class="GtkGrid" id="grid"> + <property name="margin">20</property> + <property name="row-spacing">10</property> + <property name="column-spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="label">Name</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="name"> + <property name="max-width-chars">20</property> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + <property name="column-span">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Min Size</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="min_width"> + <property name="adjustment">min_width_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="min_height"> + <property name="adjustment">min_height_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Nat Size</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="nat_width"> + <property name="adjustment">nat_width_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="nat_height"> + <property name="adjustment">nat_height_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Max Size</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="max_width"> + <property name="adjustment">max_width_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkSpinButton" id="max_height"> + <property name="adjustment">max_height_adj</property> + <property name="max-width-chars">5</property> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label">Strength</property> + <layout> + <property name="left-attach">0</property> + <property name="top-attach">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="strength"> + <layout> + <property name="left-attach">1</property> + <property name="top-attach">4</property> + <property name="column-span">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkButton" id="button"> + <property name="label">Create</property> + <signal name="clicked" handler="create_guide"/> + <layout> + <property name="left-attach">2</property> + <property name="top-attach">5</property> + </layout> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/demos/constraint-editor/main.c b/demos/constraint-editor/main.c new file mode 100644 index 0000000000..771774c08f --- /dev/null +++ b/demos/constraint-editor/main.c @@ -0,0 +1,28 @@ +/* + * Copyright © 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <constraint-editor-application.h> + +int +main (int argc, char *argv[]) +{ + return g_application_run (G_APPLICATION (constraint_editor_application_new ()), argc, argv); +} diff --git a/demos/constraint-editor/meson.build b/demos/constraint-editor/meson.build new file mode 100644 index 0000000000..af66846437 --- /dev/null +++ b/demos/constraint-editor/meson.build @@ -0,0 +1,19 @@ +constraint_editor_sources = [ + 'main.c', + 'constraint-editor-application.c', + 'constraint-editor-window.c', + 'constraint-view.c', + 'constraint-editor.c', + 'guide-editor.c', +] + +constraint_editor_resources = gnome.compile_resources('constraint_editor_resources', + 'constraint-editor.gresource.xml', + source_dir: '.') + +executable('gtk4-constraint-editor', + constraint_editor_sources, constraint_editor_resources, + dependencies: libgtk_dep, + include_directories: confinc, + gui_app: true, + install: false) diff --git a/demos/gtk-demo/constraints.c b/demos/gtk-demo/constraints.c new file mode 100644 index 0000000000..1b6b00b2f5 --- /dev/null +++ b/demos/gtk-demo/constraints.c @@ -0,0 +1,289 @@ +/* Constraints/Simple + * + * GtkConstraintLayout provides a layout manager that uses relations + * between widgets (also known as "constraints") to compute the position + * and size of each child. + */ + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +G_DECLARE_FINAL_TYPE (SimpleGrid, simple_grid, SIMPLE, GRID, GtkWidget) + +struct _SimpleGrid +{ + GtkWidget parent_instance; + + GtkWidget *button1, *button2; + GtkWidget *button3; +}; + +G_DEFINE_TYPE (SimpleGrid, simple_grid, GTK_TYPE_WIDGET) + +static void +simple_grid_destroy (GtkWidget *widget) +{ + SimpleGrid *self = SIMPLE_GRID (widget); + + g_clear_pointer (&self->button1, gtk_widget_destroy); + g_clear_pointer (&self->button2, gtk_widget_destroy); + g_clear_pointer (&self->button3, gtk_widget_destroy); + + GTK_WIDGET_CLASS (simple_grid_parent_class)->destroy (widget); +} + +static void +simple_grid_class_init (SimpleGridClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = simple_grid_destroy; + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_CONSTRAINT_LAYOUT); +} + +/* Layout: + * + * +-------------------------------------+ + * | +-----------++-------++-----------+ | + * | | Child 1 || Space || Child 2 | | + * | +-----------++-------++-----------+ | + * | +---------------------------------+ | + * | | Child 3 | | + * | +---------------------------------+ | + * +-------------------------------------+ + * + * Constraints: + * + * super.start = child1.start - 8 + * child1.width = child2.width + * child1.end = space.start + * space.end = child2.start + * child2.end = super.end - 8 + * super.start = child3.start - 8 + * child3.end = super.end - 8 + * super.top = child1.top - 8 + * super.top = child2.top - 8 + * child1.bottom = child3.top - 12 + * child2.bottom = child3.top - 12 + * child3.height = child1.height + * child3.height = child2.height + * child3.bottom = super.bottom - 8 + * + * To add some flexibility, we make the space + * stretchable: + * + * space.width >= 10 + * space.width = 100 + * space.width <= 200 + */ +static void +build_constraints (SimpleGrid *self, + GtkConstraintLayout *manager) +{ + GtkConstraintGuide *guide; + + guide = gtk_constraint_guide_new (); + gtk_constraint_guide_set_name (guide, "space"); + gtk_constraint_guide_set_min_size (guide, 10, 10); + gtk_constraint_guide_set_nat_size (guide, 100, 10); + gtk_constraint_guide_set_max_size (guide, 200, 20); + gtk_constraint_guide_set_strength (guide, GTK_CONSTRAINT_STRENGTH_STRONG); + gtk_constraint_layout_add_guide (manager, guide); + + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new_constant (GTK_CONSTRAINT_TARGET (self->button1), + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_RELATION_LE, + 200.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_RELATION_EQ, + self->button1, + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button1, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_RELATION_EQ, + self->button2, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button1, + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + guide, + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (guide, + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + self->button2, + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button2, + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + NULL, + GTK_CONSTRAINT_ATTRIBUTE_END, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_RELATION_EQ, + self->button3, + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button3, + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + NULL, + GTK_CONSTRAINT_ATTRIBUTE_END, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + self->button1, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + self->button2, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button1, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + GTK_CONSTRAINT_RELATION_EQ, + self->button3, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, + -12.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button2, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + GTK_CONSTRAINT_RELATION_EQ, + self->button3, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, + -12.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button3, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + GTK_CONSTRAINT_RELATION_EQ, + self->button1, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button3, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + GTK_CONSTRAINT_RELATION_EQ, + self->button2, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (self->button3, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + GTK_CONSTRAINT_RELATION_EQ, + NULL, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); +} + +static void +simple_grid_init (SimpleGrid *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + + self->button1 = gtk_button_new_with_label ("Child 1"); + gtk_widget_set_parent (self->button1, widget); + gtk_widget_set_name (self->button1, "button1"); + + self->button2 = gtk_button_new_with_label ("Child 2"); + gtk_widget_set_parent (self->button2, widget); + gtk_widget_set_name (self->button2, "button2"); + + self->button3 = gtk_button_new_with_label ("Child 3"); + gtk_widget_set_parent (self->button3, widget); + gtk_widget_set_name (self->button3, "button3"); + + GtkLayoutManager *manager = gtk_widget_get_layout_manager (GTK_WIDGET (self)); + build_constraints (self, GTK_CONSTRAINT_LAYOUT (manager)); +} + +GtkWidget * +do_constraints (GtkWidget *do_widget) +{ + static GtkWidget *window; + + if (!window) + { + GtkWidget *header, *box, *grid, *button; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_display (GTK_WINDOW (window), gtk_widget_get_display (do_widget)); + + header = gtk_header_bar_new (); + gtk_header_bar_set_title (GTK_HEADER_BAR (header), "Constraints"); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (header), FALSE); + gtk_window_set_titlebar (GTK_WINDOW (window), header); + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_add (GTK_CONTAINER (window), box); + + grid = g_object_new (simple_grid_get_type (), NULL); + gtk_widget_set_hexpand (grid, TRUE); + gtk_widget_set_vexpand (grid, TRUE); + gtk_container_add (GTK_CONTAINER (box), grid); + + button = gtk_button_new_with_label ("Close"); + gtk_container_add (GTK_CONTAINER (box), button); + gtk_widget_set_hexpand (grid, TRUE); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_widget_destroy), window); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (window); + + return window; +} diff --git a/demos/gtk-demo/constraints2.c b/demos/gtk-demo/constraints2.c new file mode 100644 index 0000000000..7ec7e09520 --- /dev/null +++ b/demos/gtk-demo/constraints2.c @@ -0,0 +1,245 @@ +/* Constraints/Interactive + * + * Demonstrate how constraints can be updates during + * user interaction. + */ + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +G_DECLARE_FINAL_TYPE (InteractiveGrid, interactive_grid, INTERACTIVE, GRID, GtkWidget) + +struct _InteractiveGrid +{ + GtkWidget parent_instance; + + GtkWidget *button1, *button2; + GtkWidget *button3; + GtkConstraintGuide *guide; + GtkConstraint *constraint; +}; + +G_DEFINE_TYPE (InteractiveGrid, interactive_grid, GTK_TYPE_WIDGET) + +static void +interactive_grid_destroy (GtkWidget *widget) +{ + InteractiveGrid *self = INTERACTIVE_GRID (widget); + + g_clear_pointer (&self->button1, gtk_widget_destroy); + g_clear_pointer (&self->button2, gtk_widget_destroy); + g_clear_pointer (&self->button3, gtk_widget_destroy); + + GTK_WIDGET_CLASS (interactive_grid_parent_class)->destroy (widget); +} + +static void +interactive_grid_class_init (InteractiveGridClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = interactive_grid_destroy; + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_CONSTRAINT_LAYOUT); +} + +static void +build_constraints (InteractiveGrid *self, + GtkConstraintLayout *manager) +{ + self->guide = g_object_new (GTK_TYPE_CONSTRAINT_GUIDE, NULL); + gtk_constraint_layout_add_guide (manager, self->guide); + + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new_constant (GTK_CONSTRAINT_TARGET (self->guide), + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_RELATION_EQ, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->button1), + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button1), + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->guide), + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button2), + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->guide), + GTK_CONSTRAINT_ATTRIBUTE_END, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button2), + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + NULL, + GTK_CONSTRAINT_ATTRIBUTE_END, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->button3), + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button3), + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->guide), + GTK_CONSTRAINT_ATTRIBUTE_START, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (NULL, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->button1), + GTK_CONSTRAINT_ATTRIBUTE_TOP, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button2), + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->button1), + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button3), + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_TARGET (self->button2), + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + 1.0, + 0.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); + gtk_constraint_layout_add_constraint (manager, + gtk_constraint_new (GTK_CONSTRAINT_TARGET (self->button3), + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + GTK_CONSTRAINT_RELATION_EQ, + NULL, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + 1.0, + -8.0, + GTK_CONSTRAINT_STRENGTH_REQUIRED)); +} + +static void +drag_cb (GtkGestureDrag *drag, + double offset_x, + double offset_y, + InteractiveGrid *self) +{ + GtkConstraintLayout *layout = GTK_CONSTRAINT_LAYOUT (gtk_widget_get_layout_manager (GTK_WIDGET (self))); + double x, y; + + if (self->constraint) + { + gtk_constraint_layout_remove_constraint (layout, self->constraint); + g_clear_object (&self->constraint); + } + + gtk_gesture_drag_get_start_point (drag, &x, &y); + self->constraint = gtk_constraint_new_constant (GTK_CONSTRAINT_TARGET (self->guide), + GTK_CONSTRAINT_ATTRIBUTE_LEFT, + GTK_CONSTRAINT_RELATION_EQ, + x + offset_x, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_layout_add_constraint (layout, g_object_ref (self->constraint)); + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +static void +interactive_grid_init (InteractiveGrid *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + GtkGesture *drag; + + self->button1 = gtk_button_new_with_label ("Child 1"); + gtk_widget_set_parent (self->button1, widget); + gtk_widget_set_name (self->button1, "button1"); + + self->button2 = gtk_button_new_with_label ("Child 2"); + gtk_widget_set_parent (self->button2, widget); + gtk_widget_set_name (self->button2, "button2"); + + self->button3 = gtk_button_new_with_label ("Child 3"); + gtk_widget_set_parent (self->button3, widget); + gtk_widget_set_name (self->button3, "button3"); + + GtkLayoutManager *manager = gtk_widget_get_layout_manager (GTK_WIDGET (self)); + build_constraints (self, GTK_CONSTRAINT_LAYOUT (manager)); + + drag = gtk_gesture_drag_new (); + g_signal_connect (drag, "drag-update", G_CALLBACK (drag_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag)); +} + +GtkWidget * +do_constraints2 (GtkWidget *do_widget) +{ + static GtkWidget *window; + + if (!window) + { + GtkWidget *header, *box, *grid, *button; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_display (GTK_WINDOW (window), gtk_widget_get_display (do_widget)); + + header = gtk_header_bar_new (); + gtk_header_bar_set_title (GTK_HEADER_BAR (header), "Constraints"); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (header), FALSE); + gtk_window_set_titlebar (GTK_WINDOW (window), header); + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_add (GTK_CONTAINER (window), box); + + grid = g_object_new (interactive_grid_get_type (), NULL); + gtk_widget_set_hexpand (grid, TRUE); + gtk_widget_set_vexpand (grid, TRUE); + gtk_container_add (GTK_CONTAINER (box), grid); + + button = gtk_button_new_with_label ("Close"); + gtk_container_add (GTK_CONTAINER (box), button); + gtk_widget_set_hexpand (grid, TRUE); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_widget_destroy), window); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (window); + + return window; +} diff --git a/demos/gtk-demo/constraints3.c b/demos/gtk-demo/constraints3.c new file mode 100644 index 0000000000..648f38125b --- /dev/null +++ b/demos/gtk-demo/constraints3.c @@ -0,0 +1,165 @@ +/* Constraints/VFL + * + * GtkConstraintLayout allows defining constraints using a + * compact syntax called Visual Format Language, or VFL. + */ + +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +G_DECLARE_FINAL_TYPE (VflGrid, vfl_grid, VFL, GRID, GtkWidget) + +struct _VflGrid +{ + GtkWidget parent_instance; + + GtkWidget *button1, *button2; + GtkWidget *button3; +}; + +G_DEFINE_TYPE (VflGrid, vfl_grid, GTK_TYPE_WIDGET) + +static void +vfl_grid_destroy (GtkWidget *widget) +{ + VflGrid *self = VFL_GRID (widget); + + g_clear_pointer (&self->button1, gtk_widget_destroy); + g_clear_pointer (&self->button2, gtk_widget_destroy); + g_clear_pointer (&self->button3, gtk_widget_destroy); + + GTK_WIDGET_CLASS (vfl_grid_parent_class)->destroy (widget); +} + +static void +vfl_grid_class_init (VflGridClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->destroy = vfl_grid_destroy; + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_CONSTRAINT_LAYOUT); +} + +/* Layout: + * + * +-----------------------------+ + * | +-----------+ +-----------+ | + * | | Child 1 | | Child 2 | | + * | +-----------+ +-----------+ | + * | +-------------------------+ | + * | | Child 3 | | + * | +-------------------------+ | + * +-----------------------------+ + * + * Constraints: + * + * super.start = child1.start - 8 + * child1.width = child2.width + * child1.end = child2.start - 12 + * child2.end = super.end - 8 + * super.start = child3.start - 8 + * child3.end = super.end - 8 + * super.top = child1.top - 8 + * super.top = child2.top - 8 + * child1.bottom = child3.top - 12 + * child2.bottom = child3.top - 12 + * child3.height = child1.height + * child3.height = child2.height + * child3.bottom = super.bottom - 8 + * + * Visual format: + * + * H:|-8-[view1(==view2)-12-[view2]-8-| + * H:|-8-[view3]-8-| + * V:|-8-[view1]-12-[view3(==view1)]-8-| + * V:|-8-[view2]-12-[view3(==view2)]-8-| + */ +static void +build_constraints (VflGrid *self, + GtkConstraintLayout *manager) +{ + const char * const vfl[] = { + "H:|-[button1(==button2)]-12-[button2]-|", + "H:|-[button3]-|", + "V:|-[button1]-12-[button3(==button1)]-|", + "V:|-[button2]-12-[button3(==button2)]-|", + }; + GError *error = NULL; + + gtk_constraint_layout_add_constraints_from_description (manager, vfl, G_N_ELEMENTS (vfl), + 8, 8, + &error, + "button1", self->button1, + "button2", self->button2, + "button3", self->button3, + NULL); + if (error != NULL) + { + g_printerr ("VFL parsing error:\n%s", error->message); + g_error_free (error); + } +} + +static void +vfl_grid_init (VflGrid *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + + self->button1 = gtk_button_new_with_label ("Child 1"); + gtk_widget_set_parent (self->button1, widget); + gtk_widget_set_name (self->button1, "button1"); + + self->button2 = gtk_button_new_with_label ("Child 2"); + gtk_widget_set_parent (self->button2, widget); + gtk_widget_set_name (self->button2, "button2"); + + self->button3 = gtk_button_new_with_label ("Child 3"); + gtk_widget_set_parent (self->button3, widget); + gtk_widget_set_name (self->button3, "button3"); + + GtkLayoutManager *manager = gtk_widget_get_layout_manager (GTK_WIDGET (self)); + build_constraints (self, GTK_CONSTRAINT_LAYOUT (manager)); +} + +GtkWidget * +do_constraints3 (GtkWidget *do_widget) +{ + static GtkWidget *window; + + if (!window) + { + GtkWidget *header, *box, *grid, *button; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_display (GTK_WINDOW (window), gtk_widget_get_display (do_widget)); + + header = gtk_header_bar_new (); + gtk_header_bar_set_title (GTK_HEADER_BAR (header), "Constraints"); + gtk_header_bar_set_show_title_buttons (GTK_HEADER_BAR (header), FALSE); + gtk_window_set_titlebar (GTK_WINDOW (window), header); + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_add (GTK_CONTAINER (window), box); + + grid = g_object_new (vfl_grid_get_type (), NULL); + gtk_widget_set_hexpand (grid, TRUE); + gtk_widget_set_vexpand (grid, TRUE); + gtk_container_add (GTK_CONTAINER (box), grid); + + button = gtk_button_new_with_label ("Close"); + gtk_container_add (GTK_CONTAINER (box), button); + gtk_widget_set_hexpand (grid, TRUE); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_widget_destroy), window); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (window); + + return window; +} diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index ca8558dc29..6b6205b6b7 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -150,6 +150,9 @@ <file>clipboard.c</file> <file>colorsel.c</file> <file>combobox.c</file> + <file>constraints.c</file> + <file>constraints2.c</file> + <file>constraints3.c</file> <file>css_accordion.c</file> <file>css_basics.c</file> <file>css_blendmodes.c</file> diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index 87e5ae4f85..b6f52cb25c 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -8,6 +8,9 @@ demos = files([ 'clipboard.c', 'colorsel.c', 'combobox.c', + 'constraints.c', + 'constraints2.c', + 'constraints3.c', 'css_accordion.c', 'css_basics.c', 'css_blendmodes.c', diff --git a/demos/meson.build b/demos/meson.build index 6f1905ddb6..c9d7081b40 100644 --- a/demos/meson.build +++ b/demos/meson.build @@ -1,3 +1,4 @@ +subdir('constraint-editor') subdir('gtk-demo') subdir('icon-browser') subdir('node-editor') diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml index e574d082ee..d220270a81 100644 --- a/docs/reference/gtk/gtk4-docs.xml +++ b/docs/reference/gtk/gtk4-docs.xml @@ -109,6 +109,9 @@ <xi:include href="xml/gtkcustomlayout.xml" /> <xi:include href="xml/gtkfixedlayout.xml" /> <xi:include href="xml/gtkgridlayout.xml" /> + <xi:include href="xml/gtkconstraintlayout.xml" /> + <xi:include href="xml/gtkconstraint.xml" /> + <xi:include href="xml/gtkconstraintguide.xml" /> </chapter> <chapter id="DisplayWidgets"> diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt index a9076529c2..ac9ea19fa9 100644 --- a/docs/reference/gtk/gtk4-sections.txt +++ b/docs/reference/gtk/gtk4-sections.txt @@ -7287,3 +7287,83 @@ gtk_grid_layout_get_type GTK_TYPE_GRID_LAYOUT_CHILD gtk_grid_layout_child_get_type </SECTION> + +<SECTION> +<FILE>gtkconstraint</FILE> +GtkConstraint +GtkConstraintTarget + +gtk_constraint_new +gtk_constraint_new_constant +gtk_constraint_get_target +GtkConstraintAttribute +gtk_constraint_get_target_attribute +GtkConstraintRelation +gtk_constraint_get_relation +gtk_constraint_get_source +gtk_constraint_get_source_attribute +gtk_constraint_get_multiplier +gtk_constraint_get_constant +GtkConstraintStrength +gtk_constraint_get_strength +gtk_constraint_is_required +gtk_constraint_is_attached +gtk_constraint_is_constant + +<SUBSECTION Standard> +GTK_TYPE_CONSTRAINT +gtk_constraint_get_type +GTK_TYPE_CONSTRAINT_TARGET +gtk_constraint_target_get_type +</SECTION> + +<SECTION> +<FILE>gtkconstraintlayout</FILE> +GtkConstraintLayout +GtkConstraintLayoutChild +GtkConstraintVflParserError + +gtk_constraint_layout_new + +<SUBSECTION Constraints> +gtk_constraint_layout_add_constraint +gtk_constraint_layout_remove_constraint +gtk_constraint_layout_remove_all_constraints + +<SUBSECTION Guides> +gtk_constraint_layout_add_guide +gtk_constraint_layout_remove_guide + +<SUBSECTION VFL> +gtk_constraint_layout_add_constraints_from_description +gtk_constraint_layout_add_constraints_from_descriptionv + +<SUBSECTION Standard> +GTK_TYPE_CONSTRAINT_LAYOUT +gtk_constraint_layout_get_type +GTK_TYPE_CONSTRAINT_LAYOUT_CHILD +gtk_constraint_layout_child_get_type +GTK_CONSTRAINT_VFL_PARSER_ERROR +gtk_constraint_vfl_parser_error_quark +</SECTION> + +<SECTION> +<FILE>gtkconstraintguide</FILE> +GtkConstraintGuide + +gtk_constraint_guide_new +gtk_constraint_guide_set_name +gtk_constraint_guide_get_name +gtk_constraint_guide_set_strength +gtk_constraint_guide_get_strength +gtk_constraint_guide_set_min_size +gtk_constraint_guide_get_min_size +gtk_constraint_guide_set_nat_size +gtk_constraint_guide_get_nat_size +gtk_constraint_guide_set_max_size +gtk_constraint_guide_get_max_size + +<SUBSECTION Standard> +GTK_TYPE_CONSTRAINT_GUIDE +gtk_constraint_guide_get_tyoe +</SECTION> diff --git a/docs/reference/gtk/gtk4.types.in b/docs/reference/gtk/gtk4.types.in index 8073f3ad20..a03901d769 100644 --- a/docs/reference/gtk/gtk4.types.in +++ b/docs/reference/gtk/gtk4.types.in @@ -49,6 +49,10 @@ gtk_color_chooser_dialog_get_type gtk_color_chooser_widget_get_type gtk_combo_box_get_type gtk_combo_box_text_get_type +gtk_constraint_get_type +gtk_constraint_guide_get_type +gtk_constraint_layout_get_type +gtk_constraint_target_get_type gtk_container_get_type gtk_css_provider_get_type gtk_dialog_get_type diff --git a/docs/reference/gtk/meson.build b/docs/reference/gtk/meson.build index 1243d12c95..c9f1e9c2c9 100644 --- a/docs/reference/gtk/meson.build +++ b/docs/reference/gtk/meson.build @@ -23,6 +23,13 @@ private_headers = [ 'gtkcolorswatchprivate.h', 'gtkcomboboxprivate.h', 'gtkcontainerprivate.h', + 'gtkconstraintexpressionprivate.h', + 'gtkconstraintguideprivate.h', + 'gtkconstraintlayoutprivate.h', + 'gtkconstraintprivate.h', + 'gtkconstraintsolverprivate.h', + 'gtkconstrainttypesprivate.h', + 'gtkconstraintvflparserprivate.h', 'gtkcssanimatedstyleprivate.h', 'gtkcssanimationprivate.h', 'gtkcssarrayvalueprivate.h', @@ -82,6 +82,8 @@ #include <gtk/gtkcolorutils.h> #include <gtk/gtkcombobox.h> #include <gtk/gtkcomboboxtext.h> +#include <gtk/gtkconstraintlayout.h> +#include <gtk/gtkconstraint.h> #include <gtk/gtkcontainer.h> #include <gtk/gtkcssprovider.h> #include <gtk/gtkcustomlayout.h> diff --git a/gtk/gtkconstraint.c b/gtk/gtkconstraint.c new file mode 100644 index 0000000000..4ddaa5ac1b --- /dev/null +++ b/gtk/gtkconstraint.c @@ -0,0 +1,614 @@ +/* gtkconstraint.c: Constraint between two widgets + * Copyright 2019 GNOME Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +/** + * SECTION:gtkconstraint + * @Title: GtkConstraint + * @Short_description: The description of a constraint + * + * #GtkConstraint describes a constraint between an attribute on a widget + * and another attribute on another widget, expressed as a linear equation + * like: + * + * |[ + * target.attr1 = source.attr2 × multiplier + constant + * ]| + * + * Each #GtkConstraint is part of a system that will be solved by a + * #GtkConstraintLayout in order to allocate and position each child widget. + * + * The source and target widgets, as well as their attributes, of a + * #GtkConstraint instance are immutable after creation. + */ + +#include "config.h" + +#include "gtkconstraintprivate.h" +#include "gtkconstraintsolverprivate.h" +#include "gtkintl.h" +#include "gtktypebuiltins.h" +#include "gtkwidget.h" + +enum { + PROP_TARGET = 1, + PROP_TARGET_ATTRIBUTE, + PROP_RELATION, + PROP_SOURCE, + PROP_SOURCE_ATTRIBUTE, + PROP_MULTIPLIER, + PROP_CONSTANT, + PROP_STRENGTH, + + N_PROPERTIES +}; + +static GParamSpec *obj_props[N_PROPERTIES]; + +G_DEFINE_TYPE (GtkConstraint, gtk_constraint, G_TYPE_OBJECT) + +static void +gtk_constraint_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkConstraint *self = GTK_CONSTRAINT (gobject); + + switch (prop_id) + { + case PROP_TARGET: + self->target = g_value_get_object (value); + break; + + case PROP_TARGET_ATTRIBUTE: + self->target_attribute = g_value_get_enum (value); + break; + + case PROP_RELATION: + self->relation = g_value_get_enum (value); + break; + + case PROP_SOURCE: + self->source = g_value_get_object (value); + break; + + case PROP_SOURCE_ATTRIBUTE: + self->source_attribute = g_value_get_enum (value); + break; + + case PROP_MULTIPLIER: + self->multiplier = g_value_get_double (value); + break; + + case PROP_CONSTANT: + self->constant = g_value_get_double (value); + break; + + case PROP_STRENGTH: + self->strength = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtk_constraint_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkConstraint *self = GTK_CONSTRAINT (gobject); + + switch (prop_id) + { + case PROP_TARGET: + g_value_set_object (value, self->target); + break; + + case PROP_TARGET_ATTRIBUTE: + g_value_set_enum (value, self->target_attribute); + break; + + case PROP_RELATION: + g_value_set_enum (value, self->relation); + break; + + case PROP_SOURCE: + g_value_set_object (value, self->source); + break; + + case PROP_SOURCE_ATTRIBUTE: + g_value_set_enum (value, self->source_attribute); + break; + + case PROP_MULTIPLIER: + g_value_set_double (value, self->multiplier); + break; + + case PROP_CONSTANT: + g_value_set_double (value, self->constant); + break; + + case PROP_STRENGTH: + g_value_set_int (value, self->strength); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtk_constraint_finalize (GObject *gobject) +{ + GtkConstraint *self = GTK_CONSTRAINT (gobject); + + gtk_constraint_detach (self); + + G_OBJECT_CLASS (gtk_constraint_parent_class)->finalize (gobject); +} + +static void +gtk_constraint_class_init (GtkConstraintClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = gtk_constraint_set_property; + gobject_class->get_property = gtk_constraint_get_property; + gobject_class->finalize = gtk_constraint_finalize; + + /** + * GtkConstraint:target: + * + * The target of the constraint. + * + * The constraint will set the #GtkConstraint:target-attribute of the + * target using the #GtkConstraint:source-attribute of the source + * widget. + */ + obj_props[PROP_TARGET] = + g_param_spec_object ("target", + P_("Target"), + P_("The target of the constraint"), + GTK_TYPE_CONSTRAINT_TARGET, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:target-attribute: + * + * The attribute of the #GtkConstraint:target set by the constraint. + */ + obj_props[PROP_TARGET_ATTRIBUTE] = + g_param_spec_enum ("target-attribute", + P_("Target Attribute"), + P_("The attribute of the target set by the constraint"), + GTK_TYPE_CONSTRAINT_ATTRIBUTE, + GTK_CONSTRAINT_ATTRIBUTE_NONE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:relation: + * + * The order relation between the terms of the constraint. + */ + obj_props[PROP_RELATION] = + g_param_spec_enum ("relation", + P_("Relation"), + P_("The relation between the source and target attributes"), + GTK_TYPE_CONSTRAINT_RELATION, + GTK_CONSTRAINT_RELATION_EQ, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:source: + * + * The source of the constraint. + * + * The constraint will set the #GtkConstraint:target-attribute of the + * target using the #GtkConstraint:source-attribute of the source. + */ + obj_props[PROP_SOURCE] = + g_param_spec_object ("source", + P_("Source"), + P_("The source of the constraint"), + GTK_TYPE_CONSTRAINT_TARGET, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:source-attribute: + * + * The attribute of the #GtkConstraint:source read by the constraint. + */ + obj_props[PROP_SOURCE_ATTRIBUTE] = + g_param_spec_enum ("source-attribute", + P_("Source Attribute"), + P_("The attribute of the source widget set by the constraint"), + GTK_TYPE_CONSTRAINT_ATTRIBUTE, + GTK_CONSTRAINT_ATTRIBUTE_NONE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:multiplier: + * + * The multiplication factor to be applied to the + * #GtkConstraint:source-attribue. + */ + obj_props[PROP_MULTIPLIER] = + g_param_spec_double ("multiplier", + P_("Multiplier"), + P_("The multiplication factor to be applied to the source attribute"), + -G_MAXDOUBLE, G_MAXDOUBLE, 1.0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:constant: + * + * The constant value to be added to the #GtkConstraint:source-attribute. + */ + obj_props[PROP_CONSTANT] = + g_param_spec_double ("constant", + P_("Constant"), + P_("The constant to be added to the source attribute"), + -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + /** + * GtkConstraint:strength: + * + * The strength of the constraint. + * + * The strength can be expressed either using one of the symbolic values + * of the #GtkConstraintStrength enumeration, or any positive integer + * value. + */ + obj_props[PROP_STRENGTH] = + g_param_spec_int ("strength", + P_("Strength"), + P_("The strength of the constraint"), + 0, GTK_CONSTRAINT_STRENGTH_REQUIRED, + GTK_CONSTRAINT_STRENGTH_REQUIRED, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (gobject_class, N_PROPERTIES, obj_props); +} + +static void +gtk_constraint_init (GtkConstraint *self) +{ + self->multiplier = 1.0; + self->constant = 0.0; + + self->target_attribute = GTK_CONSTRAINT_ATTRIBUTE_NONE; + self->source_attribute = GTK_CONSTRAINT_ATTRIBUTE_NONE; + self->relation = GTK_CONSTRAINT_RELATION_EQ; + self->strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; +} + +/** + * gtk_constraint_new: + * @target: (nullable) (type GtkConstraintTarget): a #GtkConstraintTarget + * @target_attribute: the attribute of @target to be set + * @relation: the relation equivalence between @target_attribute and @source_attribute + * @source: (nullable) (type GtkConstraintTarget): a #GtkConstraintTarget + * @source_attribute: the attribute of @source to be read + * @multiplier: a multiplication factor to be applied to @source_attribute + * @constant: a constant factor to be added to @source_attribute + * @strength: the strength of the constraint + * + * Creates a new #GtkConstraint representing a relation between a layout + * attribute on a source and a layout attribute on a target. + * + * Returns: the newly created #GtkConstraint + */ +GtkConstraint * +gtk_constraint_new (gpointer target, + GtkConstraintAttribute target_attribute, + GtkConstraintRelation relation, + gpointer source, + GtkConstraintAttribute source_attribute, + double multiplier, + double constant, + int strength) +{ + g_return_val_if_fail (target == NULL || GTK_IS_CONSTRAINT_TARGET (target), NULL); + g_return_val_if_fail (source == NULL || GTK_IS_CONSTRAINT_TARGET (source), NULL); + + return g_object_new (GTK_TYPE_CONSTRAINT, + "target", target, + "target-attribute", target_attribute, + "relation", relation, + "source", source, + "source-attribute", source_attribute, + "multiplier", multiplier, + "constant", constant, + "strength", strength, + NULL); +} + +/** + * gtk_constraint_new_constant: + * @target: (nullable) (type GtkConstraintTarget): a #GtkConstraintTarget + * @target_attribute: the attribute of @target to be set + * @relation: the relation equivalence between @target_attribute and @constant + * @constant: a constant factor to be set on @target_attribute + * @strength: the strength of the constraint + * + * Creates a new #GtkConstraint representing a relation between a layout + * attribute on a target and a constant value. + * + * Returns: the newly created #GtkConstraint + */ +GtkConstraint * +gtk_constraint_new_constant (gpointer target, + GtkConstraintAttribute target_attribute, + GtkConstraintRelation relation, + double constant, + int strength) +{ + g_return_val_if_fail (target == NULL || GTK_IS_CONSTRAINT_TARGET (target), NULL); + + return g_object_new (GTK_TYPE_CONSTRAINT, + "target", target, + "target-attribute", target_attribute, + "relation", relation, + "source-attribute", GTK_CONSTRAINT_ATTRIBUTE_NONE, + "constant", constant, + "strength", strength, + NULL); +} + +/** + * gtk_constraint_get_target: + * @constraint: a #GtkConstraint + * + * Retrieves the #GtkConstraintTarget used as the target for @constraint. + * + * If the #GtkConstraint:target property is set to %NULL, the @constraint + * will use the #GtkConstraintLayout's widget. + * + * Returns: (transfer none) (nullable): a #GtkConstraintTarget + */ +GtkConstraintTarget * +gtk_constraint_get_target (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), NULL); + + return constraint->target; +} + +/** + * gtk_constraint_get_target_attribute: + * @constraint: a #GtkConstraint + * + * Retrieves the attribute of the target to be set by the @constraint. + * + * Returns: the target's attribute + */ +GtkConstraintAttribute +gtk_constraint_get_target_attribute (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), GTK_CONSTRAINT_ATTRIBUTE_NONE); + + return constraint->target_attribute; +} + +/** + * gtk_constraint_get_source: + * @constraint: a #GtkConstraint + * + * Retrieves the #GtkConstraintTarget used as the source for @constraint. + * + * If the #GtkConstraint:source property is set to %NULL, the @constraint + * will use the #GtkConstraintLayout's widget. + * + * Returns: (transfer none) (nullable): a #GtkConstraintTarget + */ +GtkConstraintTarget * +gtk_constraint_get_source (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), NULL); + + return constraint->source; +} + +/** + * gtk_constraint_get_source_attribute: + * @constraint: a #GtkConstraint + * + * Retrieves the attribute of the source to be read by the @constraint. + * + * Returns: the target's attribute + */ +GtkConstraintAttribute +gtk_constraint_get_source_attribute (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), GTK_CONSTRAINT_ATTRIBUTE_NONE); + + return constraint->source_attribute; +} + +/** + * gtk_constraint_get_relation: + * @constraint: a #GtkConstraint + * + * The order relation between the terms of the @constraint. + * + * Returns: a #GtkConstraintRelation value + */ +GtkConstraintRelation +gtk_constraint_get_relation (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), GTK_CONSTRAINT_RELATION_EQ); + + return constraint->relation; +} + +/** + * gtk_constraint_get_multiplier: + * @constraint: a #GtkConstraint + * + * Retrieves the multiplication factor applied to the source + * attribute's value. + * + * Returns: a multiplication factor + */ +double +gtk_constraint_get_multiplier (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), 1.0); + + return constraint->multiplier; +} + +/** + * gtk_constraint_get_constant: + * @constraint: a #GtkConstraint + * + * Retrieves the constant factor added to the source attributes' value. + * + * Returns: a constant factor + */ +double +gtk_constraint_get_constant (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), 0.0); + + return constraint->constant; +} + +/** + * gtk_constraint_get_strength: + * @constraint: a #GtkConstraint + * + * Retrieves the strength of the constraint. + * + * Returns: the strength of the constraint + */ +int +gtk_constraint_get_strength (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), GTK_CONSTRAINT_STRENGTH_REQUIRED); + + return constraint->strength; +} + +/** + * gtk_constraint_is_required: + * @constraint: a #GtkConstraint + * + * Checks whether the @constraint is a required relation for solving the + * constraint layout. + * + * Returns: %TRUE if the constraint is required + */ +gboolean +gtk_constraint_is_required (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), FALSE); + + return constraint->strength == GTK_CONSTRAINT_STRENGTH_REQUIRED; +} + +/** + * gtk_constraint_is_attached: + * @constraint: a #GtkConstraint + * + * Checks whether the @constraint is attached to a #GtkConstraintLayout, + * and it is contributing to the layout. + * + * Returns: %TRUE if the constraint is attached + */ +gboolean +gtk_constraint_is_attached (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), FALSE); + + return constraint->constraint_ref != NULL; +} + +/** + * gtk_constraint_is_constant: + * @constraint: a #GtkConstraint + * + * Checks whether the @constraint describes a relation between an attribute + * on the #GtkConstraint:target-widget and a constant value. + * + * Returns: %TRUE if the constraint is a constant relation + */ +gboolean +gtk_constraint_is_constant (GtkConstraint *constraint) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT (constraint), FALSE); + + return constraint->source == NULL && + constraint->source_attribute == GTK_CONSTRAINT_ATTRIBUTE_NONE; +} + +void +gtk_constraint_attach (GtkConstraint *constraint, + GtkConstraintSolver *solver, + GtkConstraintRef *ref) +{ + g_return_if_fail (GTK_IS_CONSTRAINT (constraint)); + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + g_return_if_fail (ref != NULL); + + constraint->constraint_ref = ref; + constraint->solver = solver; +} + +void +gtk_constraint_detach (GtkConstraint *constraint) +{ + g_return_if_fail (GTK_IS_CONSTRAINT (constraint)); + + if (constraint->constraint_ref == NULL) + return; + + gtk_constraint_solver_remove_constraint (constraint->solver, constraint->constraint_ref); + constraint->constraint_ref = NULL; + constraint->solver = NULL; +} + +typedef struct _GtkConstraintTargetInterface GtkConstraintTargetInterface; + +struct _GtkConstraintTargetInterface +{ + GTypeInterface g_iface; +}; + +G_DEFINE_INTERFACE (GtkConstraintTarget, gtk_constraint_target, G_TYPE_OBJECT) + +static void +gtk_constraint_target_default_init (GtkConstraintTargetInterface *iface) +{ +} diff --git a/gtk/gtkconstraint.h b/gtk/gtkconstraint.h new file mode 100644 index 0000000000..2f14e66999 --- /dev/null +++ b/gtk/gtkconstraint.h @@ -0,0 +1,100 @@ +/* gtkconstraint.h: Constraint between two widgets + * Copyright 2019 GNOME Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#pragma once + +#include <gtk/gtktypes.h> +#include <gtk/gtkenums.h> + +G_BEGIN_DECLS + +typedef struct _GtkConstraintTarget GtkConstraintTarget; + +#define GTK_TYPE_CONSTRAINT_TARGET (gtk_constraint_target_get_type ()) + +/** + * GtkConstraintTarget: + * + * The GtkConstraintTarget interface is implemented by objects that + * can be used as source or target in #GtkConstraints. Besides + * #GtkWidget, it is also implemented by #GtkConstraintGuide. + */ + +GDK_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (GtkConstraintTarget, gtk_constraint_target, GTK, CONSTRAINT_TARGET, GObject) + +#define GTK_TYPE_CONSTRAINT (gtk_constraint_get_type ()) + +/** + * GtkConstraint: + * + * An object describing the relation between two widget attributes. + * + * All relations are in the form: + * + * |[<!-- language=plain --> + * target.attr_name = source.attr_name × multiplier + constant + * ]| + * + * A #GtkConstraint is immutable once it's created. + */ +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkConstraint, gtk_constraint, GTK, CONSTRAINT, GObject) + +GDK_AVAILABLE_IN_ALL +GtkConstraint * gtk_constraint_new (gpointer target, + GtkConstraintAttribute target_attribute, + GtkConstraintRelation relation, + gpointer source, + GtkConstraintAttribute source_attribute, + double multiplier, + double constant, + int strength); +GDK_AVAILABLE_IN_ALL +GtkConstraint * gtk_constraint_new_constant (gpointer target, + GtkConstraintAttribute target_attribute, + GtkConstraintRelation relation, + double constant, + int strength); + +GDK_AVAILABLE_IN_ALL +GtkConstraintTarget * gtk_constraint_get_target (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +GtkConstraintAttribute gtk_constraint_get_target_attribute (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +GtkConstraintTarget * gtk_constraint_get_source (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +GtkConstraintAttribute gtk_constraint_get_source_attribute (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +GtkConstraintRelation gtk_constraint_get_relation (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +double gtk_constraint_get_multiplier (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +double gtk_constraint_get_constant (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +int gtk_constraint_get_strength (GtkConstraint *constraint); + +GDK_AVAILABLE_IN_ALL +gboolean gtk_constraint_is_required (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +gboolean gtk_constraint_is_attached (GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +gboolean gtk_constraint_is_constant (GtkConstraint *constraint); + +G_END_DECLS diff --git a/gtk/gtkconstraintexpression.c b/gtk/gtkconstraintexpression.c new file mode 100644 index 0000000000..f3a0a37f79 --- /dev/null +++ b/gtk/gtkconstraintexpression.c @@ -0,0 +1,1833 @@ +/* gtkconstraintexpression.c: Constraint expressions and variables + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#include "config.h" + +#include "gtkconstraintexpressionprivate.h" +#include "gtkconstraintsolverprivate.h" + +/* {{{ Variables */ + +typedef enum { + GTK_CONSTRAINT_SYMBOL_DUMMY = 'd', + GTK_CONSTRAINT_SYMBOL_OBJECTIVE = 'o', + GTK_CONSTRAINT_SYMBOL_SLACK = 'S', + GTK_CONSTRAINT_SYMBOL_REGULAR = 'v' +} GtkConstraintSymbolType; + +struct _GtkConstraintVariable +{ + guint64 _id; + + GtkConstraintSymbolType _type; + + /* Interned strings */ + const char *name; + const char *prefix; + + double value; + + guint is_external : 1; + guint is_pivotable : 1; + guint is_restricted : 1; +}; + +/* Variables are sorted by a monotonic id */ +static guint64 gtk_constraint_variable_next_id; + +static void +gtk_constraint_variable_init (GtkConstraintVariable *variable, + const char *prefix, + const char *name) +{ + variable->_id = gtk_constraint_variable_next_id++; + + variable->prefix = g_intern_string (prefix); + variable->name = g_intern_string (name); + variable->prefix = NULL; + variable->value = 0.0; +} + +/*< private > + * gtk_constraint_variable_new_dummy: + * @name: the name of the variable + * + * Allocates and initializes a new #GtkConstraintVariable for a "dummy" + * symbol. Dummy symbols are typically used as markers inside a solver, + * and will not be factored in the solution when pivoting the tableau + * of the constraint equations. + * + * Only #GtkConstraintSolver should use this function. + * + * Returns: a newly allocated #GtkConstraintVariable + */ +GtkConstraintVariable * +gtk_constraint_variable_new_dummy (const char *name) +{ + GtkConstraintVariable *res = g_rc_box_new (GtkConstraintVariable); + + gtk_constraint_variable_init (res, NULL, name); + + res->_type = GTK_CONSTRAINT_SYMBOL_DUMMY; + res->is_external = FALSE; + res->is_pivotable = FALSE; + res->is_restricted = TRUE; + + return res; +} + +/*< private > + * gtk_constraint_variable_new_objective: + * @name: the name of the variable + * + * Allocates and initializes a new #GtkConstraintVariable for an objective + * symbol. This is the constant value we wish to find as the result of the + * simplex optimization. + * + * Only #GtkConstraintSolver should use this function. + * + * Returns: a newly allocated #GtkConstraintVariable + */ +GtkConstraintVariable * +gtk_constraint_variable_new_objective (const char *name) +{ + GtkConstraintVariable *res = g_rc_box_new (GtkConstraintVariable); + + gtk_constraint_variable_init (res, NULL, name); + + res->_type = GTK_CONSTRAINT_SYMBOL_OBJECTIVE; + res->is_external = FALSE; + res->is_pivotable = FALSE; + res->is_restricted = FALSE; + + return res; +} + +/*< private > + * gtk_constraint_variable_new_slack: + * @name: the name of the variable + * + * Allocates and initializes a new #GtkConstraintVariable for a "slack" + * symbol. Slack variables are introduced inside the tableau to turn + * inequalities, like: + * + * |[ + * expr ≥ 0 + * ]| + * + * Into equalities, like: + * + * |[ + * expr - slack = 0 + * ]| + * + * Only #GtkConstraintSolver should use this function. + * + * Returns: a newly allocated #GtkConstraintVariable + */ +GtkConstraintVariable * +gtk_constraint_variable_new_slack (const char *name) +{ + GtkConstraintVariable *res = g_rc_box_new (GtkConstraintVariable); + + gtk_constraint_variable_init (res, NULL, name); + + res->_type = GTK_CONSTRAINT_SYMBOL_SLACK; + res->is_external = FALSE; + res->is_pivotable = TRUE; + res->is_restricted = TRUE; + + return res; +} + +/*< private > + * gtk_constraint_variable_new: + * @prefix: (nullable): an optional prefix string for @name + * @name: (nullable): an optional name for the variable + * + * Allocates and initializes a new #GtkConstraintVariable for a regular + * symbol. All variables introduced by constraints are regular variables. + * + * Only #GtkConstraintSolver should use this function; a constraint layout + * manager should ask the #GtkConstraintSolver to create a variable, using + * gtk_constraint_solver_create_variable(), which will insert the variable + * in the solver's tableau. + * + * Returns: a newly allocated #GtkConstraintVariable + */ +GtkConstraintVariable * +gtk_constraint_variable_new (const char *prefix, + const char *name) +{ + GtkConstraintVariable *res = g_rc_box_new (GtkConstraintVariable); + + gtk_constraint_variable_init (res, prefix, name); + + res->_type = GTK_CONSTRAINT_SYMBOL_REGULAR; + res->is_external = TRUE; + res->is_pivotable = FALSE; + res->is_restricted = FALSE; + + return res; +} + +/*< private > + * gtk_constraint_variable_ref: + * @variable: a #GtkConstraintVariable + * + * Acquires a reference to @variable. + * + * Returns: (transfer full): the given #GtkConstraintVariable, with its reference + * count increased + */ +GtkConstraintVariable * +gtk_constraint_variable_ref (GtkConstraintVariable *variable) +{ + g_return_val_if_fail (variable != NULL, NULL); + + return g_rc_box_acquire (variable); +} + +/*< private > + * gtk_constraint_variable_unref: + * @variable: (transfer full): a #GtkConstraintVariable + * + * Releases a reference to @variable. + */ +void +gtk_constraint_variable_unref (GtkConstraintVariable *variable) +{ + g_return_if_fail (variable != NULL); + + g_rc_box_release (variable); +} + +/*< private > + * gtk_constraint_variable_set_value: + * @variable: a #GtkConstraintVariable + * + * Sets the current value of a #GtkConstraintVariable. + */ +void +gtk_constraint_variable_set_value (GtkConstraintVariable *variable, + double value) +{ + variable->value = value; +} + +/*< private > + * gtk_constraint_variable_get_value: + * @variable: a #GtkConstraintVariable + * + * Retrieves the current value of a #GtkConstraintVariable + * + * Returns: the value of the variable + */ +double +gtk_constraint_variable_get_value (const GtkConstraintVariable *variable) +{ + return variable->value; +} + +/*< private > + * gtk_constraint_variable_to_string: + * @variable: a #GtkConstraintVariable + * + * Turns @variable into a string, for debugging purposes. + * + * Returns: (transfer full): a string with the contents of @variable + */ +char * +gtk_constraint_variable_to_string (const GtkConstraintVariable *variable) +{ + GString *buf = g_string_new (NULL); + + if (variable == NULL) + g_string_append (buf, "<null>"); + else + { + switch (variable->_type) + { + case GTK_CONSTRAINT_SYMBOL_DUMMY: + g_string_append (buf, "(d)"); + break; + case GTK_CONSTRAINT_SYMBOL_OBJECTIVE: + g_string_append (buf, "(O)"); + break; + case GTK_CONSTRAINT_SYMBOL_SLACK: + g_string_append (buf, "(S)"); + break; + case GTK_CONSTRAINT_SYMBOL_REGULAR: + break; + + default: + g_assert_not_reached (); + } + + g_string_append_c (buf, '['); + + if (variable->prefix != NULL) + { + g_string_append (buf, variable->prefix); + g_string_append_c (buf, '.'); + } + + if (variable->name != NULL) + g_string_append (buf, variable->name); + + if (variable->_type == GTK_CONSTRAINT_SYMBOL_REGULAR) + { + char dbl_buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_ascii_dtostr (dbl_buf, G_ASCII_DTOSTR_BUF_SIZE, variable->value); + + g_string_append_c (buf, ':'); + g_string_append (buf, dbl_buf); + } + + g_string_append_c (buf, ']'); + } + + return g_string_free (buf, FALSE); +} + +/*< private > + * gtk_constraint_variable_is_external: + * @variable: a #GtkConstraintVariable + * + * Checks whether the @variable was introduced from outside the solver. + * + * Returns: %TRUE if the variable is external + */ +gboolean +gtk_constraint_variable_is_external (const GtkConstraintVariable *variable) +{ + return variable->is_external; +} + +/*< private > + * gtk_constraint_variable_is_pivotable: + * @variable: a #GtkConstraintVariable + * + * Checks whether the @variable can be used as a pivot. + * + * Returns: %TRUE if the variable is pivotable + */ +gboolean +gtk_constraint_variable_is_pivotable (const GtkConstraintVariable *variable) +{ + return variable->is_pivotable; +} + +/*< private > + * gtk_constraint_variable_is_restricted: + * @variable: a #GtkConstraintVariable + * + * Checks whether the @variable's use is restricted. + * + * Returns: %TRUE if the variable is restricted + */ +gboolean +gtk_constraint_variable_is_restricted (const GtkConstraintVariable *variable) +{ + return variable->is_restricted; +} + +/*< private > + * gtk_constraint_variable_is_dummy: + * @variable: a #GtkConstraintVariable + * + * Checks whether the @variable is a dummy symbol. + * + * Returns: %TRUE if the variable is a dummy symbol + */ +gboolean +gtk_constraint_variable_is_dummy (const GtkConstraintVariable *variable) +{ + return variable->_type == GTK_CONSTRAINT_SYMBOL_DUMMY; +} + +/*< private > + * GtkConstraintVariableSet: + * + * A set of variables. + */ +struct _GtkConstraintVariableSet { + /* List<Variable>, owns a reference */ + GSequence *set; + + /* Age of the set, to guard against mutations while iterating */ + gint64 age; +}; + +/*< private > + * gtk_constraint_variable_set_free: + * @set: a #GtkConstraintVariableSet + * + * Frees the resources associated to a #GtkConstraintVariableSet/ + */ +void +gtk_constraint_variable_set_free (GtkConstraintVariableSet *set) +{ + g_return_if_fail (set != NULL); + + g_sequence_free (set->set); + + g_free (set); +} + +/*< private > + * gtk_constraint_variable_set_new: + * + * Creates a new #GtkConstraintVariableSet. + * + * Returns: the newly created variable set + */ +GtkConstraintVariableSet * +gtk_constraint_variable_set_new (void) +{ + GtkConstraintVariableSet *res = g_new (GtkConstraintVariableSet, 1); + + res->set = g_sequence_new ((GDestroyNotify) gtk_constraint_variable_unref); + + res->age = 0; + + return res; +} + +static int +sort_by_variable_id (gconstpointer a, + gconstpointer b, + gpointer data) +{ + const GtkConstraintVariable *va = a, *vb = b; + + if (va == vb) + return 0; + + return va->_id - vb->_id; +} + +/*< private > + * gtk_constraint_variable_set_add: + * @set: a #GtkConstraintVariableSet + * @variable: a #GtkConstraintVariable + * + * Adds @variable to the given @set, if the @variable is not already + * in it. + * + * The @set will acquire a reference on the @variable, and will release + * it after calling gtk_constraint_variable_set_remove(), or when the @set + * is freed. + * + * Returns: %TRUE if the variable was added to the set, and %FALSE otherwise + */ +gboolean +gtk_constraint_variable_set_add (GtkConstraintVariableSet *set, + GtkConstraintVariable *variable) +{ + GSequenceIter *iter; + + iter = g_sequence_search (set->set, variable, sort_by_variable_id, NULL); + if (!g_sequence_iter_is_end (iter)) + { + GtkConstraintVariable *v = g_sequence_get (iter); + if (v->_id == variable->_id) + return FALSE; + } + + g_sequence_insert_before (iter, gtk_constraint_variable_ref (variable)); + + set->age += 1; + + return TRUE; +} + +/*< private > + * gtk_constraint_variable_set_remove: + * @set: a #GtkConstraintVariableSet + * @variable: a #GtkConstraintVariable + * + * Removes @variable from the @set. + * + * This function will release the reference on @variable held by the @set. + * + * Returns: %TRUE if the variable was removed from the set, and %FALSE + * otherwise + */ +gboolean +gtk_constraint_variable_set_remove (GtkConstraintVariableSet *set, + GtkConstraintVariable *variable) +{ + GSequenceIter *iter; + + iter = g_sequence_lookup (set->set, variable, sort_by_variable_id, NULL); + if (iter != NULL) + { + g_sequence_remove (iter); + set->age += 1; + + return TRUE; + } + + return FALSE; +} + +/*< private > + * gtk_constraint_variable_set_size: + * @set: a #GtkConstraintVariableSet + * + * Retrieves the size of the @set. + * + * Returns: the number of variables in the set + */ +int +gtk_constraint_variable_set_size (GtkConstraintVariableSet *set) +{ + return g_sequence_get_length (set->set); +} + +gboolean +gtk_constraint_variable_set_is_empty (GtkConstraintVariableSet *set) +{ + return g_sequence_is_empty (set->set); +} + +gboolean +gtk_constraint_variable_set_is_singleton (GtkConstraintVariableSet *set) +{ + return g_sequence_iter_next (g_sequence_get_begin_iter (set->set)) == g_sequence_get_end_iter (set->set); +} + +/*< private > + * GtkConstraintVariableSetIter: + * + * An iterator type for #GtkConstraintVariableSet. + */ +/* Keep in sync with GtkConstraintVariableSetIter */ +typedef struct { + GtkConstraintVariableSet *set; + GSequenceIter *iter; + gint64 age; +} RealVariableSetIter; + +#define REAL_VARIABLE_SET_ITER(i) ((RealVariableSetIter *) (i)) + +/*< private > + * gtk_constraint_variable_set_iter_init: + * @iter: a #GtkConstraintVariableSetIter + * @set: the #GtkConstraintVariableSet to iterate + * + * Initializes @iter for iterating over @set. + */ +void +gtk_constraint_variable_set_iter_init (GtkConstraintVariableSetIter *iter, + GtkConstraintVariableSet *set) +{ + RealVariableSetIter *riter = REAL_VARIABLE_SET_ITER (iter); + + g_return_if_fail (iter != NULL); + g_return_if_fail (set != NULL); + + riter->set = set; + riter->iter = g_sequence_get_begin_iter (set->set); + riter->age = set->age; +} + +/*< private > + * gtk_constraint_variable_set_iter_next: + * @iter: a #GtkConstraintVariableSetIter + * @variable_p: (out): the next variable in the set + * + * Advances the @iter to the next variable in the #GtkConstraintVariableSet. + * + * Returns: %TRUE if the iterator was advanced, and %FALSE otherwise + */ +gboolean +gtk_constraint_variable_set_iter_next (GtkConstraintVariableSetIter *iter, + GtkConstraintVariable **variable_p) +{ + RealVariableSetIter *riter = REAL_VARIABLE_SET_ITER (iter); + + g_return_val_if_fail (iter != NULL, FALSE); + g_return_val_if_fail (variable_p != NULL, FALSE); + + g_assert (riter->age == riter->set->age); + + if (g_sequence_iter_is_end (riter->iter)) + return FALSE; + + *variable_p = g_sequence_get (riter->iter); + riter->iter = g_sequence_iter_next (riter->iter); + + return TRUE; +} + +/*< private > + * gtk_constraint_variable_pair_new: + * @first: a #GtkConstraintVariable + * @second: a #GtkConstraintVariable + * + * Creates a new #GtkConstraintVariablePair, containint @first and @second. + * + * The #GtkConstraintVariablePair acquires a reference over the two + * given #GtkConstraintVariables. + * + * Returns: a new #GtkConstraintVariablePair + */ +GtkConstraintVariablePair * +gtk_constraint_variable_pair_new (GtkConstraintVariable *first, + GtkConstraintVariable *second) +{ + GtkConstraintVariablePair *res = g_new (GtkConstraintVariablePair, 1); + + res->first = gtk_constraint_variable_ref (first); + res->second = gtk_constraint_variable_ref (second); + + return res; +} + +/*< private > + * gtk_constraint_variable_pair_free: + * @pair: a #GtkConstraintVariablePair + * + * Frees the resources associated by @pair. + */ +void +gtk_constraint_variable_pair_free (GtkConstraintVariablePair *pair) +{ + g_clear_pointer (&pair->first, gtk_constraint_variable_unref); + g_clear_pointer (&pair->second, gtk_constraint_variable_unref); + + g_free (pair); +} + +/* }}} */ + +/* {{{ Expressions */ + +/*< private > + * Term: + * @variable: a #GtkConstraintVariable + * @coefficient: the coefficient applied to the @variable + * @next: the next term in the expression + * @prev: the previous term in the expression; + * + * A tuple of (@variable, @coefficient) in an equation. + * + * The term acquires a reference on the variable. + */ +typedef struct _Term Term; + +struct _Term { + GtkConstraintVariable *variable; + double coefficient; + + Term *next; + Term *prev; +}; + +static Term * +term_new (GtkConstraintVariable *variable, + double coefficient) +{ + Term *res = g_new (Term, 1); + + res->variable = gtk_constraint_variable_ref (variable); + res->coefficient = coefficient; + res->next = res->prev = NULL; + + return res; +} + +static void +term_free (gpointer data) +{ + Term *term = data; + + if (term == NULL) + return; + + gtk_constraint_variable_unref (term->variable); + + g_free (term); +} + +struct _GtkConstraintExpression +{ + double constant; + + /* HashTable<Variable, Term>; the key is the term's variable, + * and the value is owned by the hash table + */ + GHashTable *terms; + + /* List of terms, in insertion order */ + Term *first_term; + Term *last_term; + + /* Used by GtkConstraintExpressionIter to guard against changes + * in the expression while iterating + */ + gint64 age; +}; + +/*< private > + * gtk_constraint_expression_add_term: + * @self: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * @coefficient: a coefficient for @variable + * + * Adds a new term formed by (@variable, @coefficient) into a + * #GtkConstraintExpression. + * + * The @expression acquires a reference on @variable. + */ +static void +gtk_constraint_expression_add_term (GtkConstraintExpression *self, + GtkConstraintVariable *variable, + double coefficient) +{ + Term *term; + + if (self->terms == NULL) + { + g_assert (self->first_term == NULL && self->last_term == NULL); + self->terms = g_hash_table_new_full (NULL, NULL, + NULL, + term_free); + } + + term = term_new (variable, coefficient); + + g_hash_table_insert (self->terms, term->variable, term); + + if (self->first_term == NULL) + self->first_term = term; + + term->prev = self->last_term; + + if (self->last_term != NULL) + self->last_term->next = term; + + self->last_term = term; + + /* Increase the age of the expression, so that we can catch + * mutations from within an iteration over the terms + */ + self->age += 1; +} + +static void +gtk_constraint_expression_remove_term (GtkConstraintExpression *self, + GtkConstraintVariable *variable) +{ + Term *term, *iter; + + if (self->terms == NULL) + return; + + term = g_hash_table_lookup (self->terms, variable); + if (term == NULL) + return; + + /* Keep the variable alive for the duration of the function */ + gtk_constraint_variable_ref (variable); + + iter = self->first_term; + while (iter != NULL) + { + Term *next = iter->next; + Term *prev = iter->prev; + + if (iter == term) + { + if (prev != NULL) + prev->next = next; + if (next != NULL) + next->prev = prev; + + if (iter == self->first_term) + self->first_term = next; + if (iter == self->last_term) + self->last_term = prev; + + iter->next = NULL; + iter->prev = NULL; + + break; + } + + iter = next; + } + + g_hash_table_remove (self->terms, variable); + + gtk_constraint_variable_unref (variable); + + self->age += 1; +} + +/*< private > + * gtk_constraint_expression_new: + * @constant: a constant for the expression + * + * Creates a new #GtkConstraintExpression with the given @constant. + * + * Returns: (transfer full): the newly created expression + */ +GtkConstraintExpression * +gtk_constraint_expression_new (double constant) +{ + GtkConstraintExpression *res = g_rc_box_new (GtkConstraintExpression); + + res->age = 0; + res->terms = NULL; + res->first_term = NULL; + res->last_term = NULL; + res->constant = constant; + + return res; +} + +/*< private > + * gtk_constraint_expression_new_from_variable: + * @variable: a #GtkConstraintVariable + * + * Creates a new #GtkConstraintExpression with the given @variable. + * + * Returns: (transfer full): the newly created expression + */ +GtkConstraintExpression * +gtk_constraint_expression_new_from_variable (GtkConstraintVariable *variable) +{ + GtkConstraintExpression *res = gtk_constraint_expression_new (0.0); + + gtk_constraint_expression_add_term (res, variable, 1.0); + + return res; +} + +/*< private > + * gtk_constraint_expression_ref: + * @expression: a #GtkConstraintExpression + * + * Acquires a reference on @expression. + * + * Returns: (transfer full): the @expression, with its reference + * count increased + */ +GtkConstraintExpression * +gtk_constraint_expression_ref (GtkConstraintExpression *expression) +{ + g_return_val_if_fail (expression != NULL, NULL); + + return g_rc_box_acquire (expression); +} + +static void +gtk_constraint_expression_clear (gpointer data) +{ + GtkConstraintExpression *self = data; + + g_clear_pointer (&self->terms, g_hash_table_unref); + + self->age = 0; + self->constant = 0.0; + self->first_term = NULL; + self->last_term = NULL; +} + +/*< private > + * gtk_constraint_expression_unref: + * @expression: (transfer full): a #GtkConstraintExpression + * + * Releases a reference on @expression. + */ +void +gtk_constraint_expression_unref (GtkConstraintExpression *expression) +{ + g_rc_box_release_full (expression, gtk_constraint_expression_clear); +} + +/*< private > + * gtk_constraint_expression_is_constant: + * @expression: a #GtkConstraintExpression + * + * Checks whether @expression is a constant value, with no variable terms. + * + * Returns: %TRUE if the @expression is a constant + */ +gboolean +gtk_constraint_expression_is_constant (const GtkConstraintExpression *expression) +{ + return expression->terms == NULL; +} + +/*< private > + * gtk_constraint_expression_set_constant: + * @expression: a #GtkConstraintExpression + * @constant: the value of the constant + * + * Sets the value of the constant part of @expression. + */ +void +gtk_constraint_expression_set_constant (GtkConstraintExpression *expression, + double constant) +{ + g_return_if_fail (expression != NULL); + + expression->constant = constant; +} + +/*< private > + * gtk_constraint_expression_get_constant: + * @expression: a #GtkConstraintExpression + * + * Retrieves the constant value of @expression. + * + * Returns: the constant of @expression + */ +double +gtk_constraint_expression_get_constant (const GtkConstraintExpression *expression) +{ + g_return_val_if_fail (expression != NULL, 0.0); + + return expression->constant; +} + +GtkConstraintExpression * +gtk_constraint_expression_clone (GtkConstraintExpression *expression) +{ + GtkConstraintExpression *res; + Term *iter; + + res = gtk_constraint_expression_new (expression->constant); + + iter = expression->first_term; + while (iter != NULL) + { + gtk_constraint_expression_add_term (res, iter->variable, iter->coefficient); + + iter = iter->next; + } + + return res; +} + +/*< private > + * gtk_constraint_expression_add_variable: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable to add to @expression + * @coefficient: the coefficient of @variable + * @subject: (nullable): a #GtkConstraintVariable + * @solver: (nullable): a #GtkConstraintSolver + * + * Adds a `(@coefficient × @variable)` term to @expression. + * + * If @expression already contains a term for @variable, this function will + * update its coefficient. + * + * If @coefficient is 0 and @expression already contains a term for @variable, + * the term for @variable will be removed. + * + * This function will notify @solver if @variable is added or removed from + * the @expression. + */ +void +gtk_constraint_expression_add_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable, + double coefficient, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver) +{ + /* If the expression already contains the variable, update the coefficient */ + if (expression->terms != NULL) + { + Term *t = g_hash_table_lookup (expression->terms, variable); + + if (t != NULL) + { + double new_coefficient = t->coefficient + coefficient; + + /* Setting the coefficient to 0 will remove the variable */ + if (G_APPROX_VALUE (new_coefficient, 0.0, 0.001)) + { + /* Update the tableau if needed */ + if (solver != NULL) + gtk_constraint_solver_note_removed_variable (solver, variable, subject); + + gtk_constraint_expression_remove_term (expression, variable); + } + else + { + t->coefficient = new_coefficient; + } + + return; + } + } + + /* Otherwise, add the variable if the coefficient is non-zero */ + if (!G_APPROX_VALUE (coefficient, 0.0, 0.001)) + { + gtk_constraint_expression_add_term (expression, variable, coefficient); + + if (solver != NULL) + gtk_constraint_solver_note_added_variable (solver, variable, subject); + } +} + +/*< private > + * gtk_constraint_expression_remove_variable: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * + * Removes @variable from @expression. + */ +void +gtk_constraint_expression_remove_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable) +{ + g_return_if_fail (expression != NULL); + g_return_if_fail (variable != NULL); + + gtk_constraint_expression_remove_term (expression, variable); +} + +/*< private > + * gtk_constraint_expression_set_variable: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * @coefficient: a coefficient for @variable + * + * Sets the @coefficient for @variable inside an @expression. + * + * If the @expression does not contain a term for @variable, a new + * one will be added. + */ +void +gtk_constraint_expression_set_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable, + double coefficient) +{ + if (expression->terms != NULL) + { + Term *t = g_hash_table_lookup (expression->terms, variable); + + if (t != NULL) + { + t->coefficient = coefficient; + return; + } + } + + gtk_constraint_expression_add_term (expression, variable, coefficient); +} + +/*< private > + * gtk_constraint_expression_add_expression: + * @a_expr: first operand + * @b_expr: second operand + * @n: the multiplication factor for @b_expr + * @subject: (nullable): a #GtkConstraintVariable + * @solver: (nullable): a #GtkConstraintSolver + * + * Adds `(@n × @b_expr)` to @a_expr. + * + * Typically, this function is used to turn two expressions in the + * form: + * + * |[ + * a.x + a.width = b.x + b.width + * ]| + * + * into a single expression: + * + * |[ + * a.x + a.width - b.x - b.width = 0 + * ]| + * + * If @solver is not %NULL, this function will notify a #GtkConstraintSolver + * of every variable that was added or removed from @a_expr. + */ +void +gtk_constraint_expression_add_expression (GtkConstraintExpression *a_expr, + GtkConstraintExpression *b_expr, + double n, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver) +{ + Term *iter; + + a_expr->constant += (n * b_expr->constant); + + iter = b_expr->last_term; + while (iter != NULL) + { + Term *next = iter->prev; + + gtk_constraint_expression_add_variable (a_expr, + iter->variable, n * iter->coefficient, + subject, + solver); + + iter = next; + } +} + +/*< private > + * gtk_constraint_expression_plus_constant: + * @expression: a #GtkConstraintExpression + * @constant: a constant value + * + * Adds a @constant value to the @expression. + * + * This is the equivalent of creating a new #GtkConstraintExpression for + * the @constant and calling gtk_constraint_expression_add_expression(). + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_plus_constant (GtkConstraintExpression *expression, + double constant) +{ + GtkConstraintExpression *e; + + e = gtk_constraint_expression_new (constant); + gtk_constraint_expression_add_expression (expression, e, 1.0, NULL, NULL); + gtk_constraint_expression_unref (e); + + return expression; +} + +/*< private > + * gtk_constraint_expression_minus_constant: + * @expression: a #GtkConstraintExpression + * @constant: a constant value + * + * Removes a @constant value from the @expression. + * + * This is the equivalent of creating a new #GtkConstraintExpression for + * the inverse of @constant and calling gtk_constraint_expression_add_expression(). + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_minus_constant (GtkConstraintExpression *expression, + double constant) +{ + return gtk_constraint_expression_plus_constant (expression, constant * -1.0); +} + +/*< private > + * gtk_constraint_expression_plus_variable: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * + * Adds a @variable to the @expression. + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_plus_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable) +{ + GtkConstraintExpression *e; + + e = gtk_constraint_expression_new_from_variable (variable); + gtk_constraint_expression_add_expression (expression, e, 1.0, NULL, NULL); + gtk_constraint_expression_unref (e); + + return expression; +} + +/*< private > + * gtk_constraint_expression_minus_variable: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * + * Subtracts a @variable from the @expression. + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_minus_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable) +{ + GtkConstraintExpression *e; + + e = gtk_constraint_expression_new_from_variable (variable); + gtk_constraint_expression_add_expression (expression, e, -1.0, NULL, NULL); + gtk_constraint_expression_unref (e); + + return expression; +} + +/*< private > + * gtk_constraint_expression_multiply_by: + * @expression: a #GtkConstraintExpression + * @factor: the multiplication factor + * + * Multiplies the constant part and the coefficient of all terms + * in @expression with the given @factor. + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_multiply_by (GtkConstraintExpression *expression, + double factor) +{ + GHashTableIter iter; + gpointer value_p; + + expression->constant *= factor; + + if (expression->terms == NULL) + return expression; + + g_hash_table_iter_init (&iter, expression->terms); + while (g_hash_table_iter_next (&iter, NULL, &value_p)) + { + Term *t = value_p; + + t->coefficient *= factor; + } + + return expression; +} + +/*< private > + * gtk_constraint_expression_divide_by: + * @expression: a #GtkConstraintExpression + * @factor: the division factor + * + * Divides the constant part and the coefficient of all terms + * in @expression by the given @factor. + * + * Returns: the @expression + */ +GtkConstraintExpression * +gtk_constraint_expression_divide_by (GtkConstraintExpression *expression, + double factor) +{ + if (G_APPROX_VALUE (factor, 0.0, 0.001)) + return expression; + + return gtk_constraint_expression_multiply_by (expression, 1.0 / factor); +} + +/*< private > + * gtk_constraint_expression_new_subject: + * @expression: a #GtkConstraintExpression + * @subject: a #GtkConstraintVariable part of @expression + * + * Modifies @expression to have a new @subject. + * + * A #GtkConstraintExpression is a linear expression in the form of + * `@expression = 0`. If @expression contains @subject, for instance: + * + * |[ + * c + (a × @subject) + (a1 × v1) + … + (an × vn) = 0 + * ]| + * + * this function will make @subject the new subject of the expression: + * + * |[ + * subject = - (c / a) - ((a1 / a) × v1) - … - ((an / a) × vn) = 0 + * ]| + * + * The term @subject is removed from the @expression. + * + * Returns: the reciprocal of the coefficient of @subject, so we + * can use this function in gtk_constraint_expression_change_subject() + */ +double +gtk_constraint_expression_new_subject (GtkConstraintExpression *expression, + GtkConstraintVariable *subject) +{ + double reciprocal = 1.0; + Term *term; + + g_assert (!gtk_constraint_expression_is_constant (expression)); + + term = g_hash_table_lookup (expression->terms, subject); + g_assert (term != NULL); + g_assert (!G_APPROX_VALUE (term->coefficient, 0.0, 0.001)); + + reciprocal = 1.0 / term->coefficient; + + gtk_constraint_expression_remove_term (expression, subject); + gtk_constraint_expression_multiply_by (expression, -reciprocal); + + return reciprocal; +} + +/*< private > + * gtk_constraint_expression_change_subject: + * @expression: a #GtkConstraintExpression + * @old_subject: the old subject #GtkConstraintVariable of @expression + * @new_subject: the new subject #GtkConstraintVariable of @expression + * + * Turns an @expression in the form of: + * + * |[ + * old_subject = c + (a × new_subject) + (a1 × v1) + … + (an × vn) + * ]| + * + * into the form of: + * + * |[ + * new_subject = -c / a + old_subject / a - ((a1 / a) × v1) - … - ((an / a) × vn) + * ]| + * + * Which means resolving @expression for @new_subject. + */ +void +gtk_constraint_expression_change_subject (GtkConstraintExpression *expression, + GtkConstraintVariable *old_subject, + GtkConstraintVariable *new_subject) +{ + double reciprocal; + + g_return_if_fail (expression != NULL); + g_return_if_fail (old_subject != NULL); + g_return_if_fail (new_subject != NULL); + + reciprocal = gtk_constraint_expression_new_subject (expression, new_subject); + gtk_constraint_expression_set_variable (expression, old_subject, reciprocal); +} + +/*< private > + * gtk_constraint_expression_get_coefficient: + * @expression: a #GtkConstraintExpression + * @variable: a #GtkConstraintVariable + * + * Retrieves the coefficient of the term for @variable inside @expression. + * + * Returns: the coefficient of @variable + */ +double +gtk_constraint_expression_get_coefficient (GtkConstraintExpression *expression, + GtkConstraintVariable *variable) +{ + const Term *term; + + g_return_val_if_fail (expression != NULL, 0.0); + g_return_val_if_fail (variable != NULL, 0.0); + + if (expression->terms == NULL) + return 0.0; + + term = g_hash_table_lookup (expression->terms, variable); + if (term == NULL) + return 0.0; + + return term->coefficient; +} + +/*< private > + * gtk_constraint_expression_substitute_out: + * @expression: a #GtkConstraintExpression + * @out_var: the variable to replace + * @expr: the expression used to replace @out_var + * @subject: (nullable): a #GtkConstraintVariable + * @solver: (nullable): a #GtkConstraintSolver + * + * Replaces every term containing @out_var inside @expression with @expr. + * + * If @solver is not %NULL, this function will notify the #GtkConstraintSolver + * for every variable added to or removed from @expression. + */ +void +gtk_constraint_expression_substitute_out (GtkConstraintExpression *expression, + GtkConstraintVariable *out_var, + GtkConstraintExpression *expr, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver) +{ + double multiplier; + Term *iter; + + if (expression->terms == NULL) + return; + + multiplier = gtk_constraint_expression_get_coefficient (expression, out_var); + gtk_constraint_expression_remove_term (expression, out_var); + + expression->constant = expression->constant + multiplier * expr->constant; + + iter = expr->first_term; + while (iter != NULL) + { + GtkConstraintVariable *clv = iter->variable; + double coeff = iter->coefficient; + Term *next = iter->next; + + if (expression->terms != NULL && + g_hash_table_contains (expression->terms, clv)) + { + double old_coefficient = gtk_constraint_expression_get_coefficient (expression, clv); + double new_coefficient = old_coefficient + multiplier * coeff; + + if (G_APPROX_VALUE (new_coefficient, 0.0, 0.001)) + { + if (solver != NULL) + gtk_constraint_solver_note_removed_variable (solver, clv, subject); + + gtk_constraint_expression_remove_term (expression, clv); + } + else + gtk_constraint_expression_set_variable (expression, clv, new_coefficient); + } + else + { + gtk_constraint_expression_set_variable (expression, clv, multiplier * coeff); + + if (solver != NULL) + gtk_constraint_solver_note_added_variable (solver, clv, subject); + } + + iter = next; + } +} + +/*< private > + * gtk_constraint_expression_get_pivotable_variable: + * @expression: a #GtkConstraintExpression + * + * Retrieves the first #GtkConstraintVariable in @expression that + * is marked as pivotable. + * + * Returns: (transfer none) (nullable): a #GtkConstraintVariable + */ +GtkConstraintVariable * +gtk_constraint_expression_get_pivotable_variable (GtkConstraintExpression *expression) +{ + Term *iter; + + if (expression->terms == NULL) + { + g_critical ("Expression %p is a constant", expression); + return NULL; + } + + iter = expression->first_term; + while (iter != NULL) + { + Term *next = iter->next; + + if (gtk_constraint_variable_is_pivotable (iter->variable)) + return iter->variable; + + iter = next; + } + + return NULL; +} + +/*< private > + * gtk_constraint_expression_to_string: + * @expression: a #GtkConstraintExpression + * + * Creates a string containing @expression. + * + * This function is only useful for debugging. + * + * Returns: (transfer full): a string containing the given expression + */ +char * +gtk_constraint_expression_to_string (const GtkConstraintExpression *expression) +{ + gboolean needs_plus = FALSE; + GString *buf; + Term *iter; + + if (expression == NULL) + return g_strdup ("<null>"); + + buf = g_string_new (NULL); + + if (!G_APPROX_VALUE (expression->constant, 0.0, 0.001)) + { + g_string_append_printf (buf, "%g", expression->constant); + + if (expression->terms != NULL) + needs_plus = TRUE; + } + + if (expression->terms == NULL) + return g_string_free (buf, FALSE); + + iter = expression->first_term; + while (iter != NULL) + { + char *str = gtk_constraint_variable_to_string (iter->variable); + Term *next = iter->next; + + if (needs_plus) + g_string_append (buf, " + "); + + if (G_APPROX_VALUE (iter->coefficient, 1.0, 0.001)) + g_string_append_printf (buf, "%s", str); + else + g_string_append_printf (buf, "(%g * %s)", iter->coefficient, str); + + g_free (str); + + if (!needs_plus) + needs_plus = TRUE; + + iter = next; + } + + return g_string_free (buf, FALSE); +} + +/* Keep in sync with GtkConstraintExpressionIter */ +typedef struct { + GtkConstraintExpression *expression; + Term *current; + gint64 age; +} RealExpressionIter; + +#define REAL_EXPRESSION_ITER(i) ((RealExpressionIter *) (i)) + +/*< private > + * gtk_constraint_expression_iter_init: + * @iter: a #GtkConstraintExpressionIter + * @expression: a #GtkConstraintExpression + * + * Initializes an iterator over @expression. + */ +void +gtk_constraint_expression_iter_init (GtkConstraintExpressionIter *iter, + GtkConstraintExpression *expression) +{ + RealExpressionIter *riter = REAL_EXPRESSION_ITER (iter); + + riter->expression = expression; + riter->current = NULL; + riter->age = expression->age; +} + +/*< private > + * gtk_constraint_expression_iter_next: + * @iter: a valid #GtkConstraintExpressionIter + * @variable: (out): the variable of the next term + * @coefficient: (out): the coefficient of the next term + * + * Moves the given #GtkConstraintExpressionIter forwards to the next + * term in the expression, starting from the first term. + * + * Returns: %TRUE if the iterator was moved, and %FALSE if the iterator + * has reached the end of the terms of the expression + */ +gboolean +gtk_constraint_expression_iter_next (GtkConstraintExpressionIter *iter, + GtkConstraintVariable **variable, + double *coefficient) +{ + RealExpressionIter *riter = REAL_EXPRESSION_ITER (iter); + + g_assert (riter->age == riter->expression->age); + + if (riter->current == NULL) + riter->current = riter->expression->first_term; + else + riter->current = riter->current->next; + + if (riter->current != NULL) + { + *coefficient = riter->current->coefficient; + *variable = riter->current->variable; + } + + return riter->current != NULL; +} + +/*< private > + * gtk_constraint_expression_iter_prev: + * @iter: a valid #GtkConstraintExpressionIter + * @variable: (out): the variable of the previous term + * @coefficient: (out): the coefficient of the previous term + * + * Moves the given #GtkConstraintExpressionIter backwards to the previous + * term in the expression, starting from the last term. + * + * Returns: %TRUE if the iterator was moved, and %FALSE if the iterator + * has reached the beginning of the terms of the expression + */ +gboolean +gtk_constraint_expression_iter_prev (GtkConstraintExpressionIter *iter, + GtkConstraintVariable **variable, + double *coefficient) +{ + RealExpressionIter *riter = REAL_EXPRESSION_ITER (iter); + + g_assert (riter->age == riter->expression->age); + + if (riter->current == NULL) + riter->current = riter->expression->last_term; + else + riter->current = riter->current->prev; + + if (riter->current != NULL) + { + *coefficient = riter->current->coefficient; + *variable = riter->current->variable; + } + + return riter->current != NULL; +} + +typedef enum { + BUILDER_OP_NONE, + BUILDER_OP_PLUS, + BUILDER_OP_MINUS, + BUILDER_OP_MULTIPLY, + BUILDER_OP_DIVIDE +} BuilderOpType; + +typedef struct +{ + GtkConstraintExpression *expression; + GtkConstraintSolver *solver; + int op; +} RealExpressionBuilder; + +#define REAL_EXPRESSION_BUILDER(b) ((RealExpressionBuilder *) (b)) + +/*< private > + * gtk_constraint_expression_builder_init: + * @builder: a #GtkConstraintExpressionBuilder + * @solver: a #GtkConstraintSolver + * + * Initializes the given #GtkConstraintExpressionBuilder for the + * given #GtkConstraintSolver. + * + * You can use the @builder to construct expressions to be added to the + * @solver, in the form of constraints. + * + * A typical use is: + * + * |[<!-- language="C" --> + * GtkConstraintExpressionBuilder builder; + * + * // "solver" is set in another part of the code + * gtk_constraint_expression_builder_init (&builder, solver); + * + * // "width" is set in another part of the code + * gtk_constraint_expression_builder_term (&builder, width); + * gtk_constraint_expression_builder_divide_by (&builder); + * gtk_constraint_expression_builder_constant (&builder, 2.0); + * + * // "left" is set in another part of the code + * gtk_constraint_expression_builder_plus (&builder); + * gtk_constraint_expression_builder_term (&builder, left); + * + * // "expr" now contains the following expression: + * // width / 2.0 + left + * GtkConstraintExpression *expr = + * gtk_constraint_expression_builder_finish (&builder); + * + * // The builder is inert, and can be re-used by calling + * // gtk_constraint_expression_builder_init() again. + * ]| + */ +void +gtk_constraint_expression_builder_init (GtkConstraintExpressionBuilder *builder, + GtkConstraintSolver *solver) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->solver = solver; + rbuilder->expression = gtk_constraint_expression_new (0); + rbuilder->op = BUILDER_OP_NONE; +} + +/*< private > + * gtk_constraint_expression_builder_term: + * @builder: a #GtkConstraintExpressionBuilder + * @term: a #GtkConstraintVariable + * + * Adds a variable @term to the @builder. + */ +void +gtk_constraint_expression_builder_term (GtkConstraintExpressionBuilder *builder, + GtkConstraintVariable *term) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + GtkConstraintExpression *expr; + + expr = gtk_constraint_expression_new_from_variable (term); + + switch (rbuilder->op) + { + case BUILDER_OP_NONE: + g_clear_pointer (&rbuilder->expression, gtk_constraint_expression_unref); + rbuilder->expression = g_steal_pointer (&expr); + break; + + case BUILDER_OP_PLUS: + gtk_constraint_expression_add_expression (rbuilder->expression, + expr, 1.0, + NULL, + NULL); + gtk_constraint_expression_unref (expr); + break; + + case BUILDER_OP_MINUS: + gtk_constraint_expression_add_expression (rbuilder->expression, + expr, -1.0, + NULL, + NULL); + gtk_constraint_expression_unref (expr); + break; + + default: + break; + } + + rbuilder->op = BUILDER_OP_NONE; +} + +/*< private > + * gtk_constraint_expression_builder_plus: + * @builder: a #GtkConstraintExpressionBuilder + * + * Adds a plus operator to the @builder. + */ +void +gtk_constraint_expression_builder_plus (GtkConstraintExpressionBuilder *builder) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->op = BUILDER_OP_PLUS; +} + +/*< private > + * gtk_constraint_expression_builder_minus: + * @builder: a #GtkConstraintExpressionBuilder + * + * Adds a minus operator to the @builder. + */ +void +gtk_constraint_expression_builder_minus (GtkConstraintExpressionBuilder *builder) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->op = BUILDER_OP_MINUS; +} + +/*< private > + * gtk_constraint_expression_builder_divide_by: + * @builder: a #GtkConstraintExpressionBuilder + * + * Adds a division operator to the @builder. + */ +void +gtk_constraint_expression_builder_divide_by (GtkConstraintExpressionBuilder *builder) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->op = BUILDER_OP_DIVIDE; +} + +/*< private > + * gtk_constraint_expression_builder_multiply_by: + * @builder: a #GtkConstraintExpressionBuilder + * + * Adds a multiplication operator to the @builder. + */ +void +gtk_constraint_expression_builder_multiply_by (GtkConstraintExpressionBuilder *builder) + +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->op = BUILDER_OP_MULTIPLY; +} + +/*< private > + * gtk_constraint_expression_builder_constant: + * @builder: a #GtkConstraintExpressionBuilder + * @value: a constant value + * + * Adds a constant value to the @builder. + */ +void +gtk_constraint_expression_builder_constant (GtkConstraintExpressionBuilder *builder, + double value) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + switch (rbuilder->op) + { + case BUILDER_OP_NONE: + gtk_constraint_expression_set_constant (rbuilder->expression, value); + break; + + case BUILDER_OP_PLUS: + gtk_constraint_expression_plus_constant (rbuilder->expression, value); + break; + + case BUILDER_OP_MINUS: + gtk_constraint_expression_minus_constant (rbuilder->expression, value); + break; + + case BUILDER_OP_MULTIPLY: + gtk_constraint_expression_multiply_by (rbuilder->expression, value); + break; + + case BUILDER_OP_DIVIDE: + gtk_constraint_expression_divide_by (rbuilder->expression, value); + break; + + default: + break; + } + + rbuilder->op = BUILDER_OP_NONE; +} + +/*< private > + * gtk_constraint_expression_builder_finish: + * @builder: a #GtkConstraintExpressionBuilder + * + * Closes the given expression builder, and returns the expression. + * + * You can only call this function once. + * + * Returns: (transfer full): the built expression + */ +GtkConstraintExpression * +gtk_constraint_expression_builder_finish (GtkConstraintExpressionBuilder *builder) +{ + RealExpressionBuilder *rbuilder = REAL_EXPRESSION_BUILDER (builder); + + rbuilder->solver = NULL; + rbuilder->op = BUILDER_OP_NONE; + + return g_steal_pointer (&rbuilder->expression); +} + +/* }}} */ diff --git a/gtk/gtkconstraintexpressionprivate.h b/gtk/gtkconstraintexpressionprivate.h new file mode 100644 index 0000000000..942915ad38 --- /dev/null +++ b/gtk/gtkconstraintexpressionprivate.h @@ -0,0 +1,279 @@ +/* gtkconstraintequationprivate.h: Constraint expressions and variables + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#pragma once + +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +GtkConstraintVariable * +gtk_constraint_variable_new (const char *prefix, + const char *name); + +GtkConstraintVariable * +gtk_constraint_variable_new_dummy (const char *name); + +GtkConstraintVariable * +gtk_constraint_variable_new_objective (const char *name); + +GtkConstraintVariable * +gtk_constraint_variable_new_slack (const char *name); + +GtkConstraintVariable * +gtk_constraint_variable_ref (GtkConstraintVariable *variable); + +void +gtk_constraint_variable_unref (GtkConstraintVariable *variable); + +void +gtk_constraint_variable_set_value (GtkConstraintVariable *variable, + double value); + +double +gtk_constraint_variable_get_value (const GtkConstraintVariable *variable); + +char * +gtk_constraint_variable_to_string (const GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_is_external (const GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_is_pivotable (const GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_is_restricted (const GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_is_dummy (const GtkConstraintVariable *variable); + +typedef struct { + GtkConstraintVariable *first; + GtkConstraintVariable *second; +} GtkConstraintVariablePair; + +GtkConstraintVariablePair * +gtk_constraint_variable_pair_new (GtkConstraintVariable *first, + GtkConstraintVariable *second); + +void +gtk_constraint_variable_pair_free (GtkConstraintVariablePair *pair); + +typedef struct _GtkConstraintVariableSet GtkConstraintVariableSet; + +GtkConstraintVariableSet * +gtk_constraint_variable_set_new (void); + +void +gtk_constraint_variable_set_free (GtkConstraintVariableSet *set); + +gboolean +gtk_constraint_variable_set_add (GtkConstraintVariableSet *set, + GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_set_remove (GtkConstraintVariableSet *set, + GtkConstraintVariable *variable); + +gboolean +gtk_constraint_variable_set_is_empty (GtkConstraintVariableSet *set); + +gboolean +gtk_constraint_variable_set_is_singleton (GtkConstraintVariableSet *set); + +int +gtk_constraint_variable_set_size (GtkConstraintVariableSet *set); + +typedef struct { + /*< private >*/ + gpointer dummy1; + gpointer dummy2; + gint64 dummy3; +} GtkConstraintVariableSetIter; + +void +gtk_constraint_variable_set_iter_init (GtkConstraintVariableSetIter *iter, + GtkConstraintVariableSet *set); + +gboolean +gtk_constraint_variable_set_iter_next (GtkConstraintVariableSetIter *iter, + GtkConstraintVariable **variable_p); + +GtkConstraintExpression * +gtk_constraint_expression_new (double constant); + +GtkConstraintExpression * +gtk_constraint_expression_new_from_variable (GtkConstraintVariable *variable); + +GtkConstraintExpression * +gtk_constraint_expression_ref (GtkConstraintExpression *expression); + +void +gtk_constraint_expression_unref (GtkConstraintExpression *expression); + +GtkConstraintExpression * +gtk_constraint_expression_clone (GtkConstraintExpression *expression); + +void +gtk_constraint_expression_set_constant (GtkConstraintExpression *expression, + double constant); +double +gtk_constraint_expression_get_constant (const GtkConstraintExpression *expression); + +gboolean +gtk_constraint_expression_is_constant (const GtkConstraintExpression *expression); + +void +gtk_constraint_expression_add_expression (GtkConstraintExpression *a_expr, + GtkConstraintExpression *b_expr, + double n, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver); + +void +gtk_constraint_expression_add_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable, + double coefficient, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver); + +void +gtk_constraint_expression_remove_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable); + +void +gtk_constraint_expression_set_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable, + double coefficient); + +double +gtk_constraint_expression_get_coefficient (GtkConstraintExpression *expression, + GtkConstraintVariable *variable); + +char * +gtk_constraint_expression_to_string (const GtkConstraintExpression *expression); + +double +gtk_constraint_expression_new_subject (GtkConstraintExpression *expression, + GtkConstraintVariable *subject); + +void +gtk_constraint_expression_change_subject (GtkConstraintExpression *expression, + GtkConstraintVariable *old_subject, + GtkConstraintVariable *new_subject); + +void +gtk_constraint_expression_substitute_out (GtkConstraintExpression *expression, + GtkConstraintVariable *out_var, + GtkConstraintExpression *expr, + GtkConstraintVariable *subject, + GtkConstraintSolver *solver); + +GtkConstraintVariable * +gtk_constraint_expression_get_pivotable_variable (GtkConstraintExpression *expression); + +GtkConstraintExpression * +gtk_constraint_expression_plus_constant (GtkConstraintExpression *expression, + double constant); + +GtkConstraintExpression * +gtk_constraint_expression_minus_constant (GtkConstraintExpression *expression, + double constant); + +GtkConstraintExpression * +gtk_constraint_expression_plus_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable); + +GtkConstraintExpression * +gtk_constraint_expression_minus_variable (GtkConstraintExpression *expression, + GtkConstraintVariable *variable); + +GtkConstraintExpression * +gtk_constraint_expression_multiply_by (GtkConstraintExpression *expression, + double factor); + +GtkConstraintExpression * +gtk_constraint_expression_divide_by (GtkConstraintExpression *expression, + double factor); + +struct _GtkConstraintExpressionBuilder +{ + /*< private >*/ + gpointer dummy1; + gpointer dummy2; + int dummy3; +}; + +void +gtk_constraint_expression_builder_init (GtkConstraintExpressionBuilder *builder, + GtkConstraintSolver *solver); + +void +gtk_constraint_expression_builder_term (GtkConstraintExpressionBuilder *builder, + GtkConstraintVariable *term); + +void +gtk_constraint_expression_builder_plus (GtkConstraintExpressionBuilder *builder); + +void +gtk_constraint_expression_builder_minus (GtkConstraintExpressionBuilder *builder); + +void +gtk_constraint_expression_builder_divide_by (GtkConstraintExpressionBuilder *builder); + +void +gtk_constraint_expression_builder_multiply_by (GtkConstraintExpressionBuilder *builder); + +void +gtk_constraint_expression_builder_constant (GtkConstraintExpressionBuilder *builder, + double value); + +GtkConstraintExpression * +gtk_constraint_expression_builder_finish (GtkConstraintExpressionBuilder *builder) G_GNUC_WARN_UNUSED_RESULT; + +/*< private > + * GtkConstraintExpressionIter: + * + * An iterator object for terms inside a #GtkConstraintExpression. + */ +typedef struct { + /*< private >*/ + gpointer dummy1; + gpointer dummy2; + gint64 dummy3; +} GtkConstraintExpressionIter; + +void +gtk_constraint_expression_iter_init (GtkConstraintExpressionIter *iter, + GtkConstraintExpression *equation); + +gboolean +gtk_constraint_expression_iter_next (GtkConstraintExpressionIter *iter, + GtkConstraintVariable **variable, + double *coefficient); + +gboolean +gtk_constraint_expression_iter_prev (GtkConstraintExpressionIter *iter, + GtkConstraintVariable **variable, + double *coefficient); + +G_END_DECLS diff --git a/gtk/gtkconstraintguide.c b/gtk/gtkconstraintguide.c new file mode 100644 index 0000000000..de7bb03499 --- /dev/null +++ b/gtk/gtkconstraintguide.c @@ -0,0 +1,694 @@ +/* gtkconstraintguide.c: Flexible space for constraints + * Copyright 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Matthias Clasen + */ + +/** + * SECTION:gtkconstraintguide + * @Title: GtkConstraintGuide + * @Short_description: An invisible constraint target + * + * A #GtkConstraintGuide is an invisible layout element that can be + * used by widgets inside a #GtkConstraintLayout as a source or a target + * of a #GtkConstraint. Guides can be used like guidelines or as + * flexible space. + * + * Unlike a #GtkWidget, a #GtkConstraintGuide will not be drawn. + */ + +#include "config.h" + +#include "gtkconstraintguide.h" + +#include "gtkconstraintguideprivate.h" +#include "gtkconstraintlayoutprivate.h" +#include "gtkconstraintexpressionprivate.h" +#include "gtkconstraintsolverprivate.h" + +#include "gtkdebug.h" +#include "gtkintl.h" +#include "gtkprivate.h" + + +typedef enum { + MIN_WIDTH, + MIN_HEIGHT, + NAT_WIDTH, + NAT_HEIGHT, + MAX_WIDTH, + MAX_HEIGHT, + LAST_VALUE +} GuideValue; + +struct _GtkConstraintGuide +{ + GObject parent_instance; + + char *name; + + int strength; + + int values[LAST_VALUE]; + + GtkConstraintLayout *layout; + + /* HashTable<static string, Variable>; a hash table of variables, + * one for each attribute; we use these to query and suggest the + * values for the solver. The string is static and does not need + * to be freed. + */ + GHashTable *bound_attributes; + + GtkConstraintRef *constraints[LAST_VALUE]; +}; + + +struct _GtkConstraintGuideClass { + GObjectClass parent_class; +}; + +enum { + PROP_MIN_WIDTH = 1, + PROP_MIN_HEIGHT, + PROP_NAT_WIDTH, + PROP_NAT_HEIGHT, + PROP_MAX_WIDTH, + PROP_MAX_HEIGHT, + PROP_STRENGTH, + PROP_NAME, + LAST_PROP +}; + +static GParamSpec *guide_props[LAST_PROP]; + +static void +gtk_constraint_guide_constraint_target_iface_init (GtkConstraintTargetInterface *iface) +{ +} + +G_DEFINE_TYPE_WITH_CODE (GtkConstraintGuide, gtk_constraint_guide, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_CONSTRAINT_TARGET, + gtk_constraint_guide_constraint_target_iface_init)) + +static void +gtk_constraint_guide_init (GtkConstraintGuide *guide) +{ + guide->strength = GTK_CONSTRAINT_STRENGTH_MEDIUM; + + guide->values[MIN_WIDTH] = 0; + guide->values[MIN_HEIGHT] = 0; + guide->values[NAT_WIDTH] = 0; + guide->values[NAT_HEIGHT] = 0; + guide->values[MAX_WIDTH] = G_MAXINT; + guide->values[MAX_HEIGHT] = G_MAXINT; + + guide->bound_attributes = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify) gtk_constraint_variable_unref); +} + +static void +gtk_constraint_guide_update_constraint (GtkConstraintGuide *guide, + GuideValue index) +{ + GtkConstraintSolver *solver; + GtkConstraintVariable *var; + + if (!guide->layout) + return; + + solver = gtk_constraint_layout_get_solver (guide->layout); + if (!solver) + return; + + if (guide->constraints[index] != NULL) + { + gtk_constraint_solver_remove_constraint (solver, guide->constraints[index]); + guide->constraints[index] = NULL; + } + + if (index == MIN_WIDTH || index == NAT_WIDTH || index == MAX_WIDTH) + var = gtk_constraint_layout_get_attribute (guide->layout, GTK_CONSTRAINT_ATTRIBUTE_WIDTH, "guide", NULL, guide->bound_attributes); + else + var = gtk_constraint_layout_get_attribute (guide->layout, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, "guide", NULL, guide->bound_attributes); + + /* We always install min-size constraints, + * but we avoid nat-size constraints if min == max + * and we avoid max-size constraints if max == G_MAXINT + */ + if (index == MIN_WIDTH || index == MIN_HEIGHT) + { + guide->constraints[index] = + gtk_constraint_solver_add_constraint (solver, + var, + GTK_CONSTRAINT_RELATION_GE, + gtk_constraint_expression_new (guide->values[index]), + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + else if ((index == NAT_WIDTH && guide->values[MIN_WIDTH] != guide->values[MAX_WIDTH]) || + (index == NAT_HEIGHT && guide->values[MIN_HEIGHT] != guide->values[MAX_HEIGHT])) + { + gtk_constraint_variable_set_value (var, guide->values[index]); + guide->constraints[index] = + gtk_constraint_solver_add_stay_variable (solver, + var, + guide->strength); + } + else if ((index == MAX_WIDTH || index == MAX_HEIGHT) && + guide->values[index] < G_MAXINT) + { + guide->constraints[index] = + gtk_constraint_solver_add_constraint (solver, + var, + GTK_CONSTRAINT_RELATION_LE, + gtk_constraint_expression_new (guide->values[index]), + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (guide->layout)); +} + +void +gtk_constraint_guide_update (GtkConstraintGuide *guide) +{ + int i; + + for (i = 0; i < LAST_VALUE; i++) + gtk_constraint_guide_update_constraint (guide, i); +} + +void +gtk_constraint_guide_detach (GtkConstraintGuide *guide) +{ + GtkConstraintSolver *solver; + int i; + + if (!guide->layout) + return; + + solver = gtk_constraint_layout_get_solver (guide->layout); + if (!solver) + return; + + for (i = 0; i < LAST_VALUE; i++) + { + if (guide->constraints[i]) + { + gtk_constraint_solver_remove_constraint (solver, guide->constraints[i]); + guide->constraints[i] = NULL; + } + } + + g_hash_table_remove_all (guide->bound_attributes); +} + +GtkConstraintVariable * +gtk_constraint_guide_get_attribute (GtkConstraintGuide *guide, + GtkConstraintAttribute attr) +{ + GtkLayoutManager *manager = GTK_LAYOUT_MANAGER (guide->layout); + GtkWidget *widget = gtk_layout_manager_get_widget (manager); + + return gtk_constraint_layout_get_attribute (guide->layout, attr, "guide", widget, guide->bound_attributes); +} + +GtkConstraintLayout * +gtk_constraint_guide_get_layout (GtkConstraintGuide *guide) +{ + return guide->layout; +} + +void +gtk_constraint_guide_set_layout (GtkConstraintGuide *guide, + GtkConstraintLayout *layout) +{ + guide->layout = layout; +} + +static void +gtk_constraint_guide_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkConstraintGuide *self = GTK_CONSTRAINT_GUIDE (gobject); + int val; + GuideValue index; + + switch (prop_id) + { + case PROP_MIN_WIDTH: + case PROP_MIN_HEIGHT: + case PROP_NAT_WIDTH: + case PROP_NAT_HEIGHT: + case PROP_MAX_WIDTH: + case PROP_MAX_HEIGHT: + val = g_value_get_int (value); + index = prop_id - 1; + if (self->values[index] != val) + { + self->values[index] = val; + g_object_notify_by_pspec (gobject, pspec); + + gtk_constraint_guide_update_constraint (self, index); + if (index == MIN_WIDTH || index == MAX_WIDTH) + gtk_constraint_guide_update_constraint (self, NAT_WIDTH); + if (index == MIN_HEIGHT || index == MAX_HEIGHT) + gtk_constraint_guide_update_constraint (self, NAT_HEIGHT); + } + break; + + case PROP_STRENGTH: + gtk_constraint_guide_set_strength (self, g_value_get_enum (value)); + break; + + case PROP_NAME: + gtk_constraint_guide_set_name (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtk_constraint_guide_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkConstraintGuide *self = GTK_CONSTRAINT_GUIDE (gobject); + + switch (prop_id) + { + case PROP_MIN_WIDTH: + case PROP_MIN_HEIGHT: + case PROP_NAT_WIDTH: + case PROP_NAT_HEIGHT: + case PROP_MAX_WIDTH: + case PROP_MAX_HEIGHT: + g_value_set_int (value, self->values[prop_id - 1]); + break; + + case PROP_STRENGTH: + g_value_set_enum (value, self->strength); + break; + + case PROP_NAME: + g_value_set_string (value, self->name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +gtk_constraint_guide_finalize (GObject *object) +{ + GtkConstraintGuide *self = GTK_CONSTRAINT_GUIDE (object); + + g_free (self->name); + + g_clear_pointer (&self->bound_attributes, g_hash_table_unref); + + G_OBJECT_CLASS (gtk_constraint_guide_parent_class)->finalize (object); +} + +static void +gtk_constraint_guide_class_init (GtkConstraintGuideClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = gtk_constraint_guide_finalize; + object_class->set_property = gtk_constraint_guide_set_property; + object_class->get_property = gtk_constraint_guide_get_property; + + /** + * GtkConstraintGuide:min-width: + * + * The minimum width of the guide. + */ + guide_props[PROP_MIN_WIDTH] = + g_param_spec_int ("min-width", + "Minimum width", + "Minimum width", + 0, G_MAXINT, 0, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:min-height: + * + * The minimum height of the guide. + */ + guide_props[PROP_MIN_HEIGHT] = + g_param_spec_int ("min-height", + "Minimum height", + "Minimum height", + 0, G_MAXINT, 0, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:nat-width: + * + * The preferred, or natural, width of the guide. + */ + guide_props[PROP_NAT_WIDTH] = + g_param_spec_int ("nat-width", + "Natural width", + "Natural width", + 0, G_MAXINT, 0, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:nat-height: + * + * The preferred, or natural, height of the guide. + */ + guide_props[PROP_NAT_HEIGHT] = + g_param_spec_int ("nat-height", + "Natural height", + "Natural height", + 0, G_MAXINT, 0, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:max-width: + * + * The maximum width of the guide. + */ + guide_props[PROP_MAX_WIDTH] = + g_param_spec_int ("max-width", + "Maximum width", + "Maximum width", + 0, G_MAXINT, G_MAXINT, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:max-height: + * + * The maximum height of the guide. + */ + guide_props[PROP_MAX_HEIGHT] = + g_param_spec_int ("max-height", + "Maximum height", + "Maximum height", + 0, G_MAXINT, G_MAXINT, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:strength: + * + * The #GtkConstraintStrength to be used for the constraint on + * the natural size of the guide. + */ + guide_props[PROP_STRENGTH] = + g_param_spec_enum ("strength", + "Strength", + "The strength to use for natural size", + GTK_TYPE_CONSTRAINT_STRENGTH, + GTK_CONSTRAINT_STRENGTH_MEDIUM, + G_PARAM_READWRITE| + G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkConstraintGuide:name: + * + * A name that identifies the #GtkConstraintGuide, for debugging. + */ + guide_props[PROP_NAME] = + g_param_spec_string ("name", + "Name", + "A name to use in debug message", + NULL, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, guide_props); +} + +/** + * gtk_constraint_guide_new: + * + * Creates a new #GtkConstraintGuide object. + * + * Return: a new #GtkConstraintGuide object. + */ +GtkConstraintGuide * +gtk_constraint_guide_new (void) +{ + return g_object_new (GTK_TYPE_CONSTRAINT_GUIDE, NULL); +} + +/** + * gtk_constraint_guide_set_min_size: + * @guide: a #GtkConstraintGuide object + * @width: the new minimum width, or -1 to not change it + * @height: the new minimum height, or -1 to not change it + * + * Sets the minimum size of @guide. + * + * If @guide is attached to a #GtkConstraintLayout, + * the constraints will be updated to reflect the new size. + */ +void +gtk_constraint_guide_set_min_size (GtkConstraintGuide *guide, + int width, + int height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + g_return_if_fail (width >= -1); + g_return_if_fail (height >= -1); + + g_object_freeze_notify (G_OBJECT (guide)); + + if (width != -1) + g_object_set (guide, "min-width", width, NULL); + + if (height != -1) + g_object_set (guide, "min-height", height, NULL); + + g_object_thaw_notify (G_OBJECT (guide)); +} + +/** + * gtk_constraint_guide_get_min_size: + * @guide: a #GtkContraintGuide object + * @width: (allow-none): return location for the minimum width, + * or %NULL + * @height: (allow-none): return location for the minimum height, + * or %NULL + * + * Gets the minimum size of @guide. + */ +void +gtk_constraint_guide_get_min_size (GtkConstraintGuide *guide, + int *width, + int *height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + + if (width) + *width = guide->values[MIN_WIDTH]; + if (height) + *height = guide->values[MIN_HEIGHT]; +} + +/** + * gtk_constraint_guide_set_nat_size: + * @guide: a #GtkConstraintGuide object + * @width: the new natural width, or -1 to not change it + * @height: the new natural height, or -1 to not change it + * + * Sets the natural size of @guide. + * + * If @guide is attached to a #GtkConstraintLayout, + * the constraints will be updated to reflect the new size. + */ +void +gtk_constraint_guide_set_nat_size (GtkConstraintGuide *guide, + int width, + int height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + g_return_if_fail (width >= -1); + g_return_if_fail (height >= -1); + + g_object_freeze_notify (G_OBJECT (guide)); + + if (width != -1) + g_object_set (guide, "nat-width", width, NULL); + + if (height != -1) + g_object_set (guide, "nat-height", height, NULL); + + g_object_thaw_notify (G_OBJECT (guide)); +} + +/** + * gtk_constraint_guide_get_nat_size: + * @guide: a #GtkContraintGuide object + * @width: (allow-none): return location for the natural width, + * or %NULL + * @height: (allow-none): return location for the natural height, + * or %NULL + * + * Gets the natural size of @guide. + */ +void +gtk_constraint_guide_get_nat_size (GtkConstraintGuide *guide, + int *width, + int *height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + + if (width) + *width = guide->values[NAT_WIDTH]; + if (height) + *height = guide->values[NAT_HEIGHT]; +} + +/** + * gtk_constraint_guide_set_max_size: + * @guide: a #GtkConstraintGuide object + * @width: the new maximum width, or -1 to not change it + * @height: the new maximum height, or -1 to not change it + * + * Sets the maximum size of @guide. + * + * If @guide is attached to a #GtkConstraintLayout, + * the constraints will be updated to reflect the new size. + */ +void +gtk_constraint_guide_set_max_size (GtkConstraintGuide *guide, + int width, + int height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + g_return_if_fail (width >= -1); + g_return_if_fail (height >= -1); + + g_object_freeze_notify (G_OBJECT (guide)); + + if (width != -1) + g_object_set (guide, "max-width", width, NULL); + + if (height != -1) + g_object_set (guide, "max-height", height, NULL); + + g_object_thaw_notify (G_OBJECT (guide)); +} + +/** + * gtk_constraint_guide_get_max_size: + * @guide: a #GtkContraintGuide object + * @width: (allow-none): return location for the maximum width, + * or %NULL + * @height: (allow-none): return location for the maximum height, + * or %NULL + * + * Gets the maximum size of @guide. + */ +void +gtk_constraint_guide_get_max_size (GtkConstraintGuide *guide, + int *width, + int *height) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + + if (width) + *width = guide->values[MAX_WIDTH]; + if (height) + *height = guide->values[MAX_HEIGHT]; +} + +/** + * gtk_constraint_guide_get_name: + * @guide: a #GtkConstraintGuide + * + * Retrieves the name set using gtk_constraint_guide_set_name(). + * + * Returns: (transfer none) (nullable): the name of the guide + */ +const char * +gtk_constraint_guide_get_name (GtkConstraintGuide *guide) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide), NULL); + + return guide->name; +} + +/** + * gtk_constraint_guide_set_name: + * @guide: a #GtkConstraintGuide + * @name: (nullable): a name for the @guide + * + * Sets a name for the given #GtkConstraintGuide. + * + * The name is useful for debugging purposes. + */ +void +gtk_constraint_guide_set_name (GtkConstraintGuide *guide, + const char *name) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + + g_free (guide->name); + guide->name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (guide), guide_props[PROP_NAME]); +} + +/** + * gtk_constraint_guide_get_strength: + * @guide: a #GtkConstraintGuide + * + * Retrieves the strength set using gtk_constraint_guide_set_strength(). + * + * Returns: the strength of the constraint on the natural size + */ +GtkConstraintStrength +gtk_constraint_guide_get_strength (GtkConstraintGuide *guide) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide), + GTK_CONSTRAINT_STRENGTH_MEDIUM); + + return guide->strength; +} + +/** + * gtk_constraint_guide_set_strength: + * @guide: a #GtkConstraintGuide + * @strength: the strength of the constraint + * + * Sets the strength of the constraint on the natural size of the + * given #GtkConstraintGuide. + */ +void +gtk_constraint_guide_set_strength (GtkConstraintGuide *guide, + GtkConstraintStrength strength) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + + if (guide->strength == strength) + return; + + guide->strength = strength; + g_object_notify_by_pspec (G_OBJECT (guide), guide_props[PROP_STRENGTH]); + gtk_constraint_guide_update_constraint (guide, NAT_WIDTH); + gtk_constraint_guide_update_constraint (guide, NAT_HEIGHT); +} diff --git a/gtk/gtkconstraintguide.h b/gtk/gtkconstraintguide.h new file mode 100644 index 0000000000..dbd7235009 --- /dev/null +++ b/gtk/gtkconstraintguide.h @@ -0,0 +1,83 @@ +/* gtkconstraintguide.h: Flexible space for constraints + * Copyright 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Matthias Clasen + */ + +#pragma once + +#include <gtk/gtktypes.h> +#include <gtk/gtkenums.h> +#include <gtk/gtktypebuiltins.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_CONSTRAINT_GUIDE (gtk_constraint_guide_get_type ()) + +/** + * GtkConstraintGuide: + * + * An object that can be added to a #GtkConstraintLayout and be + * used in constraints like a widget, without being drawn. + * + * Guides have a minimum, maximum and natural size. Depending + * on the constraints that are applied, they can act like a + * guideline that widgets can be aligned to, or like 'flexible space'. + */ +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkConstraintGuide, gtk_constraint_guide, GTK, CONSTRAINT_GUIDE, GObject) + +GDK_AVAILABLE_IN_ALL +GtkConstraintGuide * gtk_constraint_guide_new (void); + +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_set_min_size (GtkConstraintGuide *guide, + int width, + int height); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_get_min_size (GtkConstraintGuide *guide, + int *width, + int *height); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_set_nat_size (GtkConstraintGuide *guide, + int width, + int height); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_get_nat_size (GtkConstraintGuide *guide, + int *width, + int *height); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_set_max_size (GtkConstraintGuide *guide, + int width, + int height); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_get_max_size (GtkConstraintGuide *guide, + int *width, + int *height); + +GDK_AVAILABLE_IN_ALL +GtkConstraintStrength gtk_constraint_guide_get_strength (GtkConstraintGuide *guide); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_set_strength (GtkConstraintGuide *guide, + GtkConstraintStrength strength); + +GDK_AVAILABLE_IN_ALL +void gtk_constraint_guide_set_name (GtkConstraintGuide *guide, + const char *name); +GDK_AVAILABLE_IN_ALL +const char * gtk_constraint_guide_get_name (GtkConstraintGuide *guide); + +G_END_DECLS diff --git a/gtk/gtkconstraintguideprivate.h b/gtk/gtkconstraintguideprivate.h new file mode 100644 index 0000000000..69c7e5f2c4 --- /dev/null +++ b/gtk/gtkconstraintguideprivate.h @@ -0,0 +1,38 @@ +/* gtkconstraintguideprivate.h: Constraint between two widgets + * Copyright 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Matthias Clasen + */ + +#pragma once + +#include "gtkconstraintguide.h" +#include "gtkconstraintlayout.h" +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +void gtk_constraint_guide_update (GtkConstraintGuide *guide); +void gtk_constraint_guide_detach (GtkConstraintGuide *guide); + +GtkConstraintVariable *gtk_constraint_guide_get_attribute (GtkConstraintGuide *guide, + GtkConstraintAttribute attr); + +GtkConstraintLayout *gtk_constraint_guide_get_layout (GtkConstraintGuide *guide); +void gtk_constraint_guide_set_layout (GtkConstraintGuide *guide, + GtkConstraintLayout *layout); + +G_END_DECLS diff --git a/gtk/gtkconstraintlayout.c b/gtk/gtkconstraintlayout.c new file mode 100644 index 0000000000..4623798b5e --- /dev/null +++ b/gtk/gtkconstraintlayout.c @@ -0,0 +1,2118 @@ +/* gtkconstraintlayout.c: Layout manager using constraints + * Copyright 2019 GNOME Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +/** + * SECTION: gtkconstraintlayout + * @Title: GtkConstraintLayout + * @Short_description: A layout manager using constraints + * + * GtkConstraintLayout is a layout manager that uses relations between + * widget attributes, expressed via #GtkConstraint instances, to measure + * and allocate widgets. + * + * # How do constraints work + * + * Constraints are objects defining the relationship between attributes + * of a widget; you can read the description of the #GtkConstraint + * class to have a more in depth definition. + * + * By taking multiple constraints and applying them to the children of + * a widget using #GtkConstraintLayout, it's possible to describe complex + * layout policies; each constraint applied to a child or to the parent + * widgets contributes to the full description of the layout, in terms of + * parameters for resolving the value of each attribute. + * + * It is important to note that a layout is defined by the totality of + * constraints; removing a child, or a constraint, from an existing layout + * without changing the remaining constraints may result in an unstable + * or unsolvable layout. + * + * Constraints have an implicit "reading order"; you should start describing + * each edge of each child, as well as their relationship with the parent + * container, from the top left (or top right, in RTL languages), horizontally + * first, and then vertically. + * + * A constraint-based layout with too few constraints can become "unstable", + * that is: have more than one solution. The behavior of an unstable layout + * is undefined. + * + * A constraint-based layout with conflicting constraints may be unsolvable, + * and lead to an unstable layout. You can use the #GtkConstraint:strength + * property of #GtkConstraint to "nudge" the layout towards a solution. + * + * # GtkConstraintLayout as GtkBuildable + * + * GtkConstraintLayout implements the #GtkBuildable interface and has a + * custom "constraints" element which allows describing constraints in a + * GtkBuilder UI file. + * + * An example of a UI definition fragment specifying a constraint: + * + * |[ + * <object class="GtkConstraintLayout"> + * <constraints> + * <constraint target="button" target-attribute="start" + * relation="eq" + * source="super" source-attribute="start" + * constant="12" + * strength="required" /> + * <constraint target="button" target-attribute="width" + * relation="ge" + * constant="250" + * strength="strong" /> + * </constraints> + * </object> + * ]| + * + * The definition above will add two constraints to the GtkConstraintLayout: + * + * - a required constraint between the leading edge of "button" and + * the leading edge of the widget using the constraint layout, plus + * 12 pixels + * - a strong, constant constraint making the width of "button" greater + * than, or equal to 250 pixels + * + * The "target" and "target-attribute" attributes are required. + * + * The "source" and "source-attribute" attributes of the "constraint" + * element are optional; if they are not specified, the constraint is + * assumed to be a constant. + * + * The "relation" attribute is optional; if not specified, the constraint + * is assumed to be an equality. + * + * The "strength" attribute is optional; if not specified, the constraint + * is assumed to be required. + * + * The "source" and "target" attributes can be set to "super" to indicate + * that the constraint target is the widget using the GtkConstraintLayout. + * + * Additionally, the "constraints" element can also contain a description + * of the #GtkConstraintGuides used by the layout: + * + * |[ + * <constraints> + * <guide min-width="100" max-width="500" name="hspace"/> + * <guide min-height="64" nat-height="128" name="vspace" strength="strong"/> + * </constraints> + * ]| + * + * The "guide" element has the following optional attributes: + * + * - "min-width", "nat-width", and "max-width", describe the minimum, + * natural, and maximum width of the guide, respectively + * - "min-height", "nat-height", and "max-height", describe the minimum, + * natural, and maximum height of the guide, respectively + * - "strength" describes the strength of the constraint on the natural + * size of the guide; if not specified, the constraint is assumed to + * have a medium strength + * - "name" describes a name for the guide, useful when debugging + * + * # Using the Visual Format Language + * + * Complex constraints can be described using a compact syntax called VFL, + * or *Visual Format Language*. + * + * The Visual Format Language describes all the constraints on a row or + * column, typically starting from the leading edge towards the trailing + * one. Each element of the layout is composed by "views", which identify + * a #GtkConstraintTarget. + * + * For instance: + * + * |[ + * [button]-[textField] + * ]| + * + * Describes a constraint that binds the trailing edge of "button" to the + * leading edge of "textField", leaving a default space between the two. + * + * Using VFL is also possible to specify predicates that describe constraints + * on attributes like width and height: + * + * |[ + * // Width must be greater than, or equal to 50 + * [button(>=50)] + * + * // Width of button1 must be equal to width of button2 + * [button1(==button2)] + * ]| + * + * The default orientation for a VFL description is horizontal, unless + * otherwise specified: + * + * |[ + * // horizontal orientation, default attribute: width + * H:[button(>=150)] + * + * // vertical orientation, default attribute: height + * V:[button1(==button2)] + * ]| + * + * It's also possible to specify multiple predicates, as well as their + * strength: + * + * |[ + * // minimum width of button must be 150 + * // natural width of button can be 250 + * [button(>=150@required, ==250@medium)] + * ]| + * + * Finally, it's also possible to use simple arithmetic operators: + * + * |[ + * // width of button1 must be equal to width of button2 + * // divided by 2 plus 12 + * [button1(button2 / 2 + 12)] + * ]| + */ + +#include "config.h" + +#include "gtkconstraintlayout.h" +#include "gtkconstraintlayoutprivate.h" + +#include "gtkconstraintprivate.h" +#include "gtkconstraintexpressionprivate.h" +#include "gtkconstraintguideprivate.h" +#include "gtkconstraintsolverprivate.h" +#include "gtkconstraintvflparserprivate.h" + +#include "gtkbuildable.h" +#include "gtkbuilderprivate.h" +#include "gtkdebug.h" +#include "gtklayoutchild.h" +#include "gtkintl.h" +#include "gtkprivate.h" +#include "gtksizerequest.h" +#include "gtkwidgetprivate.h" + +#include <string.h> +#include <errno.h> + +enum { + MIN_WIDTH, + MIN_HEIGHT, + NAT_WIDTH, + NAT_HEIGHT, + LAST_VALUE +}; + +struct _GtkConstraintLayoutChild +{ + GtkLayoutChild parent_instance; + + int values[LAST_VALUE]; + GtkConstraintRef *constraints[LAST_VALUE]; + + /* HashTable<static string, Variable>; a hash table of variables, + * one for each attribute; we use these to query and suggest the + * values for the solver. The string is static and does not need + * to be freed. + */ + GHashTable *bound_attributes; +}; + +struct _GtkConstraintLayout +{ + GtkLayoutManager parent_instance; + + /* A pointer to the GtkConstraintSolver used by the layout manager; + * we acquire one when the layout manager gets rooted, and release + * it when it gets unrooted. + */ + GtkConstraintSolver *solver; + + /* HashTable<static string, Variable>; a hash table of variables, + * one for each attribute; we use these to query and suggest the + * values for the solver. The string is static and does not need + * to be freed. + */ + GHashTable *bound_attributes; + + /* HashSet<GtkConstraint>; the set of constraints on the + * parent widget, using the public API objects. + */ + GHashTable *constraints; + + /* HashSet<GtkConstraintGuide> */ + GHashTable *guides; +}; + +G_DEFINE_TYPE (GtkConstraintLayoutChild, gtk_constraint_layout_child, GTK_TYPE_LAYOUT_CHILD) + +GtkConstraintSolver * +gtk_constraint_layout_get_solver (GtkConstraintLayout *self) +{ + GtkWidget *widget; + GtkRoot *root; + + if (self->solver != NULL) + return self->solver; + + widget = gtk_layout_manager_get_widget (GTK_LAYOUT_MANAGER (self)); + if (widget == NULL) + return NULL; + + root = gtk_widget_get_root (widget); + if (root == NULL) + return NULL; + + self->solver = gtk_root_get_constraint_solver (root); + + return self->solver; +} + +static const char * const attribute_names[] = { + [GTK_CONSTRAINT_ATTRIBUTE_NONE] = "none", + [GTK_CONSTRAINT_ATTRIBUTE_LEFT] = "left", + [GTK_CONSTRAINT_ATTRIBUTE_RIGHT] = "right", + [GTK_CONSTRAINT_ATTRIBUTE_TOP] = "top", + [GTK_CONSTRAINT_ATTRIBUTE_BOTTOM] = "bottom", + [GTK_CONSTRAINT_ATTRIBUTE_START] = "start", + [GTK_CONSTRAINT_ATTRIBUTE_END] = "end", + [GTK_CONSTRAINT_ATTRIBUTE_WIDTH] = "width", + [GTK_CONSTRAINT_ATTRIBUTE_HEIGHT] = "height", + [GTK_CONSTRAINT_ATTRIBUTE_CENTER_X] = "center-x", + [GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y] = "center-y", + [GTK_CONSTRAINT_ATTRIBUTE_BASELINE] = "baseline", +}; + +G_GNUC_PURE +static const char * +get_attribute_name (GtkConstraintAttribute attr) +{ + return attribute_names[attr]; +} + +static GtkConstraintAttribute +resolve_direction (GtkConstraintAttribute attr, + GtkWidget *widget) +{ + GtkTextDirection text_dir; + + /* Resolve the start/end attributes depending on the layout's text direction */ + + if (widget) + text_dir = gtk_widget_get_direction (widget); + else + text_dir = GTK_TEXT_DIR_LTR; + + if (attr == GTK_CONSTRAINT_ATTRIBUTE_START) + { + if (text_dir == GTK_TEXT_DIR_RTL) + attr = GTK_CONSTRAINT_ATTRIBUTE_RIGHT; + else + attr = GTK_CONSTRAINT_ATTRIBUTE_LEFT; + } + else if (attr == GTK_CONSTRAINT_ATTRIBUTE_END) + { + if (text_dir == GTK_TEXT_DIR_RTL) + attr = GTK_CONSTRAINT_ATTRIBUTE_LEFT; + else + attr = GTK_CONSTRAINT_ATTRIBUTE_RIGHT; + } + + return attr; +} + +GtkConstraintVariable * +gtk_constraint_layout_get_attribute (GtkConstraintLayout *layout, + GtkConstraintAttribute attr, + const char *prefix, + GtkWidget *widget, + GHashTable *bound_attributes) +{ + const char *attr_name; + GtkConstraintVariable *res; + GtkConstraintSolver *solver = layout->solver; + + attr = resolve_direction (attr, widget); + + attr_name = get_attribute_name (attr); + res = g_hash_table_lookup (bound_attributes, attr_name); + if (res != NULL) + return res; + + res = gtk_constraint_solver_create_variable (solver, prefix, attr_name, 0.0); + g_hash_table_insert (bound_attributes, (gpointer) attr_name, res); + + /* Some attributes are really constraints computed from other + * attributes, to avoid creating additional constraints from + * the user's perspective + */ + switch (attr) + { + /* right = left + width */ + case GTK_CONSTRAINT_ATTRIBUTE_RIGHT: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *left, *width; + GtkConstraintExpression *expr; + + left = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_LEFT, prefix, widget, bound_attributes); + width = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_WIDTH, prefix, widget, bound_attributes); + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, left); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, width); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* bottom = top + height */ + case GTK_CONSTRAINT_ATTRIBUTE_BOTTOM: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *top, *height; + GtkConstraintExpression *expr; + + top = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_TOP, prefix, widget, bound_attributes); + height = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, prefix, widget, bound_attributes); + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, top); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, height); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* centerX = (width / 2.0) + left*/ + case GTK_CONSTRAINT_ATTRIBUTE_CENTER_X: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *left, *width; + GtkConstraintExpression *expr; + + left = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_LEFT, prefix, widget, bound_attributes); + width = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_WIDTH, prefix, widget, bound_attributes); + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, width); + gtk_constraint_expression_builder_divide_by (&builder); + gtk_constraint_expression_builder_constant (&builder, 2.0); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, left); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* centerY = (height / 2.0) + top */ + case GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *top, *height; + GtkConstraintExpression *expr; + + top = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_TOP, prefix, widget, bound_attributes); + height = gtk_constraint_layout_get_attribute (layout, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, prefix, widget, bound_attributes); + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, height); + gtk_constraint_expression_builder_divide_by (&builder); + gtk_constraint_expression_builder_constant (&builder, 2.0); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, top); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* We do not allow negative sizes */ + case GTK_CONSTRAINT_ATTRIBUTE_WIDTH: + case GTK_CONSTRAINT_ATTRIBUTE_HEIGHT: + { + GtkConstraintExpression *expr; + + expr = gtk_constraint_expression_new (0.0); + gtk_constraint_solver_add_constraint (solver, + res, GTK_CONSTRAINT_RELATION_GE, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* These are "pure" attributes */ + case GTK_CONSTRAINT_ATTRIBUTE_NONE: + case GTK_CONSTRAINT_ATTRIBUTE_LEFT: + case GTK_CONSTRAINT_ATTRIBUTE_TOP: + case GTK_CONSTRAINT_ATTRIBUTE_BASELINE: + break; + + /* These attributes must have been resolved to their real names */ + case GTK_CONSTRAINT_ATTRIBUTE_START: + case GTK_CONSTRAINT_ATTRIBUTE_END: + g_assert_not_reached (); + break; + + default: + break; + } + + return res; +} + +static GtkConstraintVariable * +get_child_attribute (GtkConstraintLayout *layout, + GtkWidget *widget, + GtkConstraintAttribute attr) +{ + GtkConstraintLayoutChild *child_info; + const char *prefix = gtk_widget_get_name (widget); + + child_info = GTK_CONSTRAINT_LAYOUT_CHILD (gtk_layout_manager_get_layout_child (GTK_LAYOUT_MANAGER (layout), widget)); + + return gtk_constraint_layout_get_attribute (layout, attr, prefix, widget, child_info->bound_attributes); +} + +static void +gtk_constraint_layout_child_finalize (GObject *gobject) +{ + GtkConstraintLayoutChild *self = GTK_CONSTRAINT_LAYOUT_CHILD (gobject); + + g_clear_pointer (&self->bound_attributes, g_hash_table_unref); + + G_OBJECT_CLASS (gtk_constraint_layout_child_parent_class)->finalize (gobject); +} + +static void +gtk_constraint_layout_child_class_init (GtkConstraintLayoutChildClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gtk_constraint_layout_child_finalize; +} + +static void +gtk_constraint_layout_child_init (GtkConstraintLayoutChild *self) +{ + self->bound_attributes = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify) gtk_constraint_variable_unref); +} + +static void gtk_buildable_interface_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtkConstraintLayout, gtk_constraint_layout, GTK_TYPE_LAYOUT_MANAGER, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_buildable_interface_init)) + +static void +gtk_constraint_layout_finalize (GObject *gobject) +{ + GtkConstraintLayout *self = GTK_CONSTRAINT_LAYOUT (gobject); + + g_clear_pointer (&self->bound_attributes, g_hash_table_unref); + g_clear_pointer (&self->constraints, g_hash_table_unref); + g_clear_pointer (&self->guides, g_hash_table_unref); + + G_OBJECT_CLASS (gtk_constraint_layout_parent_class)->finalize (gobject); +} + +static GtkConstraintVariable * +get_layout_attribute (GtkConstraintLayout *self, + GtkWidget *widget, + GtkConstraintAttribute attr) +{ + GtkTextDirection text_dir; + const char *attr_name; + GtkConstraintVariable *res; + + /* Resolve the start/end attributes depending on the layout's text direction */ + if (attr == GTK_CONSTRAINT_ATTRIBUTE_START) + { + text_dir = gtk_widget_get_direction (widget); + if (text_dir == GTK_TEXT_DIR_RTL) + attr = GTK_CONSTRAINT_ATTRIBUTE_RIGHT; + else + attr = GTK_CONSTRAINT_ATTRIBUTE_LEFT; + } + else if (attr == GTK_CONSTRAINT_ATTRIBUTE_END) + { + text_dir = gtk_widget_get_direction (widget); + if (text_dir == GTK_TEXT_DIR_RTL) + attr = GTK_CONSTRAINT_ATTRIBUTE_LEFT; + else + attr = GTK_CONSTRAINT_ATTRIBUTE_RIGHT; + } + + attr_name = get_attribute_name (attr); + res = g_hash_table_lookup (self->bound_attributes, attr_name); + if (res != NULL) + return res; + + res = gtk_constraint_solver_create_variable (self->solver, "super", attr_name, 0.0); + g_hash_table_insert (self->bound_attributes, (gpointer) attr_name, res); + + /* Some attributes are really constraints computed from other + * attributes, to avoid creating additional constraints from + * the user's perspective + */ + switch (attr) + { + /* right = left + width */ + case GTK_CONSTRAINT_ATTRIBUTE_RIGHT: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *left, *width; + GtkConstraintExpression *expr; + + left = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_LEFT); + width = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + + gtk_constraint_expression_builder_init (&builder, self->solver); + gtk_constraint_expression_builder_term (&builder, left); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, width); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (self->solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* bottom = top + height */ + case GTK_CONSTRAINT_ATTRIBUTE_BOTTOM: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *top, *height; + GtkConstraintExpression *expr; + + top = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_TOP); + height = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + + gtk_constraint_expression_builder_init (&builder, self->solver); + gtk_constraint_expression_builder_term (&builder, top); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, height); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (self->solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* centerX = left + (width / 2.0) */ + case GTK_CONSTRAINT_ATTRIBUTE_CENTER_X: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *left, *width; + GtkConstraintExpression *expr; + + left = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_LEFT); + width = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + + gtk_constraint_expression_builder_init (&builder, self->solver); + gtk_constraint_expression_builder_term (&builder, width); + gtk_constraint_expression_builder_divide_by (&builder); + gtk_constraint_expression_builder_constant (&builder, 2.0); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, left); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (self->solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* centerY = top + (height / 2.0) */ + case GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y: + { + GtkConstraintExpressionBuilder builder; + GtkConstraintVariable *top, *height; + GtkConstraintExpression *expr; + + top = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_TOP); + height = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + + gtk_constraint_expression_builder_init (&builder, self->solver); + gtk_constraint_expression_builder_term (&builder, height); + gtk_constraint_expression_builder_divide_by (&builder); + gtk_constraint_expression_builder_constant (&builder, 2.0); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, top); + expr = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_constraint (self->solver, + res, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* We do not allow negative sizes */ + case GTK_CONSTRAINT_ATTRIBUTE_WIDTH: + case GTK_CONSTRAINT_ATTRIBUTE_HEIGHT: + { + GtkConstraintExpression *expr; + + expr = gtk_constraint_expression_new (0.0); + gtk_constraint_solver_add_constraint (self->solver, + res, GTK_CONSTRAINT_RELATION_GE, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + break; + + /* These are "pure" attributes */ + case GTK_CONSTRAINT_ATTRIBUTE_NONE: + case GTK_CONSTRAINT_ATTRIBUTE_LEFT: + case GTK_CONSTRAINT_ATTRIBUTE_TOP: + case GTK_CONSTRAINT_ATTRIBUTE_BASELINE: + break; + + /* These attributes must have been resolved to their real names */ + case GTK_CONSTRAINT_ATTRIBUTE_START: + case GTK_CONSTRAINT_ATTRIBUTE_END: + g_assert_not_reached (); + break; + + default: + break; + } + + return res; +} + +/*< private > + * layout_add_constraint: + * @self: a #GtkConstraintLayout + * @constraint: a #GtkConstraint + * + * Turns a #GtkConstraint into a #GtkConstraintRef inside the + * constraint solver associated to @self. + * + * If @self does not have a #GtkConstraintSolver, because it + * has not been rooted yet, we just store the @constraint instance, + * and we're going to call this function when the layout manager + * gets rooted. + */ +static void +layout_add_constraint (GtkConstraintLayout *self, + GtkConstraint *constraint) +{ + GtkConstraintVariable *target_attr, *source_attr; + GtkConstraintExpressionBuilder builder; + GtkConstraintExpression *expr; + GtkConstraintSolver *solver; + GtkConstraintAttribute attr; + GtkConstraintTarget *target, *source; + GtkWidget *layout_widget; + + if (gtk_constraint_is_attached (constraint)) + return; + + /* Once we pass the preconditions, we check if we can turn a GtkConstraint + * into a GtkConstraintRef; if we can't, we keep a reference to the + * constraint object and try later on + */ + layout_widget = gtk_layout_manager_get_widget (GTK_LAYOUT_MANAGER (self)); + if (layout_widget == NULL) + return; + + solver = gtk_constraint_layout_get_solver (self); + if (solver == NULL) + return; + + attr = gtk_constraint_get_target_attribute (constraint); + target = gtk_constraint_get_target (constraint); + if (target == NULL || target == GTK_CONSTRAINT_TARGET (layout_widget)) + { + /* A NULL target widget is assumed to be referring to the layout itself */ + target_attr = get_layout_attribute (self, layout_widget, attr); + } + else if (GTK_IS_WIDGET (target) && + gtk_widget_get_parent (GTK_WIDGET (target)) == layout_widget) + { + target_attr = get_child_attribute (self, GTK_WIDGET (target), attr); + } + else if (GTK_IS_CONSTRAINT_GUIDE (target)) + { + GtkConstraintGuide *guide; + + guide = (GtkConstraintGuide*)g_hash_table_lookup (self->guides, target); + target_attr = gtk_constraint_guide_get_attribute (guide, attr); + } + else + { + g_critical ("Unknown target widget '%p'", target); + target_attr = NULL; + } + + if (target_attr == NULL) + return; + + attr = gtk_constraint_get_source_attribute (constraint); + source = gtk_constraint_get_source (constraint); + + /* The constraint is a constant */ + if (attr == GTK_CONSTRAINT_ATTRIBUTE_NONE) + { + source_attr = NULL; + } + else + { + if (source == NULL || source == GTK_CONSTRAINT_TARGET (layout_widget)) + { + source_attr = get_layout_attribute (self, layout_widget, attr); + } + else if (GTK_IS_WIDGET (source) && + gtk_widget_get_parent (GTK_WIDGET (source)) == layout_widget) + { + source_attr = get_child_attribute (self, GTK_WIDGET (source), attr); + } + else if (GTK_IS_CONSTRAINT_GUIDE (source)) + { + GtkConstraintGuide *guide; + + guide = (GtkConstraintGuide*)g_hash_table_lookup (self->guides, source); + source_attr = gtk_constraint_guide_get_attribute (guide, attr); + } + else + { + g_critical ("Unknown source widget '%p'", source); + source_attr = NULL; + return; + } + } + + /* Build the expression */ + gtk_constraint_expression_builder_init (&builder, self->solver); + + if (source_attr != NULL) + { + gtk_constraint_expression_builder_term (&builder, source_attr); + gtk_constraint_expression_builder_multiply_by (&builder); + gtk_constraint_expression_builder_constant (&builder, gtk_constraint_get_multiplier (constraint)); + gtk_constraint_expression_builder_plus (&builder); + } + + gtk_constraint_expression_builder_constant (&builder, gtk_constraint_get_constant (constraint)); + expr = gtk_constraint_expression_builder_finish (&builder); + + constraint->solver = solver; + constraint->constraint_ref = + gtk_constraint_solver_add_constraint (self->solver, + target_attr, + gtk_constraint_get_relation (constraint), + expr, + gtk_constraint_get_strength (constraint)); +} + +static void +update_child_constraint (GtkConstraintLayout *self, + GtkConstraintLayoutChild *child_info, + GtkWidget *child, + int index, + int value) +{ + + GtkConstraintVariable *var; + int attr[LAST_VALUE] = { + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT + }; + int relation[LAST_VALUE] = { + GTK_CONSTRAINT_RELATION_GE, + GTK_CONSTRAINT_RELATION_GE, + GTK_CONSTRAINT_RELATION_EQ, + GTK_CONSTRAINT_RELATION_EQ + }; + + if (child_info->values[index] != value) + { + child_info->values[index] = value; + + if (child_info->constraints[index]) + gtk_constraint_solver_remove_constraint (self->solver, + child_info->constraints[index]); + + var = get_child_attribute (self, child, attr[index]); + + if (relation[index] == GTK_CONSTRAINT_RELATION_EQ) + { + gtk_constraint_variable_set_value (var, value); + child_info->constraints[index] = + gtk_constraint_solver_add_stay_variable (self->solver, + var, + GTK_CONSTRAINT_STRENGTH_MEDIUM); + } + else + { + child_info->constraints[index] = + gtk_constraint_solver_add_constraint (self->solver, + var, + relation[index], + gtk_constraint_expression_new (value), + GTK_CONSTRAINT_STRENGTH_REQUIRED); + } + } +} + +static void +gtk_constraint_layout_measure (GtkLayoutManager *manager, + GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkConstraintLayout *self = GTK_CONSTRAINT_LAYOUT (manager); + GtkConstraintVariable *size, *opposite_size; + GtkConstraintSolver *solver; + GtkWidget *child; + int min_value; + int nat_value; + + solver = gtk_constraint_layout_get_solver (self); + if (solver == NULL) + return; + + gtk_constraint_solver_freeze (solver); + + /* We measure each child in the layout and impose restrictions on the + * minimum and natural size, so we can solve the size of the overall + * layout later on + */ + for (child = _gtk_widget_get_first_child (widget); + child != NULL; + child = _gtk_widget_get_next_sibling (child)) + { + GtkConstraintLayoutChild *info; + GtkRequisition min_req, nat_req; + + if (!gtk_widget_should_layout (child)) + continue; + + gtk_widget_get_preferred_size (child, &min_req, &nat_req); + + info = GTK_CONSTRAINT_LAYOUT_CHILD (gtk_layout_manager_get_layout_child (manager, child)); + + update_child_constraint (self, info, child, MIN_WIDTH, min_req.width); + update_child_constraint (self, info, child, MIN_HEIGHT, min_req.height); + update_child_constraint (self, info, child, NAT_WIDTH, nat_req.width); + update_child_constraint (self, info, child, NAT_HEIGHT, nat_req.height); + } + + gtk_constraint_solver_thaw (solver); + + switch (orientation) + { + case GTK_ORIENTATION_HORIZONTAL: + size = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + opposite_size = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + break; + + case GTK_ORIENTATION_VERTICAL: + size = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + opposite_size = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + break; + + default: + g_assert_not_reached (); + } + + g_assert (size != NULL && opposite_size != NULL); + + nat_value = gtk_constraint_variable_get_value (size); + + /* We impose a temporary value on the size and opposite size of the + * layout, with a low weight to let the solver settle towards the + * natural state of the system. Once we get the value out, we can + * remove these constraints + */ + gtk_constraint_solver_add_edit_variable (solver, size, GTK_CONSTRAINT_STRENGTH_STRONG * 2); + if (for_size > 0) + gtk_constraint_solver_add_edit_variable (solver, opposite_size, GTK_CONSTRAINT_STRENGTH_STRONG * 2); + gtk_constraint_solver_begin_edit (solver); + gtk_constraint_solver_suggest_value (solver, size, 0.0); + if (for_size > 0) + gtk_constraint_solver_suggest_value (solver, opposite_size, for_size); + gtk_constraint_solver_resolve (solver); + + min_value = gtk_constraint_variable_get_value (size); + + gtk_constraint_solver_remove_edit_variable (solver, size); + if (for_size > 0) + gtk_constraint_solver_remove_edit_variable (solver, opposite_size); + gtk_constraint_solver_end_edit (solver); + + GTK_NOTE (LAYOUT, + g_print ("layout %p %s size: min %d nat %d (for opposite size: %d)\n", + self, + orientation == GTK_ORIENTATION_HORIZONTAL ? "horizontal" : "vertical", + min_value, nat_value, + for_size)); + + if (minimum != NULL) + *minimum = min_value; + + if (natural != NULL) + *natural = nat_value; +} + +static void +gtk_constraint_layout_allocate (GtkLayoutManager *manager, + GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkConstraintLayout *self = GTK_CONSTRAINT_LAYOUT (manager); + GtkConstraintRef *stay_w, *stay_h, *stay_t, *stay_l; + GtkConstraintSolver *solver; + GtkConstraintVariable *layout_top, *layout_height; + GtkConstraintVariable *layout_left, *layout_width; + GtkWidget *child; + + solver = gtk_constraint_layout_get_solver (self); + if (solver == NULL) + return; + + /* We add required stay constraints to ensure that the layout remains + * within the bounds of the allocation + */ + layout_top = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_TOP); + layout_left = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_LEFT); + layout_width = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + layout_height = get_layout_attribute (self, widget, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + + gtk_constraint_variable_set_value (layout_top, 0.0); + stay_t = gtk_constraint_solver_add_stay_variable (solver, + layout_top, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_variable_set_value (layout_left, 0.0); + stay_l = gtk_constraint_solver_add_stay_variable (solver, + layout_left, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_variable_set_value (layout_width, width); + stay_w = gtk_constraint_solver_add_stay_variable (solver, + layout_width, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_variable_set_value (layout_height, height); + stay_h = gtk_constraint_solver_add_stay_variable (solver, + layout_height, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + GTK_NOTE (LAYOUT, + g_print ("Layout [%p]: { .x: %g, .y: %g, .w: %g, .h: %g }\n", + self, + gtk_constraint_variable_get_value (layout_left), + gtk_constraint_variable_get_value (layout_top), + gtk_constraint_variable_get_value (layout_width), + gtk_constraint_variable_get_value (layout_height))); + + for (child = _gtk_widget_get_first_child (widget); + child != NULL; + child = _gtk_widget_get_next_sibling (child)) + { + GtkConstraintVariable *var_top, *var_left, *var_width, *var_height; + GtkConstraintVariable *var_baseline; + GtkAllocation child_alloc; + int child_baseline = -1; + + if (!gtk_widget_should_layout (child)) + continue; + + /* Retrieve all the values associated with the child */ + var_top = get_child_attribute (self, child, GTK_CONSTRAINT_ATTRIBUTE_TOP); + var_left = get_child_attribute (self, child, GTK_CONSTRAINT_ATTRIBUTE_LEFT); + var_width = get_child_attribute (self, child, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + var_height = get_child_attribute (self, child, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + var_baseline = get_child_attribute (self, child, GTK_CONSTRAINT_ATTRIBUTE_BASELINE); + + GTK_NOTE (LAYOUT, + g_print ("Allocating child '%s'[%p] with { .x: %g, .y: %g, .w: %g, .h: %g, .b: %g }\n", + gtk_widget_get_name (child), child, + gtk_constraint_variable_get_value (var_left), + gtk_constraint_variable_get_value (var_top), + gtk_constraint_variable_get_value (var_width), + gtk_constraint_variable_get_value (var_height), + gtk_constraint_variable_get_value (var_baseline))); + + child_alloc.x = floor (gtk_constraint_variable_get_value (var_left)); + child_alloc.y = floor (gtk_constraint_variable_get_value (var_top)); + child_alloc.width = ceil (gtk_constraint_variable_get_value (var_width)); + child_alloc.height = ceil (gtk_constraint_variable_get_value (var_height)); + + if (gtk_constraint_variable_get_value (var_baseline) > 0) + child_baseline = floor (gtk_constraint_variable_get_value (var_baseline)); + + gtk_widget_size_allocate (GTK_WIDGET (child), + &child_alloc, + child_baseline); + } + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (LAYOUT)) + { + GHashTableIter iter; + gpointer key; + g_hash_table_iter_init (&iter, self->guides); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraintGuide *guide = key; + GtkConstraintVariable *var_top, *var_left, *var_width, *var_height; + var_top = gtk_constraint_guide_get_attribute (guide, GTK_CONSTRAINT_ATTRIBUTE_TOP); + var_left = gtk_constraint_guide_get_attribute (guide, GTK_CONSTRAINT_ATTRIBUTE_LEFT); + var_width = gtk_constraint_guide_get_attribute (guide, GTK_CONSTRAINT_ATTRIBUTE_WIDTH); + var_height = gtk_constraint_guide_get_attribute (guide, GTK_CONSTRAINT_ATTRIBUTE_HEIGHT); + g_print ("Allocating guide '%s'[%p] with { .x: %g .y: %g .w: %g .h: %g }\n", + gtk_constraint_guide_get_name (guide), guide, + gtk_constraint_variable_get_value (var_left), + gtk_constraint_variable_get_value (var_top), + gtk_constraint_variable_get_value (var_width), + gtk_constraint_variable_get_value (var_height)); + } + } +#endif + + /* The allocation stay constraints are not needed any more */ + gtk_constraint_solver_remove_constraint (solver, stay_w); + gtk_constraint_solver_remove_constraint (solver, stay_h); + gtk_constraint_solver_remove_constraint (solver, stay_t); + gtk_constraint_solver_remove_constraint (solver, stay_l); +} + +static void +gtk_constraint_layout_root (GtkLayoutManager *manager) +{ + GtkConstraintLayout *self = GTK_CONSTRAINT_LAYOUT (manager); + GHashTableIter iter; + GtkWidget *widget; + GtkRoot *root; + gpointer key; + + widget = gtk_layout_manager_get_widget (manager); + root = gtk_widget_get_root (widget); + + self->solver = gtk_root_get_constraint_solver (root); + + /* Now that we have a solver, attach all constraints we have */ + g_hash_table_iter_init (&iter, self->constraints); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraint *constraint = key; + layout_add_constraint (self, constraint); + } + + g_hash_table_iter_init (&iter, self->guides); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraintGuide *guide = key; + gtk_constraint_guide_update (guide); + } +} + +static void +gtk_constraint_layout_unroot (GtkLayoutManager *manager) +{ + GtkConstraintLayout *self = GTK_CONSTRAINT_LAYOUT (manager); + GHashTableIter iter; + gpointer key; + + /* Detach all constraints we're holding, as we're removing the layout + * from the global solver, and they should not contribute to the other + * layouts + */ + g_hash_table_iter_init (&iter, self->constraints); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraint *constraint = key; + gtk_constraint_detach (constraint); + } + + g_hash_table_iter_init (&iter, self->guides); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraintGuide *guide = key; + gtk_constraint_guide_detach (guide); + } + + self->solver = NULL; +} + +static void +gtk_constraint_layout_class_init (GtkConstraintLayoutClass *klass) +{ + GtkLayoutManagerClass *manager_class = GTK_LAYOUT_MANAGER_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gtk_constraint_layout_finalize; + + manager_class->layout_child_type = GTK_TYPE_CONSTRAINT_LAYOUT_CHILD; + manager_class->measure = gtk_constraint_layout_measure; + manager_class->allocate = gtk_constraint_layout_allocate; + manager_class->root = gtk_constraint_layout_root; + manager_class->unroot = gtk_constraint_layout_unroot; +} + +static void +gtk_constraint_layout_init (GtkConstraintLayout *self) +{ + /* The bound variables in the solver */ + self->bound_attributes = + g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify) gtk_constraint_variable_unref); + + /* The GtkConstraint instances we own */ + self->constraints = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) g_object_unref, + NULL); + + self->guides = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) g_object_unref, + NULL); +} + +typedef struct { + GtkConstraintLayout *layout; + GtkBuilder *builder; + GList *constraints; + GList *guides; +} ConstraintsParserData; + +typedef struct { + char *source_name; + char *source_attr; + char *target_name; + char *target_attr; + char *relation; + char *strength; + double constant; + double multiplier; +} ConstraintData; + +typedef struct { + char *name; + char *strength; + struct { + int min, nat, max; + } sizes[2]; +} GuideData; + +static void +constraint_data_free (gpointer _data) +{ + ConstraintData *data = _data; + + g_free (data->source_name); + g_free (data->source_attr); + g_free (data->target_name); + g_free (data->target_attr); + g_free (data->relation); + g_free (data->strength); + + g_free (data); +} + +static void +guide_data_free (gpointer _data) +{ + GuideData *data = _data; + + g_free (data->name); + g_free (data->strength); + + g_free (data); +} + +static void +parse_double (const char *string, + double *value_p, + double default_value) +{ + double value; + char *endptr; + int saved_errno; + + if (string == NULL || string[0] == '\0') + { + *value_p = default_value; + return; + } + + saved_errno = errno; + errno = 0; + value = g_ascii_strtod (string, &endptr); + if (errno == 0 && endptr != string) + *value_p = value; + else + *value_p = default_value; + + errno = saved_errno; +} + +static void +parse_int (const char *string, + int *value_p, + int default_value) +{ + gint64 value; + char *endptr; + int saved_errno; + + if (string == NULL || string[0] == '\0') + { + *value_p = default_value; + return; + } + + saved_errno = errno; + errno = 0; + value = g_ascii_strtoll (string, &endptr, 10); + if (errno == 0 && endptr != string) + *value_p = (int) value; + else + *value_p = default_value; + + errno = saved_errno; +} + +static GtkConstraint * +constraint_data_to_constraint (const ConstraintData *data, + GtkBuilder *builder, + GError **error) +{ + gpointer source, target; + int source_attr, target_attr; + int relation, strength; + gboolean res; + + if (g_strcmp0 (data->source_name, "super") == 0) + source = NULL; + else if (data->source_name == NULL) + { + if (data->source_attr != NULL) + { + g_set_error (error, GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_VALUE, + "Constraints without 'source' must also not " + "have a 'source-attribute' attribute"); + return NULL; + } + + source = NULL; + } + else + source = gtk_builder_get_object (builder, data->source_name); + + if (g_strcmp0 (data->target_name, "super") == 0) + target = NULL; + else + { + target = gtk_builder_get_object (builder, data->target_name); + + if (target == NULL) + { + g_set_error (error, GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_VALUE, + "Unable to find target '%s' for constraint", + data->target_name); + return NULL; + } + } + + if (data->source_attr != NULL) + { + res = _gtk_builder_enum_from_string (GTK_TYPE_CONSTRAINT_ATTRIBUTE, + data->source_attr, + &source_attr, + error); + if (!res) + return NULL; + } + else + source_attr = GTK_CONSTRAINT_ATTRIBUTE_NONE; + + res = _gtk_builder_enum_from_string (GTK_TYPE_CONSTRAINT_ATTRIBUTE, + data->target_attr, + &target_attr, + error); + if (!res) + return NULL; + + if (data->relation != NULL) + { + res = _gtk_builder_enum_from_string (GTK_TYPE_CONSTRAINT_RELATION, + data->relation, + &relation, + error); + if (!res) + return NULL; + } + else + relation = GTK_CONSTRAINT_RELATION_EQ; + + if (data->strength != NULL) + { + res = _gtk_builder_enum_from_string (GTK_TYPE_CONSTRAINT_STRENGTH, + data->strength, + &strength, + error); + if (!res) + return NULL; + } + else + strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + + if (source != NULL && source_attr != GTK_CONSTRAINT_ATTRIBUTE_NONE) + return gtk_constraint_new (target, target_attr, + relation, + source, source_attr, + data->multiplier, + data->constant, + strength); + + return gtk_constraint_new_constant (target, target_attr, + relation, + data->constant, + strength); +} + +static GtkConstraintGuide * +guide_data_to_guide (const GuideData *data, + GtkBuilder *builder, + GError **error) +{ + int strength; + gboolean res; + + if (data->strength != NULL) + { + res = _gtk_builder_enum_from_string (GTK_TYPE_CONSTRAINT_STRENGTH, + data->strength, + &strength, + error); + if (!res) + return NULL; + } + else + strength = GTK_CONSTRAINT_STRENGTH_MEDIUM; + + return g_object_new (GTK_TYPE_CONSTRAINT_GUIDE, + "min-width", data->sizes[GTK_ORIENTATION_HORIZONTAL].min, + "nat-width", data->sizes[GTK_ORIENTATION_HORIZONTAL].nat, + "max-width", data->sizes[GTK_ORIENTATION_HORIZONTAL].max, + "min-height", data->sizes[GTK_ORIENTATION_VERTICAL].min, + "nat-height", data->sizes[GTK_ORIENTATION_VERTICAL].nat, + "max-height", data->sizes[GTK_ORIENTATION_VERTICAL].max, + "strength", strength, + "name", data->name, + NULL); +} + +static void +constraints_start_element (GMarkupParseContext *context, + const char *element_name, + const char **attr_names, + const char **attr_values, + gpointer user_data, + GError **error) +{ + ConstraintsParserData *data = user_data; + + if (strcmp (element_name, "constraints") == 0) + { + if (!_gtk_builder_check_parent (data->builder, context, "object", error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_INVALID, NULL, NULL, + G_MARKUP_COLLECT_INVALID)) + _gtk_builder_prefix_error (data->builder, context, error); + } + else if (strcmp (element_name, "constraint") == 0) + { + const char *target_name, *target_attribute; + const char *relation_str = NULL; + const char *source_name = NULL, *source_attribute = NULL; + const char *multiplier_str = NULL, *constant_str = NULL; + const char *strength_str = NULL; + ConstraintData *cdata; + + if (!_gtk_builder_check_parent (data->builder, context, "constraints", error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING, "target", &target_name, + G_MARKUP_COLLECT_STRING, "target-attribute", &target_attribute, + G_MARKUP_COLLECT_STRING, "relation", &relation_str, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "source", &source_name, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "source-attribute", &source_attribute, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "multiplier", &multiplier_str, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "constant", &constant_str, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "strength", &strength_str, + G_MARKUP_COLLECT_INVALID)) + { + _gtk_builder_prefix_error (data->builder, context, error); + return; + } + + cdata = g_new0 (ConstraintData, 1); + cdata->target_name = g_strdup (target_name); + cdata->target_attr = g_strdup (target_attribute); + cdata->relation = g_strdup (relation_str); + cdata->source_name = g_strdup (source_name); + cdata->source_attr = g_strdup (source_attribute); + parse_double (multiplier_str, &cdata->multiplier, 1.0); + parse_double (constant_str, &cdata->constant, 0.0); + cdata->strength = g_strdup (strength_str); + + data->constraints = g_list_prepend (data->constraints, cdata); + } + else if (strcmp (element_name, "guide") == 0) + { + const char *min_width, *nat_width, *max_width; + const char *min_height, *nat_height, *max_height; + const char *strength_str; + const char *name; + GuideData *gdata; + + if (!_gtk_builder_check_parent (data->builder, context, "constraints", error)) + return; + + if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "min-width", &min_width, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "nat-width", &nat_width, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "max-width", &max_width, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "min-height", &min_height, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "nat-height", &nat_height, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "max-height", &max_height, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "strength", &strength_str, + G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "name", &name, + G_MARKUP_COLLECT_INVALID)) + { + _gtk_builder_prefix_error (data->builder, context, error); + return; + } + + gdata = g_new0 (GuideData, 1); + parse_int (min_width, &(gdata->sizes[GTK_ORIENTATION_HORIZONTAL].min), 0); + parse_int (nat_width, &(gdata->sizes[GTK_ORIENTATION_HORIZONTAL].nat), 0); + parse_int (max_width, &(gdata->sizes[GTK_ORIENTATION_HORIZONTAL].max), G_MAXINT); + parse_int (min_height, &(gdata->sizes[GTK_ORIENTATION_VERTICAL].min), 0); + parse_int (nat_height, &(gdata->sizes[GTK_ORIENTATION_VERTICAL].nat), 0); + parse_int (max_height, &(gdata->sizes[GTK_ORIENTATION_VERTICAL].max), G_MAXINT); + gdata->name = g_strdup (name); + gdata->strength = g_strdup (strength_str); + + data->guides = g_list_prepend (data->guides, gdata); + } + else + { + _gtk_builder_error_unhandled_tag (data->builder, context, + "GtkConstraintLayout", element_name, + error); + } +} + +static void +constraints_end_element (GMarkupParseContext *context, + const char *element_name, + gpointer user_data, + GError **error) +{ +} + +static const GMarkupParser constraints_parser = { + constraints_start_element, + constraints_end_element, + NULL, +}; + +static gboolean +gtk_constraint_layout_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const char *element_name, + GMarkupParser *parser, + gpointer *parser_data) +{ + if (strcmp (element_name, "constraints") == 0) + { + ConstraintsParserData *data = g_new (ConstraintsParserData, 1); + + data->layout = g_object_ref (GTK_CONSTRAINT_LAYOUT (buildable)); + data->builder = builder; + data->constraints = NULL; + data->guides = NULL; + + *parser = constraints_parser; + *parser_data = data; + + return TRUE; + } + + return FALSE; +} + +static void +gtk_constraint_layout_custom_tag_end (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const char *element_name, + gpointer data) +{ +} + +static void +gtk_constraint_layout_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const char *element_name, + gpointer user_data) +{ + ConstraintsParserData *data = user_data; + + if (strcmp (element_name, "constraints") == 0) + { + GList *l; + + data->guides = g_list_reverse (data->guides); + for (l = data->guides; l != NULL; l = l->next) + { + const GuideData *gdata = l->data; + GtkConstraintGuide *g; + GError *error = NULL; + + g = guide_data_to_guide (gdata, builder, &error); + if (error != NULL) + { + g_critical ("Unable to parse guide definition: %s", + error->message); + g_error_free (error); + continue; + } + + gtk_constraint_layout_add_guide (data->layout, g); + } + + data->constraints = g_list_reverse (data->constraints); + for (l = data->constraints; l != NULL; l = l->next) + { + const ConstraintData *cdata = l->data; + GtkConstraint *c; + GError *error = NULL; + + c = constraint_data_to_constraint (cdata, builder, &error); + if (error != NULL) + { + g_critical ("Unable to parse constraint definition '%s.%s [%s] %s.%s * %g + %g': %s", + cdata->target_name, cdata->target_attr, + cdata->relation, + cdata->source_name, cdata->source_attr, + cdata->multiplier, + cdata->constant, + error->message); + g_error_free (error); + continue; + } + + gtk_constraint_layout_add_constraint (data->layout, c); + } + + g_list_free_full (data->constraints, constraint_data_free); + g_list_free_full (data->guides, guide_data_free); + g_object_unref (data->layout); + g_free (data); + } +} + +static void +gtk_buildable_interface_init (GtkBuildableIface *iface) +{ + iface->custom_tag_start = gtk_constraint_layout_custom_tag_start; + iface->custom_tag_end = gtk_constraint_layout_custom_tag_end; + iface->custom_finished = gtk_constraint_layout_custom_finished; +} + +/** + * gtk_constraint_layout_new: + * + * Creates a new #GtkConstraintLayout layout manager. + * + * Returns: the newly created #GtkConstraintLayout + */ +GtkLayoutManager * +gtk_constraint_layout_new (void) +{ + return g_object_new (GTK_TYPE_CONSTRAINT_LAYOUT, NULL); +} + +/** + * gtk_constraint_layout_add_constraint: + * @layout: a #GtkConstraintLayout + * @constraint: (transfer full): a #GtkConstraint + * + * Adds a #GtkConstraint to the layout manager. + * + * The #GtkConstraint:source and #GtkConstraint:target + * properties of @constraint can be: + * + * - set to %NULL to indicate that the constraint refers to the + * widget using @layout + * - set to the #GtkWidget using @layout + * - set to a child of the #GtkWidget using @layout + * - set to a guide that is part of @layout + * + * The @layout acquires the ownership of @constraint after calling + * this function. + */ +void +gtk_constraint_layout_add_constraint (GtkConstraintLayout *layout, + GtkConstraint *constraint) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout)); + g_return_if_fail (GTK_IS_CONSTRAINT (constraint)); + g_return_if_fail (!gtk_constraint_is_attached (constraint)); + + layout_add_constraint (layout, constraint); + + g_hash_table_add (layout->constraints, constraint); + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); +} + +/** + * gtk_constraint_layout_remove_constraint: + * @layout: a #GtkConstraintLayout + * @constraint: a #GtkConstraint + * + * Removes @constraint from the layout manager, + * so that it no longer influences the layout. + */ +void +gtk_constraint_layout_remove_constraint (GtkConstraintLayout *layout, + GtkConstraint *constraint) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout)); + g_return_if_fail (GTK_IS_CONSTRAINT (constraint)); + g_return_if_fail (gtk_constraint_is_attached (constraint)); + + gtk_constraint_detach (constraint); + g_hash_table_remove (layout->constraints, constraint); + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); +} + +/** + * gtk_constraint_layout_remove_all_constraints: + * @layout: a #GtkConstraintLayout + * + * Removes all constraints from the layout manager. + */ +void +gtk_constraint_layout_remove_all_constraints (GtkConstraintLayout *layout) +{ + GHashTableIter iter; + gpointer key; + + g_return_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout)); + + g_hash_table_iter_init (&iter, layout->constraints); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + GtkConstraint *constraint = key; + + gtk_constraint_detach (constraint); + g_hash_table_iter_remove (&iter); + } + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); +} + +/** + * gtk_constraint_layout_add_guide: + * @layout: a #GtkConstraintLayout + * @guide: (transfer full): a #GtkConstraintGuide object + * + * Adds a guide to @layout. A guide can be used as + * the source or target of constraints, like a widget, + * but it is not visible. + * + * The @layout acquires the ownership of @guide after calling + * this function. + */ +void +gtk_constraint_layout_add_guide (GtkConstraintLayout *layout, + GtkConstraintGuide *guide) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout)); + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + g_return_if_fail (gtk_constraint_guide_get_layout (guide) == NULL); + + gtk_constraint_guide_set_layout (guide, layout); + g_hash_table_add (layout->guides, guide); + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); +} + +/** + * gtk_constraint_layout_remove_guide: + * @layout: a #GtkConstraintManager + * @guide: a #GtkConstraintGuide object + * + * Removes @guide from the layout manager, + * so that it no longer influences the layout. + */ +void +gtk_constraint_layout_remove_guide (GtkConstraintLayout *layout, + GtkConstraintGuide *guide) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout)); + g_return_if_fail (GTK_IS_CONSTRAINT_GUIDE (guide)); + g_return_if_fail (gtk_constraint_guide_get_layout (guide) == layout); + + gtk_constraint_guide_detach (guide); + + gtk_constraint_guide_set_layout (guide, NULL); + g_hash_table_remove (layout->guides, guide); + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); +} + +static GtkConstraintAttribute +attribute_from_name (const char *name) +{ + if (name == NULL || *name == '\0') + return GTK_CONSTRAINT_ATTRIBUTE_NONE; + + /* We sadly need to special case these two because the name does + * not match the VFL grammar rules + */ + if (strcmp (name, "centerX") == 0) + return GTK_CONSTRAINT_ATTRIBUTE_CENTER_X; + + if (strcmp (name, "centerY") == 0) + return GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y; + + for (int i = 0; i < G_N_ELEMENTS (attribute_names); i++) + { + if (strcmp (attribute_names[i], name) == 0) + return i; + } + + return GTK_CONSTRAINT_ATTRIBUTE_NONE; +} + +GQuark +gtk_constraint_vfl_parser_error_quark (void) +{ + return g_quark_from_static_string ("gtk-constraint-vfl-parser-error-quark"); +} + +/** + * gtk_constraint_layout_add_constraints_from_descriptionv: (rename-to gtk_constraint_layout_add_constraints_from_description) + * @layout: a #GtkConstraintLayout + * @lines: (array length=n_lines): an array of Visual Format Language lines + * defining a set of constraints + * @n_lines: the number of lines + * @hspacing: default horizontal spacing value, or -1 for the fallback value + * @vspacing: default vertical spacing value, or -1 for the fallback value + * @views: (element-type utf8 Gtk.ConstraintTarget): a dictionary of [ name, target ] + * pairs; the `name` keys map to the view names in the VFL lines, while + * the `target` values map to children of the widget using a #GtkConstraintLayout, or guides + * @error: return location for a #GError + * + * Creates a list of constraints from a formal description using a compact + * description syntax called VFL, or "Visual Format Language". + * + * The Visual Format Language is based on Apple's AutoLayout [VFL](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html). + * + * The @views dictionary is used to match #GtkConstraintTargets to the symbolic + * view name inside the VFL. + * + * The VFL grammar is: + * + * |[<!-- language="plain" --> + * <visualFormatString> = (<orientation>)? + * (<superview><connection>)? + * <view>(<connection><view>)* + * (<connection><superview>)? + * <orientation> = 'H' | 'V' + * <superview> = '|' + * <connection> = '' | '-' <predicateList> '-' | '-' + * <predicateList> = <simplePredicate> | <predicateListWithParens> + * <simplePredicate> = <metricName> | <positiveNumber> + * <predicateListWithParens> = '(' <predicate> (',' <predicate>)* ')' + * <predicate> = (<relation>)? <objectOfPredicate> (<operatorList>)? ('@' <priority>)? + * <relation> = '==' | '<=' | '>=' + * <objectOfPredicate> = <constant> | <viewName> | ('.' <attributeName>)? + * <priority> = <positiveNumber> | 'required' | 'strong' | 'medium' | 'weak' + * <constant> = <number> + * <operatorList> = (<multiplyOperator>)? (<addOperator>)? + * <multiplyOperator> = [ '*' | '/' ] <positiveNumber> + * <addOperator> = [ '+' | '-' ] <positiveNumber> + * <viewName> = [A-Za-z_]([A-Za-z0-9_]*) // A C identifier + * <metricName> = [A-Za-z_]([A-Za-z0-9_]*) // A C identifier + * <attributeName> = 'top' | 'bottom' | 'left' | 'right' | 'width' | 'height' | + * 'start' | 'end' | 'centerX' | 'centerY' | 'baseline' + * <positiveNumber> // A positive real number parseable by g_ascii_strtod() + * <number> // A real number parseable by g_ascii_strtod() + * ]| + * + * **Note**: The VFL grammar used by GTK is slightly different than the one + * defined by Apple, as it can use symbolic values for the constraint's + * strength instead of numeric values; additionally, GTK allows adding + * simple arithmetic operations inside predicates. + * + * Examples of VFL descriptions are: + * + * |[<!-- language="plain" --> + * // Default spacing + * [button]-[textField] + * + * // Width constraint + * [button(>=50)] + * + * // Connection to super view + * |-50-[purpleBox]-50-| + * + * // Vertical layout + * V:[topField]-10-[bottomField] + * + * // Flush views + * [maroonView][blueView] + * + * // Priority + * [button(100@strong)] + * + * // Equal widths + * [button1(==button2)] + * + * // Multiple predicates + * [flexibleButton(>=70,<=100)] + * + * // A complete line of layout + * |-[find]-[findNext]-[findField(>=20)]-| + * + * // Operators + * [button1(button2 / 3 + 50)] + * + * // Named attributes + * [button1(==button2.height)] + * ]| + * + * Returns: (transfer container) (element-type GtkConstraint): the list of + * #GtkConstraints that were added to the layout + */ +GList * +gtk_constraint_layout_add_constraints_from_descriptionv (GtkConstraintLayout *layout, + const char * const lines[], + gsize n_lines, + int hspacing, + int vspacing, + GHashTable *views, + GError **error) +{ + GtkConstraintVflParser *parser; + GList *res = NULL; + + g_return_val_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout), NULL); + g_return_val_if_fail (lines != NULL, NULL); + g_return_val_if_fail (views != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + parser = gtk_constraint_vfl_parser_new (); + gtk_constraint_vfl_parser_set_default_spacing (parser, hspacing, vspacing); + gtk_constraint_vfl_parser_set_views (parser, views); + + for (gsize i = 0; i < n_lines; i++) + { + const char *line = lines[i]; + GError *internal_error = NULL; + + gtk_constraint_vfl_parser_parse_line (parser, line, -1, &internal_error); + if (internal_error != NULL) + { + int offset = gtk_constraint_vfl_parser_get_error_offset (parser); + int range = gtk_constraint_vfl_parser_get_error_range (parser); + char *squiggly = NULL; + + if (range > 0) + { + squiggly = g_new (char, range + 1); + + for (int r = 0; r < range; i++) + squiggly[r] = '~'; + + squiggly[range] = '\0'; + } + + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + internal_error->code, + "%" G_GSIZE_FORMAT ":%d: %s\n" + "%s\n" + "%*s^%s", + i, offset + 1, + internal_error->message, + line, + offset, " ", squiggly != NULL ? squiggly : ""); + + g_free (squiggly); + g_error_free (internal_error); + gtk_constraint_vfl_parser_free (parser); + return res; + } + + int n_constraints = 0; + GtkConstraintVfl *constraints = gtk_constraint_vfl_parser_get_constraints (parser, &n_constraints); + for (int j = 0; j < n_constraints; j++) + { + const GtkConstraintVfl *c = &constraints[j]; + gpointer source, target; + GtkConstraintAttribute source_attr, target_attr; + + target = g_hash_table_lookup (views, c->view1); + target_attr = attribute_from_name (c->attr1); + + if (c->view2 != NULL) + source = g_hash_table_lookup (views, c->view2); + else + source = NULL; + + if (c->attr2 != NULL) + source_attr = attribute_from_name (c->attr2); + else + source_attr = GTK_CONSTRAINT_ATTRIBUTE_NONE; + + GtkConstraint *constraint = + gtk_constraint_new (target, target_attr, + c->relation, + source, source_attr, + c->multiplier, + c->constant, + c->strength); + + layout_add_constraint (layout, constraint); + g_hash_table_add (layout->constraints, constraint); + + res = g_list_prepend (res, constraint); + } + + g_free (constraints); + } + + gtk_constraint_vfl_parser_free (parser); + + gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (layout)); + + return res; +} + +/** + * gtk_constraint_layout_add_constraints_from_description: + * @layout: a #GtkConstraintLayout + * @lines: (array length=n_lines): an array of Visual Format Language lines + * defining a set of constraints + * @n_lines: the number of lines + * @hspacing: default horizontal spacing value, or -1 for the fallback value + * @vspacing: default vertical spacing value, or -1 for the fallback value + * @error: return location for a #GError + * @first_view: the name of a view in the VFL description, followed by the + * #GtkConstraintTarget to which it maps + * @...: a %NULL-terminated list of view names and #GtkConstraintTargets + * + * Creates a list of constraints they formal description using a compact + * description syntax called VFL, or "Visual Format Language". + * + * This function is a convenience wrapper around + * gtk_constraint_layout_add_constraints_from_descriptionv(), using + * variadic arguments to populate the view/target map. + * + * Returns: (transfer container) (element-type GtkConstraint): the list of + * #GtkConstraints that were added to the layout + */ +GList * +gtk_constraint_layout_add_constraints_from_description (GtkConstraintLayout *layout, + const char * const lines[], + gsize n_lines, + int hspacing, + int vspacing, + GError **error, + const char *first_view, + ...) +{ + GtkConstraintVflParser *parser; + GHashTable *views; + const char *view; + GList *res; + va_list args; + + g_return_val_if_fail (GTK_IS_CONSTRAINT_LAYOUT (layout), NULL); + g_return_val_if_fail (lines != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + g_return_val_if_fail (first_view != NULL, NULL); + + parser = gtk_constraint_vfl_parser_new (); + gtk_constraint_vfl_parser_set_default_spacing (parser, hspacing, vspacing); + + views = g_hash_table_new (g_str_hash, g_str_equal); + + va_start (args, first_view); + + view = first_view; + while (view != NULL) + { + GtkConstraintTarget *target = va_arg (args, GtkConstraintTarget *); + + if (target == NULL) + break; + + g_hash_table_insert (views, (gpointer) view, target); + + view = va_arg (args, const char *); + } + + va_end (args); + + res = + gtk_constraint_layout_add_constraints_from_descriptionv (layout, lines, n_lines, + hspacing, vspacing, + views, + error); + + g_hash_table_unref (views); + + return res; +} diff --git a/gtk/gtkconstraintlayout.h b/gtk/gtkconstraintlayout.h new file mode 100644 index 0000000000..90d5e84926 --- /dev/null +++ b/gtk/gtkconstraintlayout.h @@ -0,0 +1,88 @@ +/* gtkconstraintlayout.h: Layout manager using constraints + * Copyright 2019 GNOME Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ +#pragma once + +#include <gtk/gtklayoutmanager.h> +#include <gtk/gtkconstraint.h> +#include <gtk/gtkconstraintguide.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_CONSTRAINT_LAYOUT (gtk_constraint_layout_get_type ()) +#define GTK_TYPE_CONSTRAINT_LAYOUT_CHILD (gtk_constraint_layout_child_get_type ()) +#define GTK_CONSTRAINT_VFL_PARSER_ERROR (gtk_constraint_vfl_parser_error_quark ()) + +/** + * GtkConstraintLayoutChild: + * + * A #GtkLayoutChild in a #GtkConstraintLayout. + */ +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkConstraintLayoutChild, gtk_constraint_layout_child, GTK, CONSTRAINT_LAYOUT_CHILD, GtkLayoutChild) + +/** + * GtkConstraintLayout: + * + * A layout manager using #GtkConstraint to describe + * relations between widgets. + */ +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkConstraintLayout, gtk_constraint_layout, GTK, CONSTRAINT_LAYOUT, GtkLayoutManager) + +GDK_AVAILABLE_IN_ALL +GQuark gtk_constraint_vfl_parser_error_quark (void); + +GDK_AVAILABLE_IN_ALL +GtkLayoutManager * gtk_constraint_layout_new (void); + +GDK_AVAILABLE_IN_ALL +void gtk_constraint_layout_add_constraint (GtkConstraintLayout *layout, + GtkConstraint *constraint); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_layout_remove_constraint (GtkConstraintLayout *layout, + GtkConstraint *constraint); + +GDK_AVAILABLE_IN_ALL +void gtk_constraint_layout_add_guide (GtkConstraintLayout *layout, + GtkConstraintGuide *guide); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_layout_remove_guide (GtkConstraintLayout *layout, + GtkConstraintGuide *guide); +GDK_AVAILABLE_IN_ALL +void gtk_constraint_layout_remove_all_constraints (GtkConstraintLayout *layout); + +GDK_AVAILABLE_IN_ALL +GList * gtk_constraint_layout_add_constraints_from_description (GtkConstraintLayout *layout, + const char * const lines[], + gsize n_lines, + int hspacing, + int vspacing, + GError **error, + const char *first_view, + ...) G_GNUC_NULL_TERMINATED; +GDK_AVAILABLE_IN_ALL +GList * gtk_constraint_layout_add_constraints_from_descriptionv (GtkConstraintLayout *layout, + const char * const lines[], + gsize n_lines, + int hspacing, + int vspacing, + GHashTable *views, + GError **error); + +G_END_DECLS diff --git a/gtk/gtkconstraintlayoutprivate.h b/gtk/gtkconstraintlayoutprivate.h new file mode 100644 index 0000000000..0d591911fd --- /dev/null +++ b/gtk/gtkconstraintlayoutprivate.h @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Matthias Clasen + */ + +#pragma once + +#include "gtkconstraintlayout.h" +#include "gtkconstraintsolverprivate.h" + +G_BEGIN_DECLS + +GtkConstraintSolver * +gtk_constraint_layout_get_solver (GtkConstraintLayout *layout); + +GtkConstraintVariable * +gtk_constraint_layout_get_attribute (GtkConstraintLayout *layout, + GtkConstraintAttribute attr, + const char *prefix, + GtkWidget *widget, + GHashTable *bound_attributes); + +G_END_DECLS diff --git a/gtk/gtkconstraintprivate.h b/gtk/gtkconstraintprivate.h new file mode 100644 index 0000000000..d90b313f42 --- /dev/null +++ b/gtk/gtkconstraintprivate.h @@ -0,0 +1,60 @@ +/* gtkconstraintprivate.h: Constraint between two widgets + * Copyright 2019 GNOME Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#pragma once + +#include "gtkconstraint.h" +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +struct _GtkConstraint +{ + GObject parent_instance; + + GtkConstraintAttribute target_attribute; + GtkConstraintAttribute source_attribute; + + GtkConstraintTarget *target; + GtkConstraintTarget *source; + + GtkConstraintRelation relation; + + double multiplier; + double constant; + + int strength; + + /* A reference to the real constraint inside the + * GtkConstraintSolver, so we can remove it when + * finalizing the GtkConstraint instance + */ + GtkConstraintRef *constraint_ref; + + GtkConstraintSolver *solver; + + guint active : 1; +}; + +void gtk_constraint_attach (GtkConstraint *constraint, + GtkConstraintSolver *solver, + GtkConstraintRef *ref); +void gtk_constraint_detach (GtkConstraint *constraint); + +G_END_DECLS diff --git a/gtk/gtkconstraintsolver.c b/gtk/gtkconstraintsolver.c new file mode 100644 index 0000000000..b7097024c5 --- /dev/null +++ b/gtk/gtkconstraintsolver.c @@ -0,0 +1,2247 @@ +/* gtkconstraintsolver.c: Constraint solver based on the Cassowary method + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +/*< private > + * SECTION: gtkconstraintsolver + * @Title: GtkConstraintSolver + * @Short_description: An incremental solver for a tableau of linear equations + * + * GtkConstraintSolver is an object that encodes constraints into a tableau + * of linear equations and solves them, using an incremental optimization + * algorithm known as the "Cassowary Linear Arithmetic Constraint Solving + * Algorithm" (Badros, Borning & Stuckey 2001). + * + * Each constraint is expressed as a linear equation, whose terms are variables + * containing widget attributes like the width, height, or position; the simplex + * solver takes all the constraints and incrementally optimizes the tableau to + * replace known terms; additionally, the algorithm will try to assign a value + * to all remaining variables in order to satisfy the various constraints. + * + * Each constraint is given a "strength", which determines whether satisfying + * the constraint is required in order to solve the tableau or not. + * + * A typical example of GtkConstraintSolver use is solving the following + * system of constraints: + * + * - [required] right = left + 10 + * - [required] right ≤ 100 + * - [required] middle = left + right / 2 + * - [required] left ≥ 0 + * + * Once we create a GtkConstraintSolver instance, we need to create the + * various variables and expressions that represent the values we want to + * compute and the constraints we wish to impose on the solutions: + * + * |[ + * GtkConstraintSolver *solver = gtk_constraint_solver_new (); + * + * // Our variables + * GtkConstraintVariable *left = + * gtk_constraint_solver_create_variable (solver, NULL, "left", 0.0); + * GtkConstraintVariable *middle = + * gtk_constraint_solver_create_variable (solver, NULL, "middle", 0.0); + * GtkConstraintVariable *right = + * gtk_constraint_solver_create_variable (solver, NULL, "right", 0.0); + * + * // Our constraints + * GtkConstraintExpressionBuilder builder; + * GtkConstraintExpression *e; + * + * // right = left + 10 + * gtk_constraint_expression_builder_init (&builder, solver); + * gtk_constraint_expression_builder_term (&builder, left); + * gtk_constraint_expression_builder_plus (&builder); + * gtk_constraint_expression_builder_constant (&builder, 10.0); + * e = gtk_constraint_expression_builder_finish (&builder); + * gtk_constraint_solver_add_constraint (solver, + * right, GTK_CONSTRAINT_RELATION_EQ, e, + * GTK_CONSTRAINT_STRENGTH_REQUIRED); + * + * // right ≤ 100 + * gtk_constraint_expression_builder_constant (&builder, 100.0); + * e = gtk_constraint_expression_builder_finish (&builder); + * gtk_constraint_solver_add_constraint (solver, + * right, GTK_CONSTRAINT_RELATION_LE, e, + * GTK_CONSTRAINT_STRENGTH_REQUIRED); + * + * // middle = (left + right) / 2 + * gtk_constraint_expression_builder_term (&builder, left); + * gtk_constraint_expression_builder_plus (&builder) + * gtk_constraint_expression_builder_term (&builder, right); + * gtk_constraint_expression_builder_divide_by (&builder); + * gtk_constraint_expression_builder_constant (&builder, 2.0); + * e = gtk_constraint_expression_builder_finish (&builder); + * gtk_constraint_solver_add_constraint (solver + * middle, GTK_CONSTRAINT_RELATION_EQ, e, + * GTK_CONSTRAINT_STRENGTH_REQUIRED); + * + * // left ≥ 0 + * gtk_constraint_expression_builder_constant (&builder, 0.0); + * e = gtk_constraint_expression_builder_finish (&builder); + * gtk_constraint_solver_add_constraint (solver, + * left, GTK_CONSTRAINT_RELATION_GE, e, + * GTK_CONSTRAINT_STRENGTH_REQUIRED); + * ]| + * + * Now that we have all our constraints in place, suppose we wish to find + * the values of `left` and `right` if we specify the value of `middle`. In + * order to do that, we need to add an additional "stay" constraint, i.e. + * a constraint that tells the solver to optimize for the solution that keeps + * the variable in place: + * + * |[ + * // Set the value first + * gtk_constraint_variable_set_value (middle, 45.0); + * // and then add the stay constraint, with a weak strength + * gtk_constraint_solver_add_stay_variable (solver, middle, GTK_CONSTRAINT_STRENGTH_WEAK); + * ]| + * + * GtkConstraintSolver incrementally solves the system every time a constraint + * is added or removed, so it's possible to query the values of the variables + * immediately afterward: + * + * |[ + * double left_val = gtk_constraint_variable_get_value (left); + * double right_val = gtk_constraint_variable_get_value (right); + * double middle_val = gtk_constraint_variable_get_value (middle); + * + * // These are the values computed by the solver: + * g_assert_cmpfloat_with_epsilon (left_val, 40.0, 0.001); + * g_assert_cmpfloat_with_epsilon (middle_val, 45.0, 0.001); + * g_assert_cmpfloat_with_epsilon (right_val, 50.0, 0.001); + * ]| + * + * As you can see: + * + * - the middle value hasn't changed + * - the left value is ≥ 0 + * - the right value is ≤ 100 + * - the right value is left + 10 + * - the middle value is (left + right) / 2.0 + * + * For more information about the Cassowary constraint solving algorithm and + * toolkit, see the following papers: + * + * - Badros G & Borning A, 1998, 'Cassowary Linear Arithmetic Constraint + * Solving Algorithm: Interface and Implementation', Technical Report + * UW-CSE-98-06-04, June 1998 (revised July 1999) + * https://constraints.cs.washington.edu/cassowary/cassowary-tr.pdf + * - Badros G, Borning A & Stuckey P, 2001, 'Cassowary Linear Arithmetic + * Constraint Solving Algorithm', ACM Transactions on Computer-Human + * Interaction, vol. 8 no. 4, December 2001, pages 267-306 + * https://constraints.cs.washington.edu/solvers/cassowary-tochi.pdf + * + * The following implementation is based on these projects: + * + * - the original [C++ implementation](https://sourceforge.net/projects/cassowary/) + * - the JavaScript port [Cassowary.js](https://github.com/slightlyoff/cassowary.js) + * - the Python port [Cassowary](https://github.com/pybee/cassowary) + */ + +#include "config.h" + +#include "gtkconstraintsolverprivate.h" +#include "gtkconstraintexpressionprivate.h" + +#include "gtkdebug.h" + +#include <glib.h> +#include <string.h> +#include <math.h> +#include <float.h> + +struct _GtkConstraintRef +{ + /* The constraint's normal form inside the solver: + * + * x - (y × coefficient + constant) = 0 + * + * We only use equalities, and replace inequalities with slack + * variables. + */ + GtkConstraintExpression *expression; + + /* A constraint variable, only used by stay and edit constraints */ + GtkConstraintVariable *variable; + + /* The original relation used when creating the constraint */ + GtkConstraintRelation relation; + + /* The strength of the constraint; this value is used to strengthen + * or weaken a constraint weight in the tableau when coming to a + * solution + */ + int strength; + + GtkConstraintSolver *solver; + + guint is_edit : 1; + guint is_stay : 1; +}; + +typedef struct { + GtkConstraintRef *constraint; + + GtkConstraintVariable *eplus; + GtkConstraintVariable *eminus; + + double prev_constant; +} EditInfo; + +typedef struct { + GtkConstraintRef *constraint; +} StayInfo; + +struct _GtkConstraintSolver +{ + GObject parent_instance; + + /* HashTable<Variable, VariableSet>; owns keys and values */ + GHashTable *columns; + /* HashTable<Variable, Expression>; owns keys and values */ + GHashTable *rows; + + /* Set<Variable>; does not own keys */ + GHashTable *external_rows; + /* Set<Variable>; does not own keys */ + GHashTable *external_parametric_vars; + + /* Vec<Variable> */ + GPtrArray *infeasible_rows; + /* Vec<VariablePair>; owns the pair */ + GPtrArray *stay_error_vars; + + /* HashTable<Constraint, VariableSet>; owns the set */ + GHashTable *error_vars; + /* HashTable<Constraint, Variable> */ + GHashTable *marker_vars; + + /* HashTable<Variable, EditInfo>; does not own keys, but owns values */ + GHashTable *edit_var_map; + /* HashTable<Variable, StayInfo>; does not own keys, but owns values */ + GHashTable *stay_var_map; + + GtkConstraintVariable *objective; + + /* Set<Constraint>; owns the key */ + GHashTable *constraints; + + /* Counters */ + int var_counter; + int slack_counter; + int artificial_counter; + int dummy_counter; + int optimize_count; + int freeze_count; + + /* Bitfields; keep at the end */ + guint auto_solve : 1; + guint needs_solving : 1; + guint in_edit_phase : 1; +}; + +static void gtk_constraint_ref_free (GtkConstraintRef *ref); +static void edit_info_free (gpointer data); + +G_DEFINE_TYPE (GtkConstraintSolver, gtk_constraint_solver, G_TYPE_OBJECT) + +static void +gtk_constraint_solver_finalize (GObject *gobject) +{ + GtkConstraintSolver *self = GTK_CONSTRAINT_SOLVER (gobject); + + g_hash_table_remove_all (self->constraints); + g_clear_pointer (&self->constraints, g_hash_table_unref); + + g_clear_pointer (&self->stay_error_vars, g_ptr_array_unref); + g_clear_pointer (&self->infeasible_rows, g_ptr_array_unref); + + g_clear_pointer (&self->external_rows, g_hash_table_unref); + g_clear_pointer (&self->external_parametric_vars, g_hash_table_unref); + g_clear_pointer (&self->error_vars, g_hash_table_unref); + g_clear_pointer (&self->marker_vars, g_hash_table_unref); + g_clear_pointer (&self->edit_var_map, g_hash_table_unref); + g_clear_pointer (&self->stay_var_map, g_hash_table_unref); + + g_clear_pointer (&self->rows, g_hash_table_unref); + g_clear_pointer (&self->columns, g_hash_table_unref); + + G_OBJECT_CLASS (gtk_constraint_solver_parent_class)->finalize (gobject); +} + +static void +gtk_constraint_solver_class_init (GtkConstraintSolverClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gtk_constraint_solver_finalize; +} + +static void +gtk_constraint_solver_init (GtkConstraintSolver *self) +{ + self->columns = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) gtk_constraint_variable_unref, + (GDestroyNotify) gtk_constraint_variable_set_free); + + self->rows = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) gtk_constraint_variable_unref, + (GDestroyNotify) gtk_constraint_expression_unref); + + self->external_rows = g_hash_table_new (NULL, NULL); + + self->external_parametric_vars = g_hash_table_new (NULL, NULL); + + self->infeasible_rows = g_ptr_array_new (); + + self->stay_error_vars = + g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_constraint_variable_pair_free); + + self->error_vars = + g_hash_table_new_full (NULL, NULL, + NULL, + (GDestroyNotify) gtk_constraint_variable_set_free); + + self->marker_vars = g_hash_table_new (NULL, NULL); + + self->edit_var_map = g_hash_table_new_full (NULL, NULL, + NULL, + edit_info_free); + + self->stay_var_map = g_hash_table_new_full (NULL, NULL, + NULL, + g_free); + + /* The rows table owns the objective variable */ + self->objective = gtk_constraint_variable_new_objective ("Z"); + g_hash_table_insert (self->rows, + self->objective, + gtk_constraint_expression_new (0.0)); + + self->constraints = + g_hash_table_new_full (NULL, NULL, + (GDestroyNotify) gtk_constraint_ref_free, + NULL); + + self->slack_counter = 0; + self->dummy_counter = 0; + self->artificial_counter = 0; + self->freeze_count = 0; + + self->needs_solving = FALSE; + self->auto_solve = TRUE; +} + +static void +gtk_constraint_ref_free (GtkConstraintRef *self) +{ + gtk_constraint_solver_remove_constraint (self->solver, self); + + gtk_constraint_expression_unref (self->expression); + + if (self->is_edit || self->is_stay) + { + g_assert (self->variable != NULL); + gtk_constraint_variable_unref (self->variable); + } + + g_free (self); +} + +static gboolean +gtk_constraint_ref_is_inequality (const GtkConstraintRef *self) +{ + return self->relation != GTK_CONSTRAINT_RELATION_EQ; +} + +static gboolean +gtk_constraint_ref_is_required (const GtkConstraintRef *self) +{ + return self->strength == GTK_CONSTRAINT_STRENGTH_REQUIRED; +} + +static const char *relations[] = { + "<=", + "==", + ">=", +}; + +static const char * +relation_to_string (GtkConstraintRelation r) +{ + return relations[r + 1]; +} + +static const char * +strength_to_string (int s) +{ + if (s >= GTK_CONSTRAINT_STRENGTH_STRONG) + return "strong"; + + if (s >= GTK_CONSTRAINT_STRENGTH_MEDIUM) + return "medium"; + + return "weak"; +} + +static char * +gtk_constraint_ref_to_string (const GtkConstraintRef *self) +{ + GString *buf = g_string_new (NULL); + char *str; + + if (self->is_stay) + g_string_append (buf, "[stay]"); + else if (self->is_edit) + g_string_append (buf, "[edit]"); + + str = gtk_constraint_expression_to_string (self->expression); + g_string_append (buf, str); + g_free (str); + + g_string_append_c (buf, ' '); + g_string_append (buf, relation_to_string (self->relation)); + g_string_append (buf, " 0.0"); + + if (gtk_constraint_ref_is_required (self)) + g_string_append (buf, " [strength:required]"); + else + g_string_append_printf (buf, " [strength:%d (%s)]", + self->strength, + strength_to_string (self->strength)); + + return g_string_free (buf, FALSE); +} + +static GtkConstraintVariableSet * +gtk_constraint_solver_get_column_set (GtkConstraintSolver *self, + GtkConstraintVariable *param_var) +{ + return g_hash_table_lookup (self->columns, param_var); +} + +static gboolean +gtk_constraint_solver_column_has_key (GtkConstraintSolver *self, + GtkConstraintVariable *subject) +{ + return g_hash_table_contains (self->columns, subject); +} + +static void +gtk_constraint_solver_insert_column_variable (GtkConstraintSolver *self, + GtkConstraintVariable *param_var, + GtkConstraintVariable *row_var) +{ + GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, param_var); + + if (cset == NULL) + { + cset = gtk_constraint_variable_set_new (); + g_hash_table_insert (self->columns, gtk_constraint_variable_ref (param_var), cset); + } + + if (row_var != NULL) + gtk_constraint_variable_set_add (cset, row_var); +} + +static void +gtk_constraint_solver_insert_error_variable (GtkConstraintSolver *self, + GtkConstraintRef *constraint, + GtkConstraintVariable *variable) +{ + GtkConstraintVariableSet *cset = g_hash_table_lookup (self->error_vars, constraint); + + if (cset == NULL) + { + cset = gtk_constraint_variable_set_new (); + g_hash_table_insert (self->error_vars, constraint, cset); + } + + gtk_constraint_variable_set_add (cset, variable); +} + +static void +gtk_constraint_solver_reset_stay_constants (GtkConstraintSolver *self) +{ + int i; + + for (i = 0; i < self->stay_error_vars->len; i++) + { + GtkConstraintVariablePair *pair = g_ptr_array_index (self->stay_error_vars, i); + GtkConstraintExpression *expression; + + expression = g_hash_table_lookup (self->rows, pair->first); + + if (expression == NULL) + expression = g_hash_table_lookup (self->rows, pair->second); + + if (expression != NULL) + gtk_constraint_expression_set_constant (expression, 0.0); + } +} + +static void +gtk_constraint_solver_set_external_variables (GtkConstraintSolver *self) +{ + GHashTableIter iter; + gpointer key_p; + + g_hash_table_iter_init (&iter, self->external_parametric_vars); + while (g_hash_table_iter_next (&iter, &key_p, NULL)) + { + GtkConstraintVariable *variable = key_p; + + if (g_hash_table_contains (self->rows, variable)) + continue; + + gtk_constraint_variable_set_value (variable, 0.0); + } + + g_hash_table_iter_init (&iter, self->external_rows); + while (g_hash_table_iter_next (&iter, &key_p, NULL)) + { + GtkConstraintVariable *variable = key_p; + GtkConstraintExpression *expression; + double constant; + + expression = g_hash_table_lookup (self->rows, variable); + constant = gtk_constraint_expression_get_constant (expression); + + gtk_constraint_variable_set_value (variable, constant); + } + + self->needs_solving = FALSE; +} + +static void +gtk_constraint_solver_add_row (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintExpression *expression) +{ + GtkConstraintExpressionIter iter; + GtkConstraintVariable *t_v; + double t_c; + + g_hash_table_insert (self->rows, + gtk_constraint_variable_ref (variable), + gtk_constraint_expression_ref (expression)); + + gtk_constraint_expression_iter_init (&iter, expression); + while (gtk_constraint_expression_iter_next (&iter, &t_v, &t_c)) + { + gtk_constraint_solver_insert_column_variable (self, t_v, variable); + + if (gtk_constraint_variable_is_external (t_v)) + g_hash_table_add (self->external_parametric_vars, t_v); + } + + if (gtk_constraint_variable_is_external (variable)) + g_hash_table_add (self->external_rows, variable); +} + +static void +gtk_constraint_solver_remove_column (GtkConstraintSolver *self, + GtkConstraintVariable *variable) +{ + GtkConstraintVariable *v; + GtkConstraintVariableSetIter iter; + GtkConstraintVariableSet *cset; + + /* Take a reference on the variable, as we're going to remove it + * from various maps and we want to guarantee the pointer is + * valid until we leave this function + */ + gtk_constraint_variable_ref (variable); + + cset = g_hash_table_lookup (self->columns, variable); + if (cset == NULL) + goto out; + + gtk_constraint_variable_set_iter_init (&iter, cset); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); + + gtk_constraint_expression_remove_variable (e, variable); + } + + g_hash_table_remove (self->columns, variable); + +out: + if (gtk_constraint_variable_is_external (variable)) + { + g_hash_table_remove (self->external_rows, variable); + g_hash_table_remove (self->external_parametric_vars, variable); + } + + gtk_constraint_variable_unref (variable); +} + +static GtkConstraintExpression * +gtk_constraint_solver_remove_row (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + gboolean free_res) +{ + GtkConstraintExpression *e; + GtkConstraintExpressionIter iter; + GtkConstraintVariable *t_v; + double t_c; + + e = g_hash_table_lookup (self->rows, variable); + g_assert (e != NULL); + + gtk_constraint_expression_ref (e); + + gtk_constraint_expression_iter_init (&iter, e); + while (gtk_constraint_expression_iter_next (&iter, &t_v, &t_c)) + { + GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, t_v); + + if (cset != NULL) + gtk_constraint_variable_set_remove (cset, variable); + } + + g_ptr_array_remove (self->infeasible_rows, variable); + + if (gtk_constraint_variable_is_external (variable)) + g_hash_table_remove (self->external_rows, variable); + + g_hash_table_remove (self->rows, variable); + + if (free_res) + { + gtk_constraint_expression_unref (e); + return NULL; + } + + return e; +} + +/*< private > + * gtk_constraint_solver_substitute_out: + * @self: a #GtkConstraintSolver + * @old_variable: a #GtkConstraintVariable + * @expression: a #GtkConstraintExpression + * + * Replaces @old_variable in every row of the tableau with @expression. + */ +static void +gtk_constraint_solver_substitute_out (GtkConstraintSolver *self, + GtkConstraintVariable *old_variable, + GtkConstraintExpression *expression) +{ + GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, old_variable); + if (cset != NULL) + { + GtkConstraintVariableSetIter iter; + GtkConstraintVariable *v; + + gtk_constraint_variable_set_iter_init (&iter, cset); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + GtkConstraintExpression *row = g_hash_table_lookup (self->rows, v); + + gtk_constraint_expression_substitute_out (row, old_variable, expression, v, self); + + if (gtk_constraint_variable_is_restricted (v) && + gtk_constraint_expression_get_constant (row) < 0) + g_ptr_array_add (self->infeasible_rows, v); + } + } + + if (gtk_constraint_variable_is_external (old_variable)) + { + g_hash_table_add (self->external_rows, old_variable); + g_hash_table_remove (self->external_parametric_vars, old_variable); + } + + g_hash_table_remove (self->columns, old_variable); +} + +/*< private > + * gtk_constraint_solver_pivot: + * @self: a #GtkConstraintSolver + * @entry_var: a #GtkConstraintVariable + * @exit_var: a #GtkConstraintVariable + * + * Pivots the #GtkConstraintSolver. + * + * This function will move @entry_var into the basis of the tableau, + * making it a basic variable; and move @exit_var out of the basis of + * the tableau, making it a parametric variable. + */ +static void +gtk_constraint_solver_pivot (GtkConstraintSolver *self, + GtkConstraintVariable *entry_var, + GtkConstraintVariable *exit_var) +{ + GtkConstraintExpression *expr; + + if (entry_var != NULL) + gtk_constraint_variable_ref (entry_var); + else + g_critical ("INTERNAL: invalid entry variable during pivot"); + + if (exit_var != NULL) + gtk_constraint_variable_ref (exit_var); + else + g_critical ("INTERNAL: invalid exit variable during pivot"); + + /* We keep a reference to the expression */ + expr = gtk_constraint_solver_remove_row (self, exit_var, FALSE); + + gtk_constraint_expression_change_subject (expr, exit_var, entry_var); + gtk_constraint_solver_substitute_out (self, entry_var, expr); + + if (gtk_constraint_variable_is_external (entry_var)) + g_hash_table_remove (self->external_parametric_vars, entry_var); + + gtk_constraint_solver_add_row (self, entry_var, expr); + + gtk_constraint_variable_unref (entry_var); + gtk_constraint_variable_unref (exit_var); + gtk_constraint_expression_unref (expr); +} + +static void +gtk_constraint_solver_optimize (GtkConstraintSolver *self, + GtkConstraintVariable *z) +{ + GtkConstraintVariable *entry = NULL, *exit = NULL; + GtkConstraintExpression *z_row = g_hash_table_lookup (self->rows, z); + +#ifdef G_ENABLE_DEBUG + gint64 start_time = g_get_monotonic_time (); +#endif + + g_assert (z_row != NULL); + + self->optimize_count += 1; + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (CONSTRAINTS)) + { + char *str = gtk_constraint_variable_to_string (z); + g_message ("optimize: %s", str); + g_free (str); + } +#endif + + while (TRUE) + { + GtkConstraintVariableSet *column_vars; + GtkConstraintVariableSetIter viter; + GtkConstraintExpressionIter eiter; + GtkConstraintVariable *t_v, *v; + double t_c; + double objective_coefficient = 0.0; + double min_ratio; + double r; + + gtk_constraint_expression_iter_init (&eiter, z_row); + while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) + { + if (gtk_constraint_variable_is_pivotable (t_v) && t_c < objective_coefficient) + { + entry = t_v; + objective_coefficient = t_c; + break; + } + } + + if (objective_coefficient >= -1e-8) + break; + + min_ratio = DBL_MAX; + r = 0; + + column_vars = gtk_constraint_solver_get_column_set (self, entry); + gtk_constraint_variable_set_iter_init (&viter, column_vars); + while (gtk_constraint_variable_set_iter_next (&viter, &v)) + { + if (gtk_constraint_variable_is_pivotable (v)) + { + GtkConstraintExpression *expr = g_hash_table_lookup (self->rows, v); + double coeff = gtk_constraint_expression_get_coefficient (expr, entry); + + if (coeff < 0.0) + { + double constant = gtk_constraint_expression_get_constant (expr); + + r = -1.0 * constant / coeff; + if (r < min_ratio) + { + min_ratio = r; + exit = v; + } + } + } + } + + if (min_ratio == DBL_MAX) + { + GTK_NOTE (CONSTRAINTS, g_message ("Unbounded objective variable during optimization")); + break; + } + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (CONSTRAINTS)) + { + char *entry_s = gtk_constraint_variable_to_string (entry); + char *exit_s = gtk_constraint_variable_to_string (exit); + g_message ("pivot(entry: %s, exit: %s)", entry_s, exit_s); + g_free (entry_s); + g_free (exit_s); + } +#endif + + gtk_constraint_solver_pivot (self, entry, exit); + } + + GTK_NOTE (CONSTRAINTS, + g_message ("solver.optimize.time := %.3f ms (pass: %d)", + (float) (g_get_monotonic_time () - start_time) / 1000.f, + self->optimize_count)); +} + +/*< private > + * gtk_constraint_solver_new_expression: + * @self: a #GtkConstraintSolver + * @constraint: a #GtkConstraintRef + * @eplus_p: (out) (optional): the positive error variable + * @eminus_p: (out) (optional): the negative error variable + * @prev_constant_p: the constant part of the @constraint's expression + * + * Creates a new expression for the @constraint, replacing + * any basic variable with their expressions, and normalizing + * the terms to avoid a negative constant. + * + * If the @constraint is not required, this function will add + * error variables with the appropriate weight to the tableau. + * + * Returns: (transfer full): the new expression for the constraint + */ +static GtkConstraintExpression * +gtk_constraint_solver_new_expression (GtkConstraintSolver *self, + GtkConstraintRef *constraint, + GtkConstraintVariable **eplus_p, + GtkConstraintVariable **eminus_p, + double *prev_constant_p) +{ + GtkConstraintExpression *cn_expr = constraint->expression; + GtkConstraintExpression *expr; + GtkConstraintExpressionIter eiter; + GtkConstraintVariable *t_v; + double t_c; + + if (eplus_p != NULL) + *eplus_p = NULL; + if (eminus_p != NULL) + *eminus_p = NULL; + if (prev_constant_p != NULL) + *prev_constant_p = 0.0; + + expr = gtk_constraint_expression_new (gtk_constraint_expression_get_constant (cn_expr)); + + gtk_constraint_expression_iter_init (&eiter, cn_expr); + while (gtk_constraint_expression_iter_next (&eiter, &t_v, &t_c)) + { + GtkConstraintExpression *e = g_hash_table_lookup (self->rows, t_v); + + if (e == NULL) + gtk_constraint_expression_add_variable (expr, t_v, t_c, NULL, self); + else + gtk_constraint_expression_add_expression (expr, e, t_c, NULL, self); + } + + if (gtk_constraint_ref_is_inequality (constraint)) + { + GtkConstraintVariable *slack_var; + + /* If the constraint is an inequality, we add a slack variable to + * turn it into an equality, e.g. from + * + * expr ≥ 0 + * + * to + * + * expr - slack = 0 + * + * Additionally, if the constraint is not required we add an + * error variable with the weight of the constraint: + * + * expr - slack + error = 0 + */ + self->slack_counter += 1; + + slack_var = gtk_constraint_variable_new_slack ("s"); + gtk_constraint_expression_set_variable (expr, slack_var, -1.0); + gtk_constraint_variable_unref (slack_var); + + g_hash_table_insert (self->marker_vars, constraint, slack_var); + + if (!gtk_constraint_ref_is_required (constraint)) + { + GtkConstraintExpression *z_row; + GtkConstraintVariable *eminus; + + self->slack_counter += 1; + + eminus = gtk_constraint_variable_new_slack ("em"); + gtk_constraint_expression_set_variable (expr, eminus, 1.0); + gtk_constraint_variable_unref (eminus); + + z_row = g_hash_table_lookup (self->rows, self->objective); + gtk_constraint_expression_set_variable (z_row, eminus, constraint->strength); + + gtk_constraint_solver_insert_error_variable (self, constraint, eminus); + gtk_constraint_solver_note_added_variable (self, eminus, self->objective); + gtk_constraint_variable_unref (eminus); + } + } + else + { + GtkConstraintVariable *dummy_var; + + if (gtk_constraint_ref_is_required (constraint)) + { + /* If the constraint is required, we use a dummy marker variable; + * the dummy won't be allowed to enter the basis of the tableau + * when pivoting. + */ + self->dummy_counter += 1; + + dummy_var = gtk_constraint_variable_new_dummy ("dummy"); + + if (eplus_p != NULL) + *eplus_p = dummy_var; + if (eminus_p != NULL) + *eminus_p = dummy_var; + if (prev_constant_p != NULL) + *prev_constant_p = gtk_constraint_expression_get_constant (cn_expr); + + gtk_constraint_expression_set_variable (expr, dummy_var, 1.0); + g_hash_table_insert (self->marker_vars, constraint, dummy_var); + + gtk_constraint_variable_unref (dummy_var); + } + else + { + GtkConstraintVariable *eplus, *eminus; + GtkConstraintExpression *z_row; + + /* Since the constraint is a non-required equality, we need to + * add error variables around it, i.e. turn it from: + * + * expr = 0 + * + * to: + * + * expr - eplus + eminus = 0 + */ + self->slack_counter += 1; + + eplus = gtk_constraint_variable_new_slack ("ep"); + eminus = gtk_constraint_variable_new_slack ("em"); + + gtk_constraint_expression_set_variable (expr, eplus, -1.0); + gtk_constraint_expression_set_variable (expr, eminus, 1.0); + + g_hash_table_insert (self->marker_vars, constraint, eplus); + + z_row = g_hash_table_lookup (self->rows, self->objective); + + gtk_constraint_expression_set_variable (z_row, eplus, constraint->strength); + gtk_constraint_expression_set_variable (z_row, eminus, constraint->strength); + gtk_constraint_solver_note_added_variable (self, eplus, self->objective); + gtk_constraint_solver_note_added_variable (self, eminus, self->objective); + + gtk_constraint_solver_insert_error_variable (self, constraint, eplus); + gtk_constraint_solver_insert_error_variable (self, constraint, eminus); + + if (constraint->is_stay) + { + g_ptr_array_add (self->stay_error_vars, gtk_constraint_variable_pair_new (eplus, eminus)); + } + else if (constraint->is_edit) + { + if (eplus_p != NULL) + *eplus_p = eplus; + if (eminus_p != NULL) + *eminus_p = eminus; + if (prev_constant_p != NULL) + *prev_constant_p = gtk_constraint_expression_get_constant (cn_expr); + } + + gtk_constraint_variable_unref (eplus); + gtk_constraint_variable_unref (eminus); + } + } + + if (gtk_constraint_expression_get_constant (expr) < 0.0) + gtk_constraint_expression_multiply_by (expr, -1.0); + + return expr; +} + +static void +gtk_constraint_solver_dual_optimize (GtkConstraintSolver *self) +{ + GtkConstraintExpression *z_row = g_hash_table_lookup (self->rows, self->objective); +#ifdef G_ENABLE_DEBUG + gint64 start_time = g_get_monotonic_time (); +#endif + + /* We iterate until we don't have any more infeasible rows; the pivot() + * at the end of the loop iteration may add or remove infeasible rows + * as well + */ + while (self->infeasible_rows->len != 0) + { + GtkConstraintVariable *entry_var, *exit_var, *t_v; + GtkConstraintExpressionIter eiter; + GtkConstraintExpression *expr; + double ratio, t_c; + + /* Pop the last element of the array */ + exit_var = g_ptr_array_index (self->infeasible_rows, self->infeasible_rows->len - 1); + g_ptr_array_set_size (self->infeasible_rows, self->infeasible_rows->len - 1); + + expr = g_hash_table_lookup (self->rows, exit_var); + if (expr == NULL) + continue; + + if (gtk_constraint_expression_get_constant (expr) >= 0.0) + continue; + + ratio = DBL_MAX; + entry_var = NULL; + + gtk_constraint_expression_iter_init (&eiter, expr); + while (gtk_constraint_expression_iter_next (&eiter, &t_v, &t_c)) + { + if (t_c > 0.0 && gtk_constraint_variable_is_pivotable (t_v)) + { + double zc = gtk_constraint_expression_get_coefficient (z_row, t_v); + double r = zc / t_c; + + if (r < ratio) + { + entry_var = t_v; + ratio = r; + } + } + } + + if (ratio == DBL_MAX) + g_critical ("INTERNAL: ratio == DBL_MAX in dual_optimize"); + + gtk_constraint_solver_pivot (self, entry_var, exit_var); + } + + GTK_NOTE (CONSTRAINTS, + g_message ("dual_optimize.time := %.3f ms", + (float) (g_get_monotonic_time () - start_time) / 1000.f)); +} + +static void +gtk_constraint_solver_delta_edit_constant (GtkConstraintSolver *self, + double delta, + GtkConstraintVariable *plus_error_var, + GtkConstraintVariable *minus_error_var) +{ + GtkConstraintExpression *plus_expr, *minus_expr; + GtkConstraintVariable *basic_var; + GtkConstraintVariableSet *column_set; + GtkConstraintVariableSetIter iter; + + plus_expr = g_hash_table_lookup (self->rows, plus_error_var); + if (plus_expr != NULL) + { + double new_constant = gtk_constraint_expression_get_constant (plus_expr) + delta; + + gtk_constraint_expression_set_constant (plus_expr, new_constant); + + if (new_constant < 0.0) + g_ptr_array_add (self->infeasible_rows, plus_error_var); + + return; + } + + minus_expr = g_hash_table_lookup (self->rows, minus_error_var); + if (minus_expr != NULL) + { + double new_constant = gtk_constraint_expression_get_constant (minus_expr) - delta; + + gtk_constraint_expression_set_constant (minus_expr, new_constant); + + if (new_constant < 0.0) + g_ptr_array_add (self->infeasible_rows, minus_error_var); + + return; + } + + column_set = g_hash_table_lookup (self->columns, minus_error_var); + if (column_set == NULL) + { + g_critical ("INTERNAL: Columns are unset during delta edit"); + return; + } + + gtk_constraint_variable_set_iter_init (&iter, column_set); + while (gtk_constraint_variable_set_iter_next (&iter, &basic_var)) + { + GtkConstraintExpression *expr; + double c, new_constant; + + expr = g_hash_table_lookup (self->rows, basic_var); + c = gtk_constraint_expression_get_coefficient (expr, minus_error_var); + + new_constant = gtk_constraint_expression_get_constant (expr) + (c * delta); + gtk_constraint_expression_set_constant (expr, new_constant); + + if (gtk_constraint_variable_is_restricted (basic_var) && new_constant < 0.0) + g_ptr_array_add (self->infeasible_rows, basic_var); + } +} + +static GtkConstraintVariable * +gtk_constraint_solver_choose_subject (GtkConstraintSolver *self, + GtkConstraintExpression *expression) +{ + GtkConstraintExpressionIter eiter; + GtkConstraintVariable *subject = NULL; + GtkConstraintVariable *retval = NULL; + GtkConstraintVariable *t_v; + gboolean found_unrestricted = FALSE; + gboolean found_new_restricted = FALSE; + gboolean retval_found = FALSE; + double coeff = 0.0; + double t_c; + + gtk_constraint_expression_iter_init (&eiter, expression); + while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) + { + if (found_unrestricted) + { + if (!gtk_constraint_variable_is_restricted (t_v)) + { + if (!g_hash_table_contains (self->columns, t_v)) + { + retval_found = TRUE; + retval = t_v; + break; + } + } + } + else + { + if (gtk_constraint_variable_is_restricted (t_v)) + { + if (!found_new_restricted && + !gtk_constraint_variable_is_dummy (t_v) && + t_c < 0.0) + { + GtkConstraintVariableSet *cset = g_hash_table_lookup (self->columns, t_v); + + if (cset == NULL || + (gtk_constraint_variable_set_is_singleton (cset) && + g_hash_table_contains (self->columns, self->objective))) + { + subject = t_v; + found_new_restricted = TRUE; + } + } + } + else + { + subject = t_v; + found_unrestricted = TRUE; + } + } + } + + if (retval_found) + return retval; + + if (subject != NULL) + return subject; + + gtk_constraint_expression_iter_init (&eiter, expression); + while (gtk_constraint_expression_iter_prev (&eiter, &t_v, &t_c)) + { + if (!gtk_constraint_variable_is_dummy (t_v)) + return NULL; + + if (!g_hash_table_contains (self->columns, t_v)) + { + subject = t_v; + coeff = t_c; + } + } + + if (!G_APPROX_VALUE (gtk_constraint_expression_get_constant (expression), 0.0, 0.001)) + { + GTK_NOTE (CONSTRAINTS, + g_message ("Unable to satisfy required constraint (choose_subject)")); + return NULL; + } + + if (coeff > 0) + gtk_constraint_expression_multiply_by (expression, -1.0); + + return subject; +} + +static gboolean +gtk_constraint_solver_try_adding_directly (GtkConstraintSolver *self, + GtkConstraintExpression *expression) +{ + GtkConstraintVariable *subject; + + subject = gtk_constraint_solver_choose_subject (self, expression); + if (subject == NULL) + return FALSE; + + gtk_constraint_variable_ref (subject); + + gtk_constraint_expression_new_subject (expression, subject); + if (gtk_constraint_solver_column_has_key (self, subject)) + gtk_constraint_solver_substitute_out (self, subject, expression); + + gtk_constraint_solver_add_row (self, subject, expression); + + gtk_constraint_variable_unref (subject); + + return TRUE; +} + +static void +gtk_constraint_solver_add_with_artificial_variable (GtkConstraintSolver *self, + GtkConstraintExpression *expression) +{ + GtkConstraintVariable *av, *az; + GtkConstraintExpression *az_row; + GtkConstraintExpression *az_tableau_row; + GtkConstraintExpression *e; + + av = gtk_constraint_variable_new_slack ("a"); + self->artificial_counter += 1; + + az = gtk_constraint_variable_new_objective ("az"); + + az_row = gtk_constraint_expression_clone (expression); + + gtk_constraint_solver_add_row (self, az, az_row); + gtk_constraint_solver_add_row (self, av, expression); + + gtk_constraint_expression_unref (az_row); + gtk_constraint_variable_unref (av); + gtk_constraint_variable_unref (az); + + gtk_constraint_solver_optimize (self, az); + + az_tableau_row = g_hash_table_lookup (self->rows, az); + if (!G_APPROX_VALUE (gtk_constraint_expression_get_constant (az_tableau_row), 0.0, 0.001)) + { + gtk_constraint_solver_remove_column (self, av); + gtk_constraint_solver_remove_row (self, az, TRUE); + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (CONSTRAINTS)) + { + char *str = gtk_constraint_expression_to_string (expression); + g_message ("Unable to satisfy a required constraint (add): %s", str); + g_free (str); + } +#endif + + return; + } + + e = g_hash_table_lookup (self->rows, av); + if (e != NULL) + { + GtkConstraintVariable *entry_var; + + if (gtk_constraint_expression_is_constant (e)) + { + gtk_constraint_solver_remove_row (self, av, TRUE); + gtk_constraint_solver_remove_row (self, az, TRUE); + return; + } + + entry_var = gtk_constraint_expression_get_pivotable_variable (e); + if (entry_var == NULL) + return; + + gtk_constraint_solver_pivot (self, entry_var, av); + } + + g_assert (!g_hash_table_contains (self->rows, av)); + + gtk_constraint_solver_remove_column (self, av); + gtk_constraint_solver_remove_row (self, az, TRUE); +} + +static void +gtk_constraint_solver_add_constraint_internal (GtkConstraintSolver *self, + GtkConstraintRef *constraint) +{ + GtkConstraintExpression *expr; + GtkConstraintVariable *eplus; + GtkConstraintVariable *eminus; + double prev_constant; + + expr = gtk_constraint_solver_new_expression (self, constraint, + &eplus, + &eminus, + &prev_constant); + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (CONSTRAINTS)) + { + char *expr_s = gtk_constraint_expression_to_string (expr); + char *ref_s = gtk_constraint_ref_to_string (constraint); + g_message ("Adding constraint '%s' (normalized expression: '%s')", ref_s, expr_s); + g_free (ref_s); + g_free (expr_s); + } +#endif + + if (constraint->is_stay) + { + StayInfo *si = g_new (StayInfo, 1); + + si->constraint = constraint; + + g_hash_table_insert (self->stay_var_map, constraint->variable, si); + } + else if (constraint->is_edit) + { + EditInfo *ei = g_new (EditInfo, 1); + + ei->constraint = constraint; + ei->eplus = eplus; + ei->eminus = eminus; + ei->prev_constant = prev_constant; + + g_hash_table_insert (self->edit_var_map, constraint->variable, ei); + } + + if (!gtk_constraint_solver_try_adding_directly (self, expr)) + gtk_constraint_solver_add_with_artificial_variable (self, expr); + + gtk_constraint_expression_unref (expr); + + self->needs_solving = TRUE; + + if (self->auto_solve) + { + gtk_constraint_solver_optimize (self, self->objective); + gtk_constraint_solver_set_external_variables (self); + } + + constraint->solver = self; + + g_hash_table_add (self->constraints, constraint); +} + +/*< private > + * gtk_constraint_solver_new: + * + * Creates a new #GtkConstraintSolver instance. + * + * Returns: the newly created #GtkConstraintSolver + */ +GtkConstraintSolver * +gtk_constraint_solver_new (void) +{ + return g_object_new (GTK_TYPE_CONSTRAINT_SOLVER, NULL); +} + +/*< private > + * gtk_constraint_solver_freeze: + * @solver: a #GtkConstraintSolver + * + * Freezes the solver; any constraint addition or removal will not + * be automatically solved until gtk_constraint_solver_thaw() is + * called. + */ +void +gtk_constraint_solver_freeze (GtkConstraintSolver *solver) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + + solver->freeze_count += 1; + + if (solver->freeze_count > 0) + solver->auto_solve = FALSE; +} + +/*< private > + * gtk_constraint_solver_thaw: + * @solver: a #GtkConstraintSolver + * + * Thaws a frozen #GtkConstraintSolver. + */ +void +gtk_constraint_solver_thaw (GtkConstraintSolver *solver) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + g_return_if_fail (solver->freeze_count > 0); + + solver->freeze_count -= 1; + + if (solver->freeze_count == 0) + { + solver->auto_solve = TRUE; + gtk_constraint_solver_resolve (solver); + } +} + +/*< private > + * gtk_constraint_solver_note_added_variable: + * @self: a #GtkConstraintSolver + * @variable: a #GtkConstraintVariable + * @subject: a #GtkConstraintVariable + * + * Adds a new @variable into the tableau of a #GtkConstraintSolver. + * + * This function is typically called by #GtkConstraintExpression, and + * should never be directly called. + */ +void +gtk_constraint_solver_note_added_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintVariable *subject) +{ + if (subject != NULL) + gtk_constraint_solver_insert_column_variable (self, variable, subject); +} + +/*< private > + * gtk_constraint_solver_note_removed_variable: + * @self: a #GtkConstraintSolver + * @variable: a #GtkConstraintVariable + * @subject: a #GtkConstraintVariable + * + * Removes a @variable from the tableau of a #GtkConstraintSolver. + * + * This function is typically called by #GtkConstraintExpression, and + * should never be directly called. + */ +void +gtk_constraint_solver_note_removed_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintVariable *subject) +{ + GtkConstraintVariableSet *set; + + set = g_hash_table_lookup (self->columns, variable); + if (set != NULL && subject != NULL) + gtk_constraint_variable_set_remove (set, subject); +} + +/*< private > + * gtk_constraint_solver_create_variable: + * @self: a #GtkConstraintSolver + * @prefix: (nullable): the prefix of the variable + * @name: (nullable): the name of the variable + * @value: the initial value of the variable + * + * Creates a new variable inside the @solver. + * + * Returns: (transfer full): the newly created variable + */ +GtkConstraintVariable * +gtk_constraint_solver_create_variable (GtkConstraintSolver *self, + const char *prefix, + const char *name, + double value) +{ + GtkConstraintVariable *res; + + res = gtk_constraint_variable_new (prefix, name); + gtk_constraint_variable_set_value (res, value); + + self->var_counter++; + + return res; +} + +/*< private > + * gtk_constraint_solver_resolve: + * @solver: a #GtkConstraintSolver + * + * Resolves the constraints currently stored in @solver. + */ +void +gtk_constraint_solver_resolve (GtkConstraintSolver *solver) +{ +#ifdef G_ENABLE_DEBUG + gint64 start_time = g_get_monotonic_time (); +#endif + + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + + gtk_constraint_solver_dual_optimize (solver); + gtk_constraint_solver_set_external_variables (solver); + + g_ptr_array_set_size (solver->infeasible_rows, 0); + + gtk_constraint_solver_reset_stay_constants (solver); + + GTK_NOTE (CONSTRAINTS, + g_message ("resolve.time := %.3f ms", + (float) (g_get_monotonic_time () - start_time) / 1000.f)); + + solver->needs_solving = FALSE; +} + +/*< private > + * gtk_constraint_solver_add_constraint: + * @self: a #GtkConstraintSolver + * @variable: the subject of the constraint + * @relation: the relation of the constraint + * @expression: the expression of the constraint + * @strength: the strength of the constraint + * + * Adds a new constraint in the form of: + * + * |[ + * variable relation expression (strength) + * |] + * + * into the #GtkConstraintSolver. + * + * Returns: (transfer none): a reference to the newly created + * constraint; you can use the reference to remove the + * constraint from the solver + */ +GtkConstraintRef * +gtk_constraint_solver_add_constraint (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintRelation relation, + GtkConstraintExpression *expression, + int strength) +{ + GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); + + res->solver = self; + res->strength = strength; + res->is_edit = FALSE; + res->is_stay = FALSE; + res->relation = relation; + + if (expression == NULL) + res->expression = gtk_constraint_expression_new_from_variable (variable); + else + { + res->expression = expression; + + if (variable != NULL) + { + switch (res->relation) + { + case GTK_CONSTRAINT_RELATION_EQ: + gtk_constraint_expression_add_variable (res->expression, + variable, -1.0, + NULL, + self); + break; + + case GTK_CONSTRAINT_RELATION_LE: + gtk_constraint_expression_add_variable (res->expression, + variable, -1.0, + NULL, + self); + break; + + case GTK_CONSTRAINT_RELATION_GE: + gtk_constraint_expression_multiply_by (res->expression, -1.0); + gtk_constraint_expression_add_variable (res->expression, + variable, 1.0, + NULL, + self); + break; + + default: + g_assert_not_reached (); + } + } + } + + gtk_constraint_solver_add_constraint_internal (self, res); + + return res; +} + +/*< private > + * gtk_constraint_solver_add_stay_variable: + * @self: a #GtkConstraintSolver + * @variable: a stay #GtkConstraintVariable + * @strength: the strength of the constraint + * + * Adds a constraint on a stay @variable with the given @strength. + * + * A stay variable is an "anchor" in the system: a variable that is + * supposed to stay at the same value. + * + * Returns: (transfer none): a reference to the newly created + * constraint; you can use the reference to remove the + * constraint from the solver + */ +GtkConstraintRef * +gtk_constraint_solver_add_stay_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + int strength) +{ + GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); + + res->solver = self; + res->variable = gtk_constraint_variable_ref (variable); + res->relation = GTK_CONSTRAINT_RELATION_EQ; + res->strength = strength; + res->is_stay = TRUE; + res->is_edit = FALSE; + + res->expression = gtk_constraint_expression_new (gtk_constraint_variable_get_value (res->variable)); + gtk_constraint_expression_add_variable (res->expression, + res->variable, -1.0, + NULL, + self); + +#ifdef G_ENABLE_DEBUG + if (GTK_DEBUG_CHECK (CONSTRAINTS)) + { + char *str = gtk_constraint_expression_to_string (res->expression); + g_message ("Adding stay variable: %s", str); + g_free (str); + } +#endif + + gtk_constraint_solver_add_constraint_internal (self, res); + + return res; +} + +/*< private > + * gtk_constraint_solver_remove_stay_variable: + * @self: a #GtkConstraintSolver + * @variable: a stay variable + * + * Removes the stay constraint associated to @variable. + * + * This is a convenience function for gtk_constraint_solver_remove_constraint(). + */ +void +gtk_constraint_solver_remove_stay_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable) +{ + StayInfo *si = g_hash_table_lookup (self->stay_var_map, variable); + + if (si == NULL) + { + char *str = gtk_constraint_variable_to_string (variable); + + g_critical ("Unknown stay variable '%s'", str); + + g_free (str); + + return; + } + + gtk_constraint_solver_remove_constraint (self, si->constraint); +} + +/*< private > + * gtk_constraint_solver_add_edit_variable: + * @self: a #GtkConstraintSolver + * @variable: an edit variable + * @strength: the strength of the constraint + * + * Adds an editable constraint to the @solver. + * + * Editable constraints can be used to suggest values to a + * #GtkConstraintSolver inside an edit phase, for instance: if + * you want to change the value of a variable without necessarily + * insert a new constraint every time. + * + * See also: gtk_constraint_solver_suggest_value() + * + * Returns: (transfer none): a reference to the newly added constraint + */ +GtkConstraintRef * +gtk_constraint_solver_add_edit_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + int strength) +{ + GtkConstraintRef *res = g_new0 (GtkConstraintRef, 1); + + res->solver = self; + res->variable = gtk_constraint_variable_ref (variable); + res->relation = GTK_CONSTRAINT_RELATION_EQ; + res->strength = strength; + res->is_stay = FALSE; + res->is_edit = TRUE; + + res->expression = gtk_constraint_expression_new (gtk_constraint_variable_get_value (variable)); + gtk_constraint_expression_add_variable (res->expression, + variable, -1.0, + NULL, + self); + + gtk_constraint_solver_add_constraint_internal (self, res); + + return res; +} + +/*< private > + * gtk_constraint_solver_remove_edit_variable: + * @self: a #GtkConstraintSolver + * @variable: an edit variable + * + * Removes the edit constraint associated to @variable. + * + * This is a convenience function around gtk_constraint_solver_remove_constraint(). + */ +void +gtk_constraint_solver_remove_edit_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable) +{ + EditInfo *ei = g_hash_table_lookup (self->edit_var_map, variable); + + if (ei == NULL) + { + char *str = gtk_constraint_variable_to_string (variable); + + g_critical ("Unknown edit variable '%s'", str); + + g_free (str); + + return; + } + + gtk_constraint_solver_remove_constraint (self, ei->constraint); +} + +/*< private > + * gtk_constraint_solver_remove_constraint: + * @self: a #GtkConstraintSolver + * @constraint: a constraint reference + * + * Removes a @constraint from the @solver. + */ +void +gtk_constraint_solver_remove_constraint (GtkConstraintSolver *self, + GtkConstraintRef *constraint) +{ + GtkConstraintExpression *z_row; + GtkConstraintVariable *marker; + GtkConstraintVariableSet *error_vars; + GtkConstraintVariableSetIter iter; + + if (!g_hash_table_contains (self->constraints, constraint)) + return; + + self->needs_solving = TRUE; + + gtk_constraint_solver_reset_stay_constants (self); + + z_row = g_hash_table_lookup (self->rows, self->objective); + error_vars = g_hash_table_lookup (self->error_vars, constraint); + + if (error_vars != NULL) + { + GtkConstraintVariable *v; + + gtk_constraint_variable_set_iter_init (&iter, error_vars); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); + + if (e == NULL) + { + gtk_constraint_expression_add_variable (z_row, + v, + constraint->strength, + self->objective, + self); + } + else + { + gtk_constraint_expression_add_expression (z_row, + e, + constraint->strength, + self->objective, + self); + } + } + } + + marker = g_hash_table_lookup (self->marker_vars, constraint); + if (marker == NULL) + { + g_critical ("Constraint %p not found", constraint); + return; + } + + g_hash_table_remove (self->marker_vars, constraint); + + if (g_hash_table_lookup (self->rows, marker) == NULL) + { + GtkConstraintVariableSet *set = g_hash_table_lookup (self->columns, marker); + GtkConstraintVariable *exit_var = NULL; + GtkConstraintVariable *v; + double min_ratio = 0; + + if (set == NULL) + goto no_columns; + + gtk_constraint_variable_set_iter_init (&iter, set); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + if (gtk_constraint_variable_is_restricted (v)) + { + GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); + double coeff = gtk_constraint_expression_get_coefficient (e, marker); + + if (coeff < 0.0) + { + double r = -gtk_constraint_expression_get_constant (e) / coeff; + + if (exit_var == NULL || + r < min_ratio || + G_APPROX_VALUE (r, min_ratio, 0.0001)) + { + min_ratio = r; + exit_var = v; + } + } + } + } + + if (exit_var == NULL) + { + gtk_constraint_variable_set_iter_init (&iter, set); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + if (gtk_constraint_variable_is_restricted (v)) + { + GtkConstraintExpression *e = g_hash_table_lookup (self->rows, v); + double coeff = gtk_constraint_expression_get_coefficient (e, marker); + double r = 0.0; + + if (!G_APPROX_VALUE (coeff, 0.0, 0.0001)) + r = gtk_constraint_expression_get_constant (e) / coeff; + + if (exit_var == NULL || r < min_ratio) + { + min_ratio = r; + exit_var = v; + } + } + } + } + + if (exit_var == NULL) + { + if (gtk_constraint_variable_set_is_empty (set)) + gtk_constraint_solver_remove_column (self, marker); + else + { + gtk_constraint_variable_set_iter_init (&iter, set); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + if (v != self->objective) + { + exit_var = v; + break; + } + } + } + } + + if (exit_var != NULL) + gtk_constraint_solver_pivot (self, marker, exit_var); + } + +no_columns: + if (g_hash_table_lookup (self->rows, marker) != NULL) + gtk_constraint_solver_remove_row (self, marker, TRUE); + else + gtk_constraint_variable_unref (marker); + + if (error_vars != NULL) + { + GtkConstraintVariable *v; + + gtk_constraint_variable_set_iter_init (&iter, error_vars); + while (gtk_constraint_variable_set_iter_next (&iter, &v)) + { + if (v != marker) + gtk_constraint_solver_remove_column (self, v); + } + } + + if (constraint->is_stay) + { + if (error_vars != NULL) + { + GPtrArray *remaining = + g_ptr_array_new_with_free_func ((GDestroyNotify) gtk_constraint_variable_pair_free); + + int i = 0; + + for (i = 0; i < self->stay_error_vars->len; i++) + { + GtkConstraintVariablePair *pair = g_ptr_array_index (self->stay_error_vars, i); + gboolean found = FALSE; + + if (gtk_constraint_variable_set_remove (error_vars, pair->first)) + found = TRUE; + + if (gtk_constraint_variable_set_remove (error_vars, pair->second)) + found = FALSE; + + if (!found) + g_ptr_array_add (remaining, gtk_constraint_variable_pair_new (pair->first, pair->second)); + } + + g_clear_pointer (&self->stay_error_vars, g_ptr_array_unref); + self->stay_error_vars = remaining; + } + + g_hash_table_remove (self->stay_var_map, constraint->variable); + } + else if (constraint->is_edit) + { + EditInfo *ei = g_hash_table_lookup (self->edit_var_map, constraint->variable); + + gtk_constraint_solver_remove_column (self, ei->eminus); + + g_hash_table_remove (self->edit_var_map, constraint->variable); + } + + if (error_vars != NULL) + g_hash_table_remove (self->error_vars, constraint); + + if (self->auto_solve) + { + gtk_constraint_solver_optimize (self, self->objective); + gtk_constraint_solver_set_external_variables (self); + } + + g_hash_table_remove (self->constraints, constraint); +} + +/*< private > + * gtk_constraint_solver_suggest_value: + * @self: a #GtkConstraintSolver + * @variable: a #GtkConstraintVariable + * @value: the suggested value for @variable + * + * Suggests a new @value for an edit @variable. + * + * The @variable must be an edit variable, and the solver must be + * in an edit phase. + */ +void +gtk_constraint_solver_suggest_value (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + double value) +{ + EditInfo *ei = g_hash_table_lookup (self->edit_var_map, variable); + double delta; + if (ei == NULL) + { + g_critical ("Suggesting value '%g' but variable %p is not editable", + value, variable); + return; + } + + if (!self->in_edit_phase) + { + g_critical ("Suggesting value '%g' for variable '%p' but solver is " + "not in an edit phase", + value, variable); + return; + } + + delta = value - ei->prev_constant; + ei->prev_constant = value; + + gtk_constraint_solver_delta_edit_constant (self, delta, ei->eplus, ei->eminus); +} + +/*< private > + * gtk_constraint_solver_has_stay_variable: + * @solver: a #GtkConstraintSolver + * @variable: a #GtkConstraintVariable + * + * Checks whether @variable is a stay variable. + * + * Returns: %TRUE if the variable is a stay variable + */ +gboolean +gtk_constraint_solver_has_stay_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver), FALSE); + g_return_val_if_fail (variable != NULL, FALSE); + + return g_hash_table_contains (solver->stay_var_map, variable); +} + +/*< private > + * gtk_constraint_solver_has_edit_variable: + * @solver: a #GtkConstraintSolver + * @variable: a #GtkConstraintVariable + * + * Checks whether @variable is an edit variable. + * + * Returns: %TRUE if the variable is an edit variable + */ +gboolean +gtk_constraint_solver_has_edit_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable) +{ + g_return_val_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver), FALSE); + g_return_val_if_fail (variable != NULL, FALSE); + + return g_hash_table_contains (solver->edit_var_map, variable); +} + +/*< private > + * gtk_constraint_solver_begin_edit: + * @solver: a #GtkConstraintSolver + * + * Begins the edit phase for a constraint system. + * + * Typically, you need to add new edit constraints for a variable to the + * system, using gtk_constraint_solver_add_edit_variable(); then you + * call this function and suggest values for the edit variables, using + * gtk_constraint_solver_suggest_value(). After you suggested a value + * for all the variables you need to edit, you will need to call + * gtk_constraint_solver_resolve() to solve the system, and get the value + * of the various variables that you're interested in. + * + * Once you completed the edit phase, call gtk_constraint_solver_end_edit() + * to remove all the edit variables. + */ +void +gtk_constraint_solver_begin_edit (GtkConstraintSolver *solver) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + + if (g_hash_table_size (solver->edit_var_map) == 0) + { + g_critical ("Solver %p does not have editable variables.", solver); + return; + } + + g_ptr_array_set_size (solver->infeasible_rows, 0); + gtk_constraint_solver_reset_stay_constants (solver); + + solver->in_edit_phase = TRUE; +} + +static void +edit_info_free (gpointer data) +{ + g_free (data); +} + +/*< private > + * gtk_constraint_solver_end_edit: + * @solver: a #GtkConstraintSolver + * + * Ends the edit phase for a constraint system, and clears + * all the edit variables introduced. + */ +void +gtk_constraint_solver_end_edit (GtkConstraintSolver *solver) +{ + solver->in_edit_phase = FALSE; + + gtk_constraint_solver_resolve (solver); + + g_hash_table_remove_all (solver->edit_var_map); +} + +void +gtk_constraint_solver_clear (GtkConstraintSolver *solver) +{ + g_return_if_fail (GTK_IS_CONSTRAINT_SOLVER (solver)); + + g_hash_table_remove_all (solver->constraints); + g_hash_table_remove_all (solver->external_rows); + g_hash_table_remove_all (solver->external_parametric_vars); + g_hash_table_remove_all (solver->error_vars); + g_hash_table_remove_all (solver->marker_vars); + g_hash_table_remove_all (solver->edit_var_map); + g_hash_table_remove_all (solver->stay_var_map); + + g_ptr_array_set_size (solver->infeasible_rows, 0); + g_ptr_array_set_size (solver->stay_error_vars, 0); + + g_hash_table_remove_all (solver->rows); + g_hash_table_remove_all (solver->columns); + + /* The rows table owns the objective variable */ + solver->objective = gtk_constraint_variable_new_objective ("Z"); + g_hash_table_insert (solver->rows, + solver->objective, + gtk_constraint_expression_new (0.0)); + + solver->slack_counter = 0; + solver->dummy_counter = 0; + solver->artificial_counter = 0; + solver->freeze_count = 0; + + solver->needs_solving = FALSE; + solver->auto_solve = TRUE; +} + +char * +gtk_constraint_solver_to_string (GtkConstraintSolver *solver) +{ + GString *buf = g_string_new (NULL); + + g_string_append (buf, "Tableau info:\n"); + g_string_append_printf (buf, "Rows: %d (= %d constraints)\n", + g_hash_table_size (solver->rows), + g_hash_table_size (solver->rows) - 1); + g_string_append_printf (buf, "Columns: %d\n", + g_hash_table_size (solver->columns)); + g_string_append_printf (buf, "Infeasible rows: %d\n", + solver->infeasible_rows->len); + g_string_append_printf (buf, "External basic variables: %d\n", + g_hash_table_size (solver->external_rows)); + g_string_append_printf (buf, "External parametric variables: %d\n", + g_hash_table_size (solver->external_parametric_vars)); + + g_string_append (buf, "Constraints:"); + if (g_hash_table_size (solver->constraints) == 0) + g_string_append (buf, " <empty>\n"); + else + { + GHashTableIter iter; + gpointer key_p; + + g_string_append (buf, "\n"); + + g_hash_table_iter_init (&iter, solver->constraints); + while (g_hash_table_iter_next (&iter, &key_p, NULL)) + { + char *ref = gtk_constraint_ref_to_string (key_p); + + g_string_append_printf (buf, " %s\n", ref); + + g_free (ref); + } + } + + g_string_append (buf, "Stay error vars:"); + if (solver->stay_error_vars->len == 0) + g_string_append (buf, " <empty>\n"); + else + { + g_string_append (buf, "\n"); + + for (int i = 0; i < solver->stay_error_vars->len; i++) + { + const GtkConstraintVariablePair *pair = g_ptr_array_index (solver->stay_error_vars, i); + char *first_s = gtk_constraint_variable_to_string (pair->first); + char *second_s = gtk_constraint_variable_to_string (pair->second); + + g_string_append_printf (buf, " (%s, %s)\n", first_s, second_s); + + g_free (first_s); + g_free (second_s); + } + } + + g_string_append (buf, "Edit var map:"); + if (g_hash_table_size (solver->edit_var_map) == 0) + g_string_append (buf, " <empty>\n"); + else + { + GHashTableIter iter; + gpointer key_p, value_p; + + g_string_append (buf, "\n"); + + g_hash_table_iter_init (&iter, solver->edit_var_map); + while (g_hash_table_iter_next (&iter, &key_p, &value_p)) + { + char *var = gtk_constraint_variable_to_string (key_p); + const EditInfo *ei = value_p; + char *c = gtk_constraint_ref_to_string (ei->constraint); + + g_string_append_printf (buf, " %s => %s\n", var, c); + + g_free (var); + g_free (c); + } + } + + return g_string_free (buf, FALSE); +} + +char * +gtk_constraint_solver_statistics (GtkConstraintSolver *solver) +{ + GString *buf = g_string_new (NULL); + + g_string_append_printf (buf, "Variables: %d\n", solver->var_counter); + g_string_append_printf (buf, "Slack vars: %d\n", solver->slack_counter); + g_string_append_printf (buf, "Artificial vars: %d\n", solver->artificial_counter); + g_string_append_printf (buf, "Dummy vars: %d\n", solver->dummy_counter); + g_string_append_printf (buf, "Stay vars: %d\n", g_hash_table_size (solver->stay_var_map)); + g_string_append_printf (buf, "Optimize count: %d\n", solver->optimize_count); + g_string_append_printf (buf, "Rows: %d\n", g_hash_table_size (solver->rows)); + g_string_append_printf (buf, "Columns: %d\n", g_hash_table_size (solver->columns)); + + if (g_hash_table_size (solver->columns) > 0) + { + GHashTableIter iter; + gpointer val; + double sum = 0; + + g_hash_table_iter_init (&iter, solver->columns); + while (g_hash_table_iter_next (&iter, NULL, &val)) + { + GtkConstraintVariableSet *set = val; + sum += gtk_constraint_variable_set_size (set); + } + g_string_append_printf (buf, "Avg column size: %g\n", sum / g_hash_table_size (solver->columns)); + } + + g_string_append_printf (buf, "Infeasible rows: %d\n", solver->infeasible_rows->len); + g_string_append_printf (buf, "External basic variables: %d\n", g_hash_table_size (solver->external_rows)); + g_string_append_printf (buf, "External parametric variables: %d\n", g_hash_table_size (solver->external_parametric_vars)); + + return g_string_free (buf, FALSE); +} diff --git a/gtk/gtkconstraintsolverprivate.h b/gtk/gtkconstraintsolverprivate.h new file mode 100644 index 0000000000..cb7f74e681 --- /dev/null +++ b/gtk/gtkconstraintsolverprivate.h @@ -0,0 +1,117 @@ +/* gtkconstraintsolverprivate.h: Constraint solver based on the Cassowary method + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#pragma once + +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +#define GTK_TYPE_CONSTRAINT_SOLVER (gtk_constraint_solver_get_type()) + +G_DECLARE_FINAL_TYPE (GtkConstraintSolver, gtk_constraint_solver, GTK, CONSTRAINT_SOLVER, GObject) + +GtkConstraintSolver * +gtk_constraint_solver_new (void); + +void +gtk_constraint_solver_freeze (GtkConstraintSolver *solver); + +void +gtk_constraint_solver_thaw (GtkConstraintSolver *solver); + +void +gtk_constraint_solver_resolve (GtkConstraintSolver *solver); + +GtkConstraintVariable * +gtk_constraint_solver_create_variable (GtkConstraintSolver *solver, + const char *prefix, + const char *name, + double value); + +GtkConstraintRef * +gtk_constraint_solver_add_constraint (GtkConstraintSolver *solver, + GtkConstraintVariable *variable, + GtkConstraintRelation relation, + GtkConstraintExpression *expression, + int strength); + +void +gtk_constraint_solver_remove_constraint (GtkConstraintSolver *solver, + GtkConstraintRef *reference); + +GtkConstraintRef * +gtk_constraint_solver_add_stay_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable, + int strength); + +void +gtk_constraint_solver_remove_stay_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable); + +gboolean +gtk_constraint_solver_has_stay_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable); + +GtkConstraintRef * +gtk_constraint_solver_add_edit_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable, + int strength); + +void +gtk_constraint_solver_remove_edit_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable); + +gboolean +gtk_constraint_solver_has_edit_variable (GtkConstraintSolver *solver, + GtkConstraintVariable *variable); + +void +gtk_constraint_solver_suggest_value (GtkConstraintSolver *solver, + GtkConstraintVariable *variable, + double value); + +void +gtk_constraint_solver_begin_edit (GtkConstraintSolver *solver); + +void +gtk_constraint_solver_end_edit (GtkConstraintSolver *solver); + +void +gtk_constraint_solver_note_added_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintVariable *subject); + +void +gtk_constraint_solver_note_removed_variable (GtkConstraintSolver *self, + GtkConstraintVariable *variable, + GtkConstraintVariable *subject); + +void +gtk_constraint_solver_clear (GtkConstraintSolver *solver); + +char * +gtk_constraint_solver_to_string (GtkConstraintSolver *solver); + +char * +gtk_constraint_solver_statistics (GtkConstraintSolver *solver); + +G_END_DECLS diff --git a/gtk/gtkconstrainttypesprivate.h b/gtk/gtkconstrainttypesprivate.h new file mode 100644 index 0000000000..052280ae98 --- /dev/null +++ b/gtk/gtkconstrainttypesprivate.h @@ -0,0 +1,50 @@ +/* gtkconstrainttypesprivate.h: Private types for the constraint solver + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Emmanuele Bassi + */ + +#pragma once + +#include <gtk/gtktypes.h> +#include <gtk/gtkenums.h> + +G_BEGIN_DECLS + +typedef struct _GtkConstraintVariable GtkConstraintVariable; +typedef struct _GtkConstraintExpression GtkConstraintExpression; +typedef struct _GtkConstraintExpressionBuilder GtkConstraintExpressionBuilder; + +/*< private > + * GtkConstraintRef: + * + * A reference to a constraint stored inside the solver; while #GtkConstraint + * represent the public API, a #GtkConstraintRef represents data stored inside + * the solver. A #GtkConstraintRef is completely opaque, and should only be + * used to remove a constraint from the solver. + */ +typedef struct _GtkConstraintRef GtkConstraintRef; + +/*< private > + * GtkConstraintSolver: + * + * A simplex solver using the Cassowary constraint solving algorithm. + */ +typedef struct _GtkConstraintSolver GtkConstraintSolver; + +G_END_DECLS diff --git a/gtk/gtkconstraintvflparser.c b/gtk/gtkconstraintvflparser.c new file mode 100644 index 0000000000..79be0b0037 --- /dev/null +++ b/gtk/gtkconstraintvflparser.c @@ -0,0 +1,1227 @@ +/* gtkconstraintvflparser.c: VFL constraint definition parser + * + * Copyright 2017 Endless + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "gtkconstraintvflparserprivate.h" +#include "gtkenums.h" + +#include <string.h> + +typedef enum { + VFL_HORIZONTAL, + VFL_VERTICAL +} VflOrientation; + +typedef struct { + GtkConstraintRelation relation; + + double constant; + double multiplier; + const char *subject; + char *object; + const char *attr; + + double priority; +} VflPredicate; + +typedef enum { + SPACING_SET = 1 << 0, + SPACING_DEFAULT = 1 << 1, + SPACING_PREDICATE = 1 << 2 +} VflSpacingFlags; + +typedef struct { + double size; + VflSpacingFlags flags; + VflPredicate predicate; +} VflSpacing; + +typedef struct _VflView VflView; + +struct _VflView +{ + char *name; + + /* Decides which attributes are admissible */ + VflOrientation orientation; + + /* A set of predicates, which will be used to + * set up constraints + */ + GArray *predicates; + + VflSpacing spacing; + + VflView *prev_view; + VflView *next_view; +}; + +struct _GtkConstraintVflParser +{ + char *buffer; + gsize buffer_len; + + int error_offset; + int error_range; + + int default_spacing[2]; + + /* Set<name, double> */ + GHashTable *metrics_set; + /* Set<name, widget> */ + GHashTable *views_set; + + const char *cursor; + + /* Decides which attributes are admissible */ + VflOrientation orientation; + + VflView *leading_super; + VflView *trailing_super; + + VflView *current_view; + VflView *views; +}; + +/* NOTE: These two symbols are defined in gtkconstraintlayout.h, but we + * cannot include that header here + */ +#define GTK_CONSTRAINT_VFL_PARSER_ERROR (gtk_constraint_vfl_parser_error_quark ()) +GQuark gtk_constraint_vfl_parser_error_quark (void); + +GtkConstraintVflParser * +gtk_constraint_vfl_parser_new (void) +{ + GtkConstraintVflParser *res = g_new0 (GtkConstraintVflParser, 1); + + res->default_spacing[VFL_HORIZONTAL] = 8; + res->default_spacing[VFL_VERTICAL] = 8; + + res->orientation = VFL_HORIZONTAL; + + return res; +} + +void +gtk_constraint_vfl_parser_set_default_spacing (GtkConstraintVflParser *parser, + int hspacing, + int vspacing) +{ + parser->default_spacing[VFL_HORIZONTAL] = hspacing < 0 ? 8 : hspacing; + parser->default_spacing[VFL_VERTICAL] = vspacing < 0 ? 8 : vspacing; +} + +void +gtk_constraint_vfl_parser_set_metrics (GtkConstraintVflParser *parser, + GHashTable *metrics) +{ + parser->metrics_set = metrics; +} + +void +gtk_constraint_vfl_parser_set_views (GtkConstraintVflParser *parser, + GHashTable *views) +{ + parser->views_set = views; +} + +static int +get_default_spacing (GtkConstraintVflParser *parser) +{ + return parser->default_spacing[parser->orientation]; +} + +/* Default attributes, if unnamed, depending on the orientation */ +static const char *default_attribute[2] = { + [VFL_HORIZONTAL] = "width", + [VFL_VERTICAL] = "height", +}; + +static gboolean +parse_relation (const char *str, + GtkConstraintRelation *relation, + char **endptr, + GError **error) +{ + const char *cur = str; + + if (*cur == '=') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_EQ; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + else if (*cur == '>') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_GE; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + else if (*cur == '<') + { + cur += 1; + + if (*cur == '=') + { + *relation = GTK_CONSTRAINT_RELATION_LE; + *endptr = (char *) cur + 1; + return TRUE; + } + + goto out; + } + +out: + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_RELATION, + "Unknown relation; must be one of '==', '>=', or '<='"); + return FALSE; +} + +static gboolean +has_metric (GtkConstraintVflParser *parser, + const char *name) +{ + if (parser->metrics_set == NULL) + return FALSE; + + return g_hash_table_contains (parser->metrics_set, name); +} + +static gboolean +has_view (GtkConstraintVflParser *parser, + const char *name) +{ + if (parser->views_set == NULL) + return FALSE; + + if (!g_hash_table_contains (parser->views_set, name)) + return FALSE; + + return g_hash_table_lookup (parser->views_set, name) != NULL; +} + +/* Valid attributes */ +static const struct { + int len; + const char *name; +} valid_attributes[] = { + { 5, "width" }, + { 6, "height" }, + { 7, "centerX" }, + { 7, "centerY" }, + { 3, "top" }, + { 6, "bottom" }, + { 4, "left" }, + { 5, "right" }, + { 5, "start" }, + { 3, "end" }, + { 8, "baseline" } +}; + +static char * +get_offset_to (const char *str, + const char *tokens) +{ + char *offset = NULL; + int n_tokens = strlen (tokens); + + for (int i = 0; i < n_tokens; i++) + { + if ((offset = strchr (str, tokens[i])) != NULL) + break; + } + + return offset; +} + +static gboolean +parse_predicate (GtkConstraintVflParser *parser, + const char *cursor, + VflPredicate *predicate, + char **endptr, + GError **error) +{ + VflOrientation orientation = parser->orientation; + const char *end = cursor; + + predicate->object = NULL; + predicate->multiplier = 1.0; + + /* <predicate> = (<relation>)? (<objectOfPredicate>) ('.'<attribute>)? (<operator>)? ('@'<priority>)? + * <relation> = '==' | '<=' | '>=' + * <objectOfPredicate> = <constant> | <viewName> + * <constant> = <number> | <metricName> + * <viewName> = [A-Za-z_]([A-Za-z0-9_]*) + * <metricName> = [A-Za-z_]([A-Za-z0-9_]*) + * <operator> = (['*'|'/']<positiveNumber>)? (['+'|'-']<positiveNumber>)? + * <priority> = <positiveNumber> | 'weak' | 'medium' | 'strong' | 'required' + */ + + /* Parse relation */ + if (*end == '=' || *end == '>' || *end == '<') + { + GtkConstraintRelation relation; + char *tmp; + + if (!parse_relation (end, &relation, &tmp, error)) + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + return FALSE; + } + + predicate->relation = relation; + + end = tmp; + } + else + predicate->relation = GTK_CONSTRAINT_RELATION_EQ; + + /* Parse object of predicate */ + if (g_ascii_isdigit (*end)) + { + char *tmp; + + /* <constant> */ + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = g_ascii_strtod (end, &tmp); + + end = tmp; + } + else if (g_ascii_isalpha (*end) || *end == '_') + { + const char *name_start = end; + + while (g_ascii_isalnum (*end) || *end == '_') + end += 1; + + char *name = g_strndup (name_start, end - name_start); + + /* We only accept view names if the subject of the predicate + * is a view, i.e. we do not allow view names inside a spacing + * predicate + */ + if (predicate->subject == NULL) + { + if (parser->metrics_set == NULL || !has_metric (parser, name)) + { + parser->error_offset = name_start - parser->cursor; + parser->error_range = end - name_start; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_METRIC, + "Unable to find metric with name '%s'", name); + g_free (name); + return FALSE; + } + + double *val = g_hash_table_lookup (parser->metrics_set, name); + + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = *val; + + g_free (name); + + goto parse_operators; + } + + if (has_metric (parser, name)) + { + double *val = g_hash_table_lookup (parser->metrics_set, name); + + predicate->object = NULL; + predicate->attr = default_attribute[orientation]; + predicate->constant = *val; + + g_free (name); + + goto parse_operators; + } + + if (has_view (parser, name)) + { + /* Transfer name's ownership to the predicate */ + predicate->object = name; + predicate->attr = default_attribute[orientation]; + predicate->constant = 0; + + goto parse_attribute; + } + + parser->error_offset = name_start - parser->cursor; + parser->error_range = end - name_start; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "Unable to find view with name '%s'", name); + g_free (name); + return FALSE; + } + else + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected constant, view name, or metric"); + return FALSE; + } + +parse_attribute: + if (*end == '.') + { + end += 1; + predicate->attr = NULL; + + for (int i = 0; i < G_N_ELEMENTS (valid_attributes); i++) + { + if (g_ascii_strncasecmp (valid_attributes[i].name, end, valid_attributes[i].len) == 0) + { + predicate->attr = valid_attributes[i].name; + end += valid_attributes[i].len; + } + } + + if (predicate->attr == NULL) + { + char *range_end = get_offset_to (end, "*/+-@,)]"); + + if (range_end != NULL) + parser->error_range = range_end - end - 1; + else + parser->error_range = 0; + + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_ATTRIBUTE, + "Attribute must be on one of 'width', 'height', " + "'centerX', 'centerY', 'top', 'bottom', " + "'left', 'right', 'start', 'end', 'baseline'"); + return FALSE; + } + } + +parse_operators: + /* Parse multiplier operator */ + while (g_ascii_isspace (*end)) + end += 1; + + if ((*end == '*') || (*end == '/')) + { + double multiplier; + const char *operator; + + operator = end; + end += 1; + + while (g_ascii_isspace (*end)) + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + multiplier = g_ascii_strtod (end, &tmp); + end = tmp; + } + else + { + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected a positive number as a multiplier"); + return FALSE; + } + + if (predicate->object != NULL) + { + if (*operator == '*') + predicate->multiplier = multiplier; + else + predicate->multiplier = 1.0 / multiplier; + } + else + { + /* If the subject is a constant then apply multiplier directly */ + if (*operator == '*') + predicate->constant *= multiplier; + else + predicate->constant *= 1.0 / multiplier; + } + } + + /* Parse constant operator */ + while (g_ascii_isspace (*end)) + end += 1; + + if ((*end == '+') || (*end == '-')) + { + double constant; + const char *operator; + + operator = end; + end += 1; + + while (g_ascii_isspace (*end)) + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + constant = g_ascii_strtod (end, &tmp); + end = tmp; + } + else + { + g_free (predicate->object); + + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected positive number as a constant"); + return FALSE; + } + + if (*operator == '+') + predicate->constant += constant; + else + predicate->constant += -1.0 * constant; + } + + /* Parse priority */ + if (*end == '@') + { + double priority; + end += 1; + + if (g_ascii_isdigit (*end)) + { + char *tmp; + + priority = g_ascii_strtod (end, &tmp); + end = tmp; + } + else if (strncmp (end, "weak", 4) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_WEAK; + end += 4; + } + else if (strncmp (end, "medium", 6) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_MEDIUM; + end += 6; + } + else if (strncmp (end, "strong", 6) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_STRONG; + end += 6; + } + else if (strncmp (end, "required", 8) == 0) + { + priority = GTK_CONSTRAINT_STRENGTH_REQUIRED; + end += 8; + } + else + { + char *range_end = get_offset_to (end, ",)]"); + + g_free (predicate->object); + + if (range_end != NULL) + parser->error_range = range_end - end - 1; + else + parser->error_range = 0; + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_PRIORITY, + "Priority must be a positive number or one of " + "'weak', 'medium', 'strong', and 'required'"); + return FALSE; + } + + predicate->priority = priority; + } + else + predicate->priority = GTK_CONSTRAINT_STRENGTH_REQUIRED; + + if (endptr != NULL) + *endptr = (char *) end; + + return TRUE; +} + +static gboolean +parse_view (GtkConstraintVflParser *parser, + const char *cursor, + VflView *view, + char **endptr, + GError **error) +{ + const char *end = cursor; + + /* <view> = '[' <viewName> (<predicateListWithParens>)? ']' + * <viewName> = [A-Za-z_]([A-Za-z0-9_]+) + */ + + g_assert (*end == '['); + + /* Skip '[' */ + end += 1; + + if (!(g_ascii_isalpha (*end) || *end == '_')) + { + parser->error_offset = end - parser->cursor; + parser->error_range = 0; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "View identifiers must be valid C identifiers"); + return FALSE; + } + + while (g_ascii_isalnum (*end)) + end += 1; + + if (*end == '\0') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A view must end with ']'"); + return FALSE; + } + + char *name = g_strndup (cursor + 1, end - cursor - 1); + if (!has_view (parser, name)) + { + parser->error_offset = (cursor + 1) - parser->cursor; + parser->error_range = end - cursor - 1; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + "Unable to find view with name '%s'", name); + g_free (name); + return FALSE; + } + + view->name = name; + view->predicates = g_array_new (FALSE, FALSE, sizeof (VflPredicate)); + + if (*end == ']') + { + if (endptr != NULL) + *endptr = (char *) end + 1; + + return TRUE; + } + + /* <predicateListWithParens> = '(' <predicate> (',' <predicate>)* ')' */ + if (*end != '(') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A predicate must follow a view name"); + return FALSE; + } + + end += 1; + + while (*end != '\0') + { + VflPredicate cur_predicate; + char *tmp; + + if (*end == ']' || *end == '\0') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "A predicate on a view must end with ')'"); + return FALSE; + } + + memset (&cur_predicate, 0, sizeof (VflPredicate)); + + cur_predicate.subject = view->name; + if (!parse_predicate (parser, end, &cur_predicate, &tmp, error)) + return FALSE; + + end = tmp; + +#ifdef G_ENABLE_DEBUG + g_debug ("*** Found predicate: %s.%s %s %g %s (%g)\n", + cur_predicate.object != NULL ? cur_predicate.object : view->name, + cur_predicate.attr, + cur_predicate.relation == GTK_CONSTRAINT_RELATION_EQ ? "==" : + cur_predicate.relation == GTK_CONSTRAINT_RELATION_LE ? "<=" : + cur_predicate.relation == GTK_CONSTRAINT_RELATION_GE ? ">=" : + "unknown relation", + cur_predicate.constant, + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_WEAK ? "weak" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_MEDIUM ? "medium" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_STRONG ? "strong" : + cur_predicate.priority == GTK_CONSTRAINT_STRENGTH_REQUIRED ? "required" : + "explicit strength", + cur_predicate.priority); +#endif + + g_array_append_val (view->predicates, cur_predicate); + + /* If the predicate is a list, iterate again */ + if (*end == ',') + { + end += 1; + continue; + } + + /* We reached the end of the predicate */ + if (*end == ')') + { + end += 1; + break; + } + + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ')' at the end of a predicate, not '%c'", *end); + return FALSE; + } + + if (*end != ']') + { + parser->error_offset = end - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ']' at the end of a view, not '%c'", *end); + return FALSE; + } + + if (endptr != NULL) + *endptr = (char *) end + 1; + + return TRUE; +} + +static void +vfl_view_free (VflView *view) +{ + if (view == NULL) + return; + + g_free (view->name); + + if (view->predicates != NULL) + { + for (int i = 0; i < view->predicates->len; i++) + { + VflPredicate *p = &g_array_index (view->predicates, VflPredicate, i); + + g_free (p->object); + } + + g_array_free (view->predicates, TRUE); + view->predicates = NULL; + } + + view->prev_view = NULL; + view->next_view = NULL; + + g_slice_free (VflView, view); +} + +static void +gtk_constraint_vfl_parser_clear (GtkConstraintVflParser *parser) +{ + parser->error_offset = 0; + parser->error_range = 0; + + VflView *iter = parser->views; + while (iter != NULL) + { + VflView *next = iter->next_view; + + vfl_view_free (iter); + + iter = next; + } + + parser->views = NULL; + parser->current_view = NULL; + parser->leading_super = NULL; + parser->trailing_super = NULL; + + parser->cursor = NULL; + + g_free (parser->buffer); + parser->buffer_len = 0; +} + +void +gtk_constraint_vfl_parser_free (GtkConstraintVflParser *parser) +{ + if (parser == NULL) + return; + + gtk_constraint_vfl_parser_clear (parser); + + g_free (parser); +} + +gboolean +gtk_constraint_vfl_parser_parse_line (GtkConstraintVflParser *parser, + const char *buffer, + gssize len, + GError **error) +{ + gtk_constraint_vfl_parser_clear (parser); + + if (len > 0) + { + parser->buffer = g_strndup (buffer, len); + parser->buffer_len = len; + } + else + { + parser->buffer = g_strdup (buffer); + parser->buffer_len = strlen (buffer); + } + + parser->cursor = parser->buffer; + + const char *cur = parser->cursor; + + /* Skip leading whitespace */ + while (g_ascii_isspace (*cur)) + cur += 1; + + /* Check orientation; if none is specified, then we assume horizontal */ + parser->orientation = VFL_HORIZONTAL; + if (*cur == 'H') + { + cur += 1; + + if (*cur != ':') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ':' after horizontal orientation"); + return FALSE; + } + + parser->orientation = VFL_HORIZONTAL; + cur += 1; + } + else if (*cur == 'V') + { + cur += 1; + + if (*cur != ':') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ':' after vertical orientation"); + return FALSE; + } + + parser->orientation = VFL_VERTICAL; + cur += 1; + } + + while (*cur != '\0') + { + /* Super-view */ + if (*cur == '|') + { + if (parser->views == NULL && parser->leading_super == NULL) + { + parser->leading_super = g_slice_new0 (VflView); + + parser->leading_super->name = g_strdup ("super"); + parser->leading_super->orientation = parser->orientation; + + parser->current_view = parser->leading_super; + parser->views = parser->leading_super; + } + else if (parser->trailing_super == NULL) + { + parser->trailing_super = g_slice_new0 (VflView); + + parser->trailing_super->name = g_strdup ("super"); + parser->trailing_super->orientation = parser->orientation; + + parser->current_view->next_view = parser->trailing_super; + parser->trailing_super->prev_view = parser->current_view; + + parser->current_view = parser->trailing_super; + + /* If we reached the second '|' then we completed a line + * of layout, and we can stop + */ + break; + } + else + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Super views can only appear at the beginning " + "and end of the layout, and not multiple times"); + return FALSE; + } + + cur += 1; + + continue; + } + + /* Spacing */ + if (*cur == '-') + { + if (*(cur + 1) == '\0') + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Unterminated spacing"); + return FALSE; + } + + if (parser->current_view == NULL) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing cannot be set without a view"); + return FALSE; + } + + if (*(cur + 1) == '|' || *(cur + 1) == '[') + { + VflSpacing *spacing = &(parser->current_view->spacing); + + /* Default spacer */ + spacing->flags = SPACING_SET | SPACING_DEFAULT; + spacing->size = 0; + + cur += 1; + + continue; + } + else if (*(cur + 1) == '(') + { + VflPredicate *predicate; + VflSpacing *spacing; + char *tmp; + + /* Predicate */ + cur += 1; + + spacing = &(parser->current_view->spacing); + spacing->flags = SPACING_SET | SPACING_PREDICATE; + spacing->size = 0; + + /* Spacing predicates have no subject */ + predicate = &(spacing->predicate); + predicate->subject = NULL; + + cur += 1; + if (!parse_predicate (parser, cur, predicate, &tmp, error)) + return FALSE; + + if (*tmp != ')') + { + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Expected ')' at the end of a predicate, not '%c'", *tmp); + return FALSE; + } + + cur = tmp + 1; + if (*cur != '-') + { + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Explicit spacing must be followed by '-'"); + return FALSE; + } + + cur += 1; + + continue; + } + else if (g_ascii_isdigit (*(cur + 1))) + { + VflSpacing *spacing; + char *tmp; + + /* Explicit spacing */ + cur += 1; + + spacing = &(parser->current_view->spacing); + spacing->flags = SPACING_SET; + spacing->size = g_ascii_strtod (cur, &tmp); + + if (tmp == cur) + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing must be a number"); + return FALSE; + } + + if (*tmp != '-') + { + parser->error_offset = cur - parser->cursor; + parser->error_range = tmp - cur; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Explicit spacing must be followed by '-'"); + return FALSE; + } + + cur = tmp + 1; + + continue; + } + else + { + parser->error_offset = cur - parser->cursor; + g_set_error (error, GTK_CONSTRAINT_VFL_PARSER_ERROR, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + "Spacing can either be '-' or a number"); + return FALSE; + } + } + + if (*cur == '[') + { + VflView *view = g_slice_new0 (VflView); + char *tmp; + + view->orientation = parser->orientation; + + if (!parse_view (parser, cur, view, &tmp, error)) + { + vfl_view_free (view); + return FALSE; + } + + cur = tmp; + + if (parser->views == NULL) + parser->views = view; + + view->prev_view = parser->current_view; + + if (parser->current_view != NULL) + parser->current_view->next_view = view; + + parser->current_view = view; + + continue; + } + + cur += 1; + } + + return TRUE; +} + +GtkConstraintVfl * +gtk_constraint_vfl_parser_get_constraints (GtkConstraintVflParser *parser, + int *n_constraints) +{ + GArray *constraints; + VflView *iter; + + constraints = g_array_new (FALSE, FALSE, sizeof (GtkConstraintVfl)); + + iter = parser->views; + while (iter != NULL) + { + GtkConstraintVfl c; + + if (iter->predicates != NULL) + { + for (int i = 0; i < iter->predicates->len; i++) + { + const VflPredicate *p = &g_array_index (iter->predicates, VflPredicate, i); + + c.view1 = iter->name; + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "width" : "height"; + if (p->object != NULL) + { + c.view2 = p->object; + c.attr2 = p->attr; + } + else + { + c.view2 = NULL; + c.attr2 = NULL; + } + c.relation = p->relation; + c.constant = p->constant; + c.multiplier = p->multiplier; + c.strength = p->priority; + + g_array_append_val (constraints, c); + } + } + + if ((iter->spacing.flags & SPACING_SET) != 0) + { + c.view1 = iter->name; + + /* If this is the first view, we need to anchor the leading edge */ + if (iter == parser->leading_super) + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + else + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + + c.view2 = iter->next_view != NULL ? iter->next_view->name : "super"; + + if (iter == parser->trailing_super || iter->next_view == parser->trailing_super) + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + else + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + + if ((iter->spacing.flags & SPACING_PREDICATE) != 0) + { + const VflPredicate *p = &(iter->spacing.predicate); + + c.constant = p->constant * -1.0; + c.relation = p->relation; + c.strength = p->priority; + } + else if ((iter->spacing.flags & SPACING_DEFAULT) != 0) + { + c.constant = get_default_spacing (parser) * -1.0; + c.relation = GTK_CONSTRAINT_RELATION_EQ; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + } + else + { + c.constant = iter->spacing.size * -1.0; + c.relation = GTK_CONSTRAINT_RELATION_EQ; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + } + + c.multiplier = 1.0; + + g_array_append_val (constraints, c); + } + else if (iter->next_view != NULL) + { + c.view1 = iter->name; + + /* If this is the first view, we need to anchor the leading edge */ + if (iter == parser->leading_super) + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + else + c.attr1 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + + c.relation = GTK_CONSTRAINT_RELATION_EQ; + + c.view2 = iter->next_view->name; + + if (iter->next_view == parser->trailing_super) + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "end" : "bottom"; + else + c.attr2 = iter->orientation == VFL_HORIZONTAL ? "start" : "top"; + + c.constant = 0.0; + c.multiplier = 1.0; + c.strength = GTK_CONSTRAINT_STRENGTH_REQUIRED; + + g_array_append_val (constraints, c); + } + + iter = iter->next_view; + } + + if (n_constraints != NULL) + *n_constraints = constraints->len; + +#ifdef G_ENABLE_DEBUG + for (int i = 0; i < constraints->len; i++) + { + const GtkConstraintVfl *c = &g_array_index (constraints, GtkConstraintVfl, i); + + g_debug ("{\n" + " .view1: '%s',\n" + " .attr1: '%s',\n" + " .relation: '%d',\n" + " .view2: '%s',\n" + " .attr2: '%s',\n" + " .constant: %g,\n" + " .multiplier: %g,\n" + " .strength: %g\n" + "}\n", + c->view1, c->attr1, + c->relation, + c->view2 != NULL ? c->view2 : "none", c->attr2 != NULL ? c->attr2 : "none", + c->constant, + c->multiplier, + c->strength); + } +#endif + + return (GtkConstraintVfl *) g_array_free (constraints, FALSE); +} + +int +gtk_constraint_vfl_parser_get_error_offset (GtkConstraintVflParser *parser) +{ + return parser->error_offset; +} + +int +gtk_constraint_vfl_parser_get_error_range (GtkConstraintVflParser *parser) +{ + return parser->error_range; +} diff --git a/gtk/gtkconstraintvflparserprivate.h b/gtk/gtkconstraintvflparserprivate.h new file mode 100644 index 0000000000..dd214f5049 --- /dev/null +++ b/gtk/gtkconstraintvflparserprivate.h @@ -0,0 +1,76 @@ +/* gtkconstraintvflparserprivate.h: VFL constraint definition parser + * + * Copyright 2017 Endless + * Copyright 2019 GNOME Foundation + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "gtkconstrainttypesprivate.h" + +G_BEGIN_DECLS + +typedef struct _GtkConstraintVflParser GtkConstraintVflParser; + +typedef struct { + const char *view1; + const char *attr1; + GtkConstraintRelation relation; + const char *view2; + const char *attr2; + double constant; + double multiplier; + double strength; +} GtkConstraintVfl; + +GtkConstraintVflParser * +gtk_constraint_vfl_parser_new (void); + +void +gtk_constraint_vfl_parser_free (GtkConstraintVflParser *parser); + +void +gtk_constraint_vfl_parser_set_default_spacing (GtkConstraintVflParser *parser, + int hspacing, + int vspacing); + +void +gtk_constraint_vfl_parser_set_metrics (GtkConstraintVflParser *parser, + GHashTable *metrics); + +void +gtk_constraint_vfl_parser_set_views (GtkConstraintVflParser *parser, + GHashTable *views); + +gboolean +gtk_constraint_vfl_parser_parse_line (GtkConstraintVflParser *parser, + const char *line, + gssize len, + GError **error); + +int +gtk_constraint_vfl_parser_get_error_offset (GtkConstraintVflParser *parser); + +int +gtk_constraint_vfl_parser_get_error_range (GtkConstraintVflParser *parser); + +GtkConstraintVfl * +gtk_constraint_vfl_parser_get_constraints (GtkConstraintVflParser *parser, + int *n_constraints); + +G_END_DECLS diff --git a/gtk/gtkdebug.h b/gtk/gtkdebug.h index 87affc189a..936a990639 100644 --- a/gtk/gtkdebug.h +++ b/gtk/gtkdebug.h @@ -51,7 +51,8 @@ typedef enum { GTK_DEBUG_ACTIONS = 1 << 13, GTK_DEBUG_RESIZE = 1 << 14, GTK_DEBUG_LAYOUT = 1 << 15, - GTK_DEBUG_SNAPSHOT = 1 << 16 + GTK_DEBUG_SNAPSHOT = 1 << 16, + GTK_DEBUG_CONSTRAINTS = 1 << 17, } GtkDebugFlag; #ifdef G_ENABLE_DEBUG diff --git a/gtk/gtkenums.h b/gtk/gtkenums.h index 6d1bf81295..9bd6f1632e 100644 --- a/gtk/gtkenums.h +++ b/gtk/gtkenums.h @@ -1053,4 +1053,98 @@ typedef enum { GTK_PICK_NON_TARGETABLE = 1 << 1 } GtkPickFlags; +/** + * GtkConstraintRelation: + * @GTK_CONSTRAINT_RELATION_EQ: Equal + * @GTK_CONSTRAINT_RELATION_LE: Less than, or equal + * @GTK_CONSTRAINT_RELATION_GE: Greater than, or equal + * + * The relation between two terms of a constraint. + */ +typedef enum { + GTK_CONSTRAINT_RELATION_LE = -1, + GTK_CONSTRAINT_RELATION_EQ = 0, + GTK_CONSTRAINT_RELATION_GE = 1 +} GtkConstraintRelation; + +/** + * GtkConstraintStrength: + * @GTK_CONSTRAINT_STRENGTH_REQUIRED: The constraint is required towards solving the layout + * @GTK_CONSTRAINT_STRENGTH_STRONG: A strong constraint + * @GTK_CONSTRAINT_STRENGTH_MEDIUM: A medium constraint + * @GTK_CONSTRAINT_STRENGTH_WEAK: A weak constraint + * + * The strength of a constraint, expressed as a symbolic constant. + * + * The strength of a #GtkConstraint can be expressed with any positive + * integer; the values of this enumeration can be used for readability. + */ +typedef enum { + GTK_CONSTRAINT_STRENGTH_REQUIRED = 1001001000, + GTK_CONSTRAINT_STRENGTH_STRONG = 1000000000, + GTK_CONSTRAINT_STRENGTH_MEDIUM = 1000, + GTK_CONSTRAINT_STRENGTH_WEAK = 1 +} GtkConstraintStrength; + +/** + * GtkConstraintAttribute: + * @GTK_CONSTRAINT_ATTRIBUTE_NONE: No attribute, used for constant + * relations + * @GTK_CONSTRAINT_ATTRIBUTE_LEFT: The left edge of a widget, regardless of + * text direction + * @GTK_CONSTRAINT_ATTRIBUTE_RIGHT: The right edge of a widget, regardless + * of text direction + * @GTK_CONSTRAINT_ATTRIBUTE_TOP: The top edge of a widget + * @GTK_CONSTRAINT_ATTRIBUTE_BOTTOM: The bottom edge of a widget + * @GTK_CONSTRAINT_ATTRIBUTE_START: The leading edge of a widget, depending + * on text direction; equivalent to %GTK_CONSTRAINT_ATTRIBUTE_LEFT for LTR + * languages, and %GTK_CONSTRAINT_ATTRIBUTE_RIGHT for RTL ones + * @GTK_CONSTRAINT_ATTRIBUTE_END: The trailing edge of a widget, depending + * on text direction; equivalent to %GTK_CONSTRAINT_ATTRIBUTE_RIGHT for LTR + * languages, and %GTK_CONSTRAINT_ATTRIBUTE_LEFT for RTL ones + * @GTK_CONSTRAINT_ATTRIBUTE_WIDTH: The width of a widget + * @GTK_CONSTRAINT_ATTRIBUTE_HEIGHT: The height of a widget + * @GTK_CONSTRAINT_ATTRIBUTE_CENTER_X: The center of a widget, on the + * horizontal axis + * @GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y: The center of a widget, on the + * vertical axis + * @GTK_CONSTRAINT_ATTRIBUTE_BASELINE: The baseline of a widget + * + * The widget attributes that can be used when creating a #GtkConstraint. + */ +typedef enum { + GTK_CONSTRAINT_ATTRIBUTE_NONE, + GTK_CONSTRAINT_ATTRIBUTE_LEFT, + GTK_CONSTRAINT_ATTRIBUTE_RIGHT, + GTK_CONSTRAINT_ATTRIBUTE_TOP, + GTK_CONSTRAINT_ATTRIBUTE_BOTTOM, + GTK_CONSTRAINT_ATTRIBUTE_START, + GTK_CONSTRAINT_ATTRIBUTE_END, + GTK_CONSTRAINT_ATTRIBUTE_WIDTH, + GTK_CONSTRAINT_ATTRIBUTE_HEIGHT, + GTK_CONSTRAINT_ATTRIBUTE_CENTER_X, + GTK_CONSTRAINT_ATTRIBUTE_CENTER_Y, + GTK_CONSTRAINT_ATTRIBUTE_BASELINE +} GtkConstraintAttribute; + +/** + * GtkConstraintVflParserError: + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL: Invalid or unknown symbol + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_ATTRIBUTE: Invalid or unknown attribute + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW: Invalid or unknown view + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_METRIC: Invalid or unknown metric + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_PRIORITY: Invalid or unknown priority + * @GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_RELATION: Invalid or unknown relation + * + * Domain for VFL parsing errors. + */ +typedef enum { + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_SYMBOL, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_ATTRIBUTE, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_VIEW, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_METRIC, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_PRIORITY, + GTK_CONSTRAINT_VFL_PARSER_ERROR_INVALID_RELATION +} GtkConstraintVflParserError; + #endif /* __GTK_ENUMS_H__ */ diff --git a/gtk/gtklayoutmanager.c b/gtk/gtklayoutmanager.c index c41a604b99..547c76bbdf 100644 --- a/gtk/gtklayoutmanager.c +++ b/gtk/gtklayoutmanager.c @@ -91,6 +91,7 @@ typedef struct { GtkWidget *widget; + GtkRoot *root; /* HashTable<Widget, LayoutChild> */ GHashTable *layout_children; @@ -98,6 +99,16 @@ typedef struct { G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtkLayoutManager, gtk_layout_manager, G_TYPE_OBJECT) +static void +gtk_layout_manager_real_root (GtkLayoutManager *manager) +{ +} + +static void +gtk_layout_manager_real_unroot (GtkLayoutManager *manager) +{ +} + static GtkSizeRequestMode gtk_layout_manager_real_get_request_mode (GtkLayoutManager *manager, GtkWidget *widget) @@ -189,12 +200,29 @@ gtk_layout_manager_real_create_layout_child (GtkLayoutManager *manager, } static void +gtk_layout_manager_finalize (GObject *gobject) +{ + GtkLayoutManager *self = GTK_LAYOUT_MANAGER (gobject); + GtkLayoutManagerPrivate *priv = gtk_layout_manager_get_instance_private (self); + + g_clear_pointer (&priv->layout_children, g_hash_table_unref); + + G_OBJECT_CLASS (gtk_layout_manager_parent_class)->finalize (gobject); +} + +static void gtk_layout_manager_class_init (GtkLayoutManagerClass *klass) { + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gtk_layout_manager_finalize; + klass->get_request_mode = gtk_layout_manager_real_get_request_mode; klass->measure = gtk_layout_manager_real_measure; klass->allocate = gtk_layout_manager_real_allocate; klass->create_layout_child = gtk_layout_manager_real_create_layout_child; + klass->root = gtk_layout_manager_real_root; + klass->unroot = gtk_layout_manager_real_unroot; } static void @@ -226,6 +254,38 @@ gtk_layout_manager_set_widget (GtkLayoutManager *layout_manager, } priv->widget = widget; + + if (widget != NULL) + gtk_layout_manager_set_root (layout_manager, gtk_widget_get_root (widget)); +} + +/*< private > + * gtk_layout_manager_set_root: + * @layout_manager: a #GtkLayoutManager + * @root: (nullable): a #GtkWidget implementing #GtkRoot + * + * Sets a back pointer from @root to @layout_manager. + * + * This function is called by #GtkWidget when getting rooted and unrooted, + * and will call #GtkLayoutManagerClass.root() or #GtkLayoutManagerClass.unroot() + * depending on whether @root is a #GtkWidget or %NULL. + */ +void +gtk_layout_manager_set_root (GtkLayoutManager *layout_manager, + GtkRoot *root) +{ + GtkLayoutManagerPrivate *priv = gtk_layout_manager_get_instance_private (layout_manager); + GtkRoot *old_root = priv->root; + + priv->root = root; + + if (old_root != root) + { + if (priv->root != NULL) + GTK_LAYOUT_MANAGER_GET_CLASS (layout_manager)->root (layout_manager); + else + GTK_LAYOUT_MANAGER_GET_CLASS (layout_manager)->unroot (layout_manager); + } } /** diff --git a/gtk/gtklayoutmanager.h b/gtk/gtklayoutmanager.h index 06659ee70e..3066dc5660 100644 --- a/gtk/gtklayoutmanager.h +++ b/gtk/gtklayoutmanager.h @@ -42,6 +42,10 @@ G_DECLARE_DERIVABLE_TYPE (GtkLayoutManager, gtk_layout_manager, GTK, LAYOUT_MANA * @layout_child_type: the type of #GtkLayoutChild used by this layout manager * @create_layout_child: a virtual function, used to create a #GtkLayoutChild * meta object for the layout properties + * @root: a virtual function, called when the widget using the layout + * manager is attached to a #GtkRoot + * @unroot: a virtual function, called when the widget using the layout + * manager is detached from a #GtkRoot * * The `GtkLayoutManagerClass` structure contains only private data, and * should only be accessed through the provided API, or when subclassing @@ -77,6 +81,9 @@ struct _GtkLayoutManagerClass GtkWidget *widget, GtkWidget *for_child); + void (* root) (GtkLayoutManager *manager); + void (* unroot) (GtkLayoutManager *manager); + /*< private >*/ gpointer _padding[16]; }; diff --git a/gtk/gtklayoutmanagerprivate.h b/gtk/gtklayoutmanagerprivate.h index 14eb30e161..5cdbf2e440 100644 --- a/gtk/gtklayoutmanagerprivate.h +++ b/gtk/gtklayoutmanagerprivate.h @@ -10,4 +10,7 @@ void gtk_layout_manager_set_widget (GtkLayoutManager *manager, void gtk_layout_manager_remove_layout_child (GtkLayoutManager *manager, GtkWidget *widget); +void gtk_layout_manager_set_root (GtkLayoutManager *manager, + GtkRoot *root); + G_END_DECLS diff --git a/gtk/gtkmain.c b/gtk/gtkmain.c index 35ea753e1f..e3e592595d 100644 --- a/gtk/gtkmain.c +++ b/gtk/gtkmain.c @@ -179,7 +179,8 @@ static const GDebugKey gtk_debug_keys[] = { { "actions", GTK_DEBUG_ACTIONS }, { "resize", GTK_DEBUG_RESIZE }, { "layout", GTK_DEBUG_LAYOUT }, - { "snapshot", GTK_DEBUG_SNAPSHOT } + { "snapshot", GTK_DEBUG_SNAPSHOT }, + { "constraints", GTK_DEBUG_CONSTRAINTS }, }; #endif /* G_ENABLE_DEBUG */ diff --git a/gtk/gtkroot.c b/gtk/gtkroot.c index 919aa65f94..dce284a57f 100644 --- a/gtk/gtkroot.c +++ b/gtk/gtkroot.c @@ -50,10 +50,18 @@ gtk_root_default_get_display (GtkRoot *self) return gdk_display_get_default (); } + +static GtkConstraintSolver * +gtk_root_default_get_constraint_solver (GtkRoot *self) +{ + return NULL; +} + static void gtk_root_default_init (GtkRootInterface *iface) { iface->get_display = gtk_root_default_get_display; + iface->get_constraint_solver = gtk_root_default_get_constraint_solver; g_object_interface_install_property (iface, g_param_spec_object ("focus-widget", @@ -82,6 +90,17 @@ gtk_root_get_display (GtkRoot *self) return iface->get_display (self); } +GtkConstraintSolver * +gtk_root_get_constraint_solver (GtkRoot *self) +{ + GtkRootInterface *iface; + + g_return_val_if_fail (GTK_IS_ROOT (self), NULL); + + iface = GTK_ROOT_GET_IFACE (self); + return iface->get_constraint_solver (self); +} + /** * gtk_root_set_focus: * @self: a #GtkRoot diff --git a/gtk/gtkroot.h b/gtk/gtkroot.h index c147c09ff0..08b3888d3c 100644 --- a/gtk/gtkroot.h +++ b/gtk/gtkroot.h @@ -34,20 +34,6 @@ G_BEGIN_DECLS GDK_AVAILABLE_IN_ALL G_DECLARE_INTERFACE (GtkRoot, gtk_root, GTK, ROOT, GtkWidget) -/** - * GtkRootIface: - * - * The list of functions that must be implemented for the #GtkRoot interface. - */ -struct _GtkRootInterface -{ - /*< private >*/ - GTypeInterface g_iface; - - /*< public >*/ - GdkDisplay * (* get_display) (GtkRoot *self); -}; - GDK_AVAILABLE_IN_ALL GdkDisplay * gtk_root_get_display (GtkRoot *self); diff --git a/gtk/gtkrootprivate.h b/gtk/gtkrootprivate.h index 3afbae712c..aae7a674a5 100644 --- a/gtk/gtkrootprivate.h +++ b/gtk/gtkrootprivate.h @@ -3,8 +3,28 @@ #include "gtkroot.h" +#include "gtkconstraintsolverprivate.h" + G_BEGIN_DECLS +/** + * GtkRootIface: + * + * The list of functions that must be implemented for the #GtkRoot interface. + */ +struct _GtkRootInterface +{ + /*< private >*/ + GTypeInterface g_iface; + + /*< public >*/ + GdkDisplay * (* get_display) (GtkRoot *self); + + GtkConstraintSolver * (* get_constraint_solver) (GtkRoot *self); +}; + +GtkConstraintSolver * gtk_root_get_constraint_solver (GtkRoot *self); + enum { GTK_ROOT_PROP_FOCUS_WIDGET, GTK_ROOT_NUM_PROPERTIES diff --git a/gtk/gtkwidget.c b/gtk/gtkwidget.c index 0c653a90f8..dd6e67dda7 100644 --- a/gtk/gtkwidget.c +++ b/gtk/gtkwidget.c @@ -73,6 +73,7 @@ #include "gtkwindowgroup.h" #include "gtkwindowprivate.h" #include "gtknativeprivate.h" +#include "gtkconstraint.h" #include "a11y/gtkwidgetaccessible.h" #include "inspector/window.h" @@ -770,6 +771,13 @@ gtk_widget_get_type (void) NULL /* interface data */ }; + const GInterfaceInfo constraint_target_info = + { + (GInterfaceInitFunc) NULL, + (GInterfaceFinalizeFunc) NULL, + NULL /* interface data */ + }; + widget_type = g_type_register_static (G_TYPE_INITIALLY_UNOWNED, g_intern_static_string ("GtkWidget"), &widget_info, G_TYPE_FLAG_ABSTRACT); @@ -782,6 +790,8 @@ gtk_widget_get_type (void) &accessibility_info) ; g_type_add_interface_static (widget_type, GTK_TYPE_BUILDABLE, &buildable_info) ; + g_type_add_interface_static (widget_type, GTK_TYPE_CONSTRAINT_TARGET, + &constraint_target_info) ; } return widget_type; @@ -2893,6 +2903,9 @@ gtk_widget_root (GtkWidget *widget) _gtk_widget_update_parent_muxer (widget); + if (priv->layout_manager) + gtk_layout_manager_set_root (priv->layout_manager, priv->root); + GTK_WIDGET_GET_CLASS (widget)->root (widget); if (!GTK_IS_ROOT (widget)) @@ -2920,6 +2933,9 @@ gtk_widget_unroot (GtkWidget *widget) if (priv->context) gtk_style_context_set_display (priv->context, gdk_display_get_default ()); + if (priv->layout_manager) + gtk_layout_manager_set_root (priv->layout_manager, NULL); + if (g_object_get_qdata (G_OBJECT (widget), quark_pango_context)) g_object_set_qdata (G_OBJECT (widget), quark_pango_context, NULL); diff --git a/gtk/gtkwindow.c b/gtk/gtkwindow.c index 3730841f36..dab50be990 100644 --- a/gtk/gtkwindow.c +++ b/gtk/gtkwindow.c @@ -281,6 +281,8 @@ typedef struct GskRenderer *renderer; GList *foci; + + GtkConstraintSolver *constraint_solver; } GtkWindowPrivate; #ifdef GDK_WINDOWING_X11 @@ -1883,6 +1885,9 @@ gtk_window_init (GtkWindow *window) g_signal_connect_swapped (priv->key_controller, "key-released", G_CALLBACK (gtk_window_key_released), window); gtk_widget_add_controller (widget, priv->key_controller); + + /* Shared constraint solver */ + priv->constraint_solver = gtk_constraint_solver_new (); } static GtkGesture * @@ -2353,6 +2358,15 @@ gtk_window_native_get_renderer (GtkNative *native) return priv->renderer; } +static GtkConstraintSolver * +gtk_window_root_get_constraint_solver (GtkRoot *root) +{ + GtkWindow *self = GTK_WINDOW (root); + GtkWindowPrivate *priv = gtk_window_get_instance_private (self); + + return priv->constraint_solver; +} + static void gtk_window_native_get_surface_transform (GtkNative *native, int *x, @@ -2381,6 +2395,7 @@ static void gtk_window_root_interface_init (GtkRootInterface *iface) { iface->get_display = gtk_window_root_get_display; + iface->get_constraint_solver = gtk_window_root_get_constraint_solver; } static void @@ -4723,6 +4738,7 @@ gtk_window_finalize (GObject *object) priv->mnemonics_display_timeout_id = 0; } + g_clear_object (&priv->constraint_solver); g_clear_object (&priv->renderer); G_OBJECT_CLASS (gtk_window_parent_class)->finalize (object); diff --git a/gtk/meson.build b/gtk/meson.build index 0b271b6f50..428e1e49ab 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -37,6 +37,9 @@ gtk_private_sources = files([ 'gtkcolorpickershell.c', 'gtkcolorscale.c', 'gtkcolorswatch.c', + 'gtkconstraintexpression.c', + 'gtkconstraintsolver.c', + 'gtkconstraintvflparser.c', 'gtkcssanimatedstyle.c', 'gtkcssanimation.c', 'gtkcssarrayvalue.c', @@ -200,6 +203,9 @@ gtk_public_sources = files([ 'gtkcombobox.c', 'gtkcomboboxtext.c', 'gtkcomposetable.c', + 'gtkconstraintguide.c', + 'gtkconstraintlayout.c', + 'gtkconstraint.c', 'gtkcontainer.c', 'gtkcssprovider.c', 'gtkdialog.c', @@ -459,6 +465,9 @@ gtk_public_headers = files([ 'gtkcolorutils.h', 'gtkcombobox.h', 'gtkcomboboxtext.h', + 'gtkconstraintguide.h', + 'gtkconstraintlayout.h', + 'gtkconstraint.h', 'gtkcontainer.h', 'gtkcssprovider.h', 'gtkcustomlayout.h', diff --git a/testsuite/gtk/constraint-solver.c b/testsuite/gtk/constraint-solver.c new file mode 100644 index 0000000000..a4597c70ba --- /dev/null +++ b/testsuite/gtk/constraint-solver.c @@ -0,0 +1,380 @@ +#include <locale.h> + +#include "../../gtk/gtkconstrainttypesprivate.h" +#include "../../gtk/gtkconstraintsolverprivate.h" +#include "../../gtk/gtkconstraintexpressionprivate.h" + +static void +constraint_solver_simple (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 167.0); + GtkConstraintVariable *y = gtk_constraint_solver_create_variable (solver, NULL, "y", 2.0); + + GtkConstraintExpression *e = gtk_constraint_expression_new_from_variable (y); + + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + double x_value = gtk_constraint_variable_get_value (x); + double y_value = gtk_constraint_variable_get_value (y); + + g_assert_cmpfloat_with_epsilon (x_value, y_value, 0.001); + g_assert_cmpfloat_with_epsilon (x_value, 0.0, 0.001); + g_assert_cmpfloat_with_epsilon (y_value, 0.0, 0.001); + + gtk_constraint_variable_unref (y); + gtk_constraint_variable_unref (x); + + g_object_unref (solver); +} + +static void +constraint_solver_stay (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 5.0); + GtkConstraintVariable *y = gtk_constraint_solver_create_variable (solver, NULL, "y", 10.0); + + gtk_constraint_solver_add_stay_variable (solver, x, GTK_CONSTRAINT_STRENGTH_WEAK); + gtk_constraint_solver_add_stay_variable (solver, y, GTK_CONSTRAINT_STRENGTH_WEAK); + + double x_value = gtk_constraint_variable_get_value (x); + double y_value = gtk_constraint_variable_get_value (y); + + g_assert_cmpfloat_with_epsilon (x_value, 5.0, 0.001); + g_assert_cmpfloat_with_epsilon (y_value, 10.0, 0.001); + + gtk_constraint_variable_unref (x); + gtk_constraint_variable_unref (y); + + g_object_unref (solver); +} + +static void +constraint_solver_variable_geq_constant (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 10.0); + GtkConstraintExpression *e = gtk_constraint_expression_new (100.0); + + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_GE, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + double x_value = gtk_constraint_variable_get_value (x); + + g_assert_cmpfloat (x_value, >=, 100.0); + + gtk_constraint_variable_unref (x); + + g_object_unref (solver); +} + +static void +constraint_solver_variable_leq_constant (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 100.0); + GtkConstraintExpression *e = gtk_constraint_expression_new (10.0); + + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_LE, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + double x_value = gtk_constraint_variable_get_value (x); + + g_assert_cmpfloat (x_value, <=, 10.0); + + gtk_constraint_variable_unref (x); + + g_object_unref (solver); +} + +static void +constraint_solver_variable_eq_constant (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 10.0); + GtkConstraintExpression *e = gtk_constraint_expression_new (100.0); + + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + double x_value = gtk_constraint_variable_get_value (x); + + g_assert_cmpfloat_with_epsilon (x_value, 100.0, 0.001); + + gtk_constraint_variable_unref (x); + + g_object_unref (solver); +} + +static void +constraint_solver_eq_with_stay (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 10.0); + GtkConstraintVariable *width = gtk_constraint_solver_create_variable (solver, NULL, "width", 10.0); + GtkConstraintVariable *right_min = gtk_constraint_solver_create_variable (solver, NULL, "rightMin", 100.0); + + GtkConstraintExpressionBuilder builder; + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, x); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, width); + GtkConstraintExpression *right = gtk_constraint_expression_builder_finish (&builder); + + gtk_constraint_solver_add_stay_variable (solver, width, GTK_CONSTRAINT_STRENGTH_WEAK); + gtk_constraint_solver_add_stay_variable (solver, right_min, GTK_CONSTRAINT_STRENGTH_WEAK); + gtk_constraint_solver_add_constraint (solver, + right_min, GTK_CONSTRAINT_RELATION_EQ, right, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + double x_value = gtk_constraint_variable_get_value (x); + double width_value = gtk_constraint_variable_get_value (width); + + g_assert_cmpfloat_with_epsilon (x_value, 90.0, 0.001); + g_assert_cmpfloat_with_epsilon (width_value, 10.0, 0.001); + + gtk_constraint_variable_unref (right_min); + gtk_constraint_variable_unref (width); + gtk_constraint_variable_unref (x); + + g_object_unref (solver); +} + +static void +constraint_solver_cassowary (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *x = gtk_constraint_solver_create_variable (solver, NULL, "x", 0.0); + GtkConstraintVariable *y = gtk_constraint_solver_create_variable (solver, NULL, "y", 0.0); + + GtkConstraintExpression *e; + + e = gtk_constraint_expression_new_from_variable (y); + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_LE, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + e = gtk_constraint_expression_plus_constant (gtk_constraint_expression_new_from_variable (x), 3.0); + gtk_constraint_solver_add_constraint (solver, + y, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + e = gtk_constraint_expression_new (10.0); + gtk_constraint_solver_add_constraint (solver, + x, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_WEAK); + + e = gtk_constraint_expression_new (10.0); + gtk_constraint_solver_add_constraint (solver, + y, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_WEAK); + + double x_val = gtk_constraint_variable_get_value (x); + double y_val = gtk_constraint_variable_get_value (y); + + g_test_message ("x = %g, y = %g", x_val, y_val); + + /* The system is unstable and has two possible solutions we need to test */ + g_assert_true ((G_APPROX_VALUE (x_val, 10.0, 1e-8) && + G_APPROX_VALUE (y_val, 13.0, 1e-8)) || + (G_APPROX_VALUE (x_val, 7.0, 1e-8) && + G_APPROX_VALUE (y_val, 10.0, 1e-8))); + + gtk_constraint_variable_unref (x); + gtk_constraint_variable_unref (y); + + g_object_unref (solver); +} + +static void +constraint_solver_edit_var_required (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *a = gtk_constraint_solver_create_variable (solver, NULL, "a", 0.0); + gtk_constraint_solver_add_stay_variable (solver, a, GTK_CONSTRAINT_STRENGTH_STRONG); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 0.0, 0.001); + + gtk_constraint_solver_add_edit_variable (solver, a, GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_solver_begin_edit (solver); + gtk_constraint_solver_suggest_value (solver, a, 2.0); + gtk_constraint_solver_resolve (solver); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 2.0, 0.001); + + gtk_constraint_solver_suggest_value (solver, a, 10.0); + gtk_constraint_solver_resolve (solver); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 10.0, 0.001); + + gtk_constraint_solver_end_edit (solver); + + gtk_constraint_variable_unref (a); + + g_object_unref (solver); +} + +static void +constraint_solver_edit_var_suggest (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *a = gtk_constraint_solver_create_variable (solver, NULL, "a", 0.0); + GtkConstraintVariable *b = gtk_constraint_solver_create_variable (solver, NULL, "b", 0.0); + + gtk_constraint_solver_add_stay_variable (solver, a, GTK_CONSTRAINT_STRENGTH_STRONG); + + GtkConstraintExpression *e = gtk_constraint_expression_new_from_variable (b); + gtk_constraint_solver_add_constraint (solver, + a, GTK_CONSTRAINT_RELATION_EQ, e, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + gtk_constraint_solver_resolve (solver); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 0.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (b), 0.0, 0.001); + + gtk_constraint_solver_add_edit_variable (solver, a, GTK_CONSTRAINT_STRENGTH_REQUIRED); + gtk_constraint_solver_begin_edit (solver); + + gtk_constraint_solver_suggest_value (solver, a, 2.0); + gtk_constraint_solver_resolve (solver); + + g_test_message ("Check values after first edit"); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 2.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (b), 2.0, 0.001); + + gtk_constraint_solver_suggest_value (solver, a, 10.0); + gtk_constraint_solver_resolve (solver); + + g_test_message ("Check values after second edit"); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 10.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (b), 10.0, 0.001); + + gtk_constraint_solver_suggest_value (solver, a, 12.0); + gtk_constraint_solver_resolve (solver); + + g_test_message ("Check values after third edit"); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (a), 12.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (b), 12.0, 0.001); + + gtk_constraint_variable_unref (a); + gtk_constraint_variable_unref (b); + + g_object_unref (solver); +} + +static void +constraint_solver_paper (void) +{ + GtkConstraintSolver *solver = gtk_constraint_solver_new (); + + GtkConstraintVariable *left = gtk_constraint_solver_create_variable (solver, NULL, "left", 0.0); + GtkConstraintVariable *middle = gtk_constraint_solver_create_variable (solver, NULL, "middle", 0.0); + GtkConstraintVariable *right = gtk_constraint_solver_create_variable (solver, NULL, "right", 0.0); + + GtkConstraintExpressionBuilder builder; + GtkConstraintExpression *expr; + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, left); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_term (&builder, right); + gtk_constraint_expression_builder_divide_by (&builder); + gtk_constraint_expression_builder_constant (&builder, 2.0); + expr = gtk_constraint_expression_builder_finish (&builder); + gtk_constraint_solver_add_constraint (solver, + middle, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + gtk_constraint_expression_builder_init (&builder, solver); + gtk_constraint_expression_builder_term (&builder, left); + gtk_constraint_expression_builder_plus (&builder); + gtk_constraint_expression_builder_constant (&builder, 10.0); + expr = gtk_constraint_expression_builder_finish (&builder); + gtk_constraint_solver_add_constraint (solver, + right, GTK_CONSTRAINT_RELATION_EQ, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + expr = gtk_constraint_expression_new (100.0); + gtk_constraint_solver_add_constraint (solver, + right, GTK_CONSTRAINT_RELATION_LE, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + expr = gtk_constraint_expression_new (0.0); + gtk_constraint_solver_add_constraint (solver, + left, GTK_CONSTRAINT_RELATION_GE, expr, + GTK_CONSTRAINT_STRENGTH_REQUIRED); + + g_test_message ("Check constraints hold"); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (middle), + (gtk_constraint_variable_get_value (left) + gtk_constraint_variable_get_value (right)) / 2.0, + 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (right), + gtk_constraint_variable_get_value (left) + 10.0, + 0.001); + g_assert_cmpfloat (gtk_constraint_variable_get_value (right), <=, 100.0); + g_assert_cmpfloat (gtk_constraint_variable_get_value (left), >=, 0.0); + + gtk_constraint_variable_set_value (middle, 45.0); + gtk_constraint_solver_add_stay_variable (solver, middle, GTK_CONSTRAINT_STRENGTH_WEAK); + + g_test_message ("Check constraints hold after setting middle"); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (middle), + (gtk_constraint_variable_get_value (left) + gtk_constraint_variable_get_value (right)) / 2.0, + 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (right), + gtk_constraint_variable_get_value (left) + 10.0, + 0.001); + g_assert_cmpfloat (gtk_constraint_variable_get_value (right), <=, 100.0); + g_assert_cmpfloat (gtk_constraint_variable_get_value (left), >=, 0.0); + + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (left), 40.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (middle), 45.0, 0.001); + g_assert_cmpfloat_with_epsilon (gtk_constraint_variable_get_value (right), 50.0, 0.001); + + gtk_constraint_variable_unref (left); + gtk_constraint_variable_unref (middle); + gtk_constraint_variable_unref (right); + + g_object_unref (solver); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + setlocale (LC_ALL, "C"); + + g_test_add_func ("/constraint-solver/paper", constraint_solver_paper); + g_test_add_func ("/constraint-solver/simple", constraint_solver_simple); + g_test_add_func ("/constraint-solver/constant/eq", constraint_solver_variable_eq_constant); + g_test_add_func ("/constraint-solver/constant/ge", constraint_solver_variable_geq_constant); + g_test_add_func ("/constraint-solver/constant/le", constraint_solver_variable_leq_constant); + g_test_add_func ("/constraint-solver/stay/simple", constraint_solver_stay); + g_test_add_func ("/constraint-solver/stay/eq", constraint_solver_eq_with_stay); + g_test_add_func ("/constraint-solver/cassowary", constraint_solver_cassowary); + g_test_add_func ("/constraint-solver/edit/required", constraint_solver_edit_var_required); + g_test_add_func ("/constraint-solver/edit/suggest", constraint_solver_edit_var_suggest); + + return g_test_run (); +} diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build index afa5c3c7dd..b02d3f51ce 100644 --- a/testsuite/gtk/meson.build +++ b/testsuite/gtk/meson.build @@ -17,6 +17,11 @@ tests = [ ['builderparser'], ['cellarea'], ['check-icon-names'], + ['constraint-solver', [ + '../../gtk/gtkconstraintsolver.c', + '../../gtk/gtkconstraintexpression.c', + ], ['-DGTK_COMPILATION', '-UG_ENABLE_DEBUG'] + ], ['cssprovider'], ['rbtree-crash', ['../../gtk/gtkrbtree.c'], ['-DGTK_COMPILATION', '-UG_ENABLE_DEBUG']], ['defaultvalue'], |