summaryrefslogtreecommitdiffstats
path: root/subprojects/libhandy/src
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/libhandy/src')
-rw-r--r--subprojects/libhandy/src/gen-public-types.sh22
-rw-r--r--subprojects/libhandy/src/gtk-window-private.h18
-rw-r--r--subprojects/libhandy/src/gtk-window.c169
-rw-r--r--subprojects/libhandy/src/gtkprogresstracker.c248
-rw-r--r--subprojects/libhandy/src/gtkprogresstrackerprivate.h74
-rw-r--r--subprojects/libhandy/src/handy.gresources.xml28
-rw-r--r--subprojects/libhandy/src/handy.h63
-rw-r--r--subprojects/libhandy/src/hdy-action-row.c774
-rw-r--r--subprojects/libhandy/src/hdy-action-row.h73
-rw-r--r--subprojects/libhandy/src/hdy-action-row.ui82
-rw-r--r--subprojects/libhandy/src/hdy-animation-private.h19
-rw-r--r--subprojects/libhandy/src/hdy-animation.c83
-rw-r--r--subprojects/libhandy/src/hdy-animation.h25
-rw-r--r--subprojects/libhandy/src/hdy-application-window.c150
-rw-r--r--subprojects/libhandy/src/hdy-application-window.h35
-rw-r--r--subprojects/libhandy/src/hdy-avatar.c811
-rw-r--r--subprojects/libhandy/src/hdy-avatar.h70
-rw-r--r--subprojects/libhandy/src/hdy-cairo-private.h21
-rw-r--r--subprojects/libhandy/src/hdy-carousel-box-private.h65
-rw-r--r--subprojects/libhandy/src/hdy-carousel-box.c1768
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-dots.c486
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-dots.h34
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-lines.c485
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-lines.h34
-rw-r--r--subprojects/libhandy/src/hdy-carousel.c1099
-rw-r--r--subprojects/libhandy/src/hdy-carousel.h81
-rw-r--r--subprojects/libhandy/src/hdy-carousel.ui21
-rw-r--r--subprojects/libhandy/src/hdy-clamp.c563
-rw-r--r--subprojects/libhandy/src/hdy-clamp.h37
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.c829
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.h112
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.ui43
-rw-r--r--subprojects/libhandy/src/hdy-css-private.h25
-rw-r--r--subprojects/libhandy/src/hdy-css.c73
-rw-r--r--subprojects/libhandy/src/hdy-deck.c1103
-rw-r--r--subprojects/libhandy/src/hdy-deck.h102
-rw-r--r--subprojects/libhandy/src/hdy-deprecation-macros.h31
-rw-r--r--subprojects/libhandy/src/hdy-enum-value-object.c73
-rw-r--r--subprojects/libhandy/src/hdy-enum-value-object.h35
-rw-r--r--subprojects/libhandy/src/hdy-enums-private.c.in38
-rw-r--r--subprojects/libhandy/src/hdy-enums-private.h.in27
-rw-r--r--subprojects/libhandy/src/hdy-enums.c.in44
-rw-r--r--subprojects/libhandy/src/hdy-enums.h.in28
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.c762
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.h83
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.ui97
-rw-r--r--subprojects/libhandy/src/hdy-header-bar.c2868
-rw-r--r--subprojects/libhandy/src/hdy-header-bar.h123
-rw-r--r--subprojects/libhandy/src/hdy-header-group.c1115
-rw-r--r--subprojects/libhandy/src/hdy-header-group.h81
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button-private.h32
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button.c331
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button.ui36
-rw-r--r--subprojects/libhandy/src/hdy-keypad.c793
-rw-r--r--subprojects/libhandy/src/hdy-keypad.h76
-rw-r--r--subprojects/libhandy/src/hdy-keypad.ui216
-rw-r--r--subprojects/libhandy/src/hdy-leaflet.c1209
-rw-r--r--subprojects/libhandy/src/hdy-leaflet.h113
-rw-r--r--subprojects/libhandy/src/hdy-main-private.h24
-rw-r--r--subprojects/libhandy/src/hdy-main.c201
-rw-r--r--subprojects/libhandy/src/hdy-main.h21
-rw-r--r--subprojects/libhandy/src/hdy-navigation-direction.c26
-rw-r--r--subprojects/libhandy/src/hdy-navigation-direction.h23
-rw-r--r--subprojects/libhandy/src/hdy-nothing-private.h23
-rw-r--r--subprojects/libhandy/src/hdy-nothing.c47
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group-private.h16
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.c449
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.ui55
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page-private.h18
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.c365
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.ui34
-rw-r--r--subprojects/libhandy/src/hdy-preferences-row.c259
-rw-r--r--subprojects/libhandy/src/hdy-preferences-row.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.c721
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.h58
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.ui248
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.c659
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.h51
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.ui63
-rw-r--r--subprojects/libhandy/src/hdy-shadow-helper-private.h32
-rw-r--r--subprojects/libhandy/src/hdy-shadow-helper.c445
-rw-r--r--subprojects/libhandy/src/hdy-squeezer.c1576
-rw-r--r--subprojects/libhandy/src/hdy-squeezer.h83
-rw-r--r--subprojects/libhandy/src/hdy-stackable-box-private.h133
-rw-r--r--subprojects/libhandy/src/hdy-stackable-box.c3151
-rw-r--r--subprojects/libhandy/src/hdy-swipe-group.c568
-rw-r--r--subprojects/libhandy/src/hdy-swipe-group.h37
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker-private.h26
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker.c1113
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker.h53
-rw-r--r--subprojects/libhandy/src/hdy-swipeable.c273
-rw-r--r--subprojects/libhandy/src/hdy-swipeable.h91
-rw-r--r--subprojects/libhandy/src/hdy-title-bar.c347
-rw-r--r--subprojects/libhandy/src/hdy-title-bar.h33
-rw-r--r--subprojects/libhandy/src/hdy-types.h17
-rw-r--r--subprojects/libhandy/src/hdy-value-object.c267
-rw-r--r--subprojects/libhandy/src/hdy-value-object.h45
-rw-r--r--subprojects/libhandy/src/hdy-version.h.in87
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.c392
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.h48
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.ui20
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button-private.h49
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button.c536
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button.ui105
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.c600
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.h63
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.ui52
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher.c734
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher.h52
-rw-r--r--subprojects/libhandy/src/hdy-window-handle-controller-private.h23
-rw-r--r--subprojects/libhandy/src/hdy-window-handle-controller.c515
-rw-r--r--subprojects/libhandy/src/hdy-window-handle.c83
-rw-r--r--subprojects/libhandy/src/hdy-window-handle.h27
-rw-r--r--subprojects/libhandy/src/hdy-window-mixin-private.h42
-rw-r--r--subprojects/libhandy/src/hdy-window-mixin.c583
-rw-r--r--subprojects/libhandy/src/hdy-window.c195
-rw-r--r--subprojects/libhandy/src/hdy-window.h35
-rw-r--r--subprojects/libhandy/src/icons/avatar-default-symbolic.svg3
-rw-r--r--subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg7
-rw-r--r--subprojects/libhandy/src/meson.build298
-rw-r--r--subprojects/libhandy/src/themes/Adwaita-dark.css197
-rw-r--r--subprojects/libhandy/src/themes/Adwaita-dark.scss5
-rw-r--r--subprojects/libhandy/src/themes/Adwaita.css197
-rw-r--r--subprojects/libhandy/src/themes/Adwaita.scss5
-rw-r--r--subprojects/libhandy/src/themes/HighContrast.css197
-rw-r--r--subprojects/libhandy/src/themes/HighContrast.scss6
-rw-r--r--subprojects/libhandy/src/themes/HighContrastInverse.css197
-rw-r--r--subprojects/libhandy/src/themes/HighContrastInverse.scss6
-rw-r--r--subprojects/libhandy/src/themes/_Adwaita-base.scss336
-rw-r--r--subprojects/libhandy/src/themes/_definitions.scss66
-rw-r--r--subprojects/libhandy/src/themes/_fallback-base.scss146
-rw-r--r--subprojects/libhandy/src/themes/_shared-base.scss21
-rw-r--r--subprojects/libhandy/src/themes/fallback.css74
-rw-r--r--subprojects/libhandy/src/themes/fallback.scss5
-rwxr-xr-xsubprojects/libhandy/src/themes/parse-sass.sh44
-rw-r--r--subprojects/libhandy/src/themes/shared.css6
-rw-r--r--subprojects/libhandy/src/themes/shared.scss5
139 files changed, 35802 insertions, 0 deletions
diff --git a/subprojects/libhandy/src/gen-public-types.sh b/subprojects/libhandy/src/gen-public-types.sh
new file mode 100644
index 0000000..036c336
--- /dev/null
+++ b/subprojects/libhandy/src/gen-public-types.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -e
+
+echo '/* This file was generated by gen-plublic-types.sh, do not edit it. */
+'
+
+for var in "$@"
+do
+ echo "#include \"$var\""
+done
+
+echo '#include "hdy-main-private.h"
+
+void
+hdy_init_public_types (void)
+{'
+
+sed -ne 's/^#define \{1,\}\(HDY_TYPE_[A-Z0-9_]\{1,\}\) \{1,\}.*/ g_type_ensure (\1);/p' "$@" | sort
+
+echo '}
+'
diff --git a/subprojects/libhandy/src/gtk-window-private.h b/subprojects/libhandy/src/gtk-window-private.h
new file mode 100644
index 0000000..e2f183e
--- /dev/null
+++ b/subprojects/libhandy/src/gtk-window-private.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void hdy_gtk_window_toggle_maximized (GtkWindow *window);
+GdkPixbuf *hdy_gtk_window_get_icon_for_size (GtkWindow *window,
+ gint size);
+GdkWindowState hdy_gtk_window_get_state (GtkWindow *window);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/gtk-window.c b/subprojects/libhandy/src/gtk-window.c
new file mode 100644
index 0000000..154acdb
--- /dev/null
+++ b/subprojects/libhandy/src/gtk-window.c
@@ -0,0 +1,169 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
+ * file for a list of people on the GTK+ Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/* Bits taken from GTK 3.24 and tweaked to be used by libhandy. */
+
+#include "gtk-window-private.h"
+
+typedef struct
+{
+ GList *icon_list;
+ gchar *icon_name;
+ guint realized : 1;
+ guint using_default_icon : 1;
+ guint using_parent_icon : 1;
+ guint using_themed_icon : 1;
+} GtkWindowIconInfo;
+
+static GQuark quark_gtk_window_icon_info = 0;
+
+static void
+ensure_quarks (void)
+{
+ if (!quark_gtk_window_icon_info)
+ quark_gtk_window_icon_info = g_quark_from_static_string ("gtk-window-icon-info");
+}
+
+void
+hdy_gtk_window_toggle_maximized (GtkWindow *window)
+{
+ if (gtk_window_is_maximized (window))
+ gtk_window_unmaximize (window);
+ else
+ gtk_window_maximize (window);
+}
+
+static GtkWindowIconInfo*
+get_icon_info (GtkWindow *window)
+{
+ ensure_quarks ();
+
+ return g_object_get_qdata (G_OBJECT (window), quark_gtk_window_icon_info);
+}
+
+static void
+free_icon_info (GtkWindowIconInfo *info)
+{
+ g_free (info->icon_name);
+ g_slice_free (GtkWindowIconInfo, info);
+}
+
+static GtkWindowIconInfo*
+ensure_icon_info (GtkWindow *window)
+{
+ GtkWindowIconInfo *info;
+
+ ensure_quarks ();
+
+ info = get_icon_info (window);
+
+ if (info == NULL)
+ {
+ info = g_slice_new0 (GtkWindowIconInfo);
+ g_object_set_qdata_full (G_OBJECT (window),
+ quark_gtk_window_icon_info,
+ info,
+ (GDestroyNotify)free_icon_info);
+ }
+
+ return info;
+}
+
+static GdkPixbuf *
+icon_from_list (GList *list,
+ gint size)
+{
+ GdkPixbuf *best;
+ GdkPixbuf *pixbuf;
+ GList *l;
+
+ best = NULL;
+ for (l = list; l; l = l->next)
+ {
+ pixbuf = list->data;
+ if (gdk_pixbuf_get_width (pixbuf) <= size &&
+ gdk_pixbuf_get_height (pixbuf) <= size)
+ {
+ best = g_object_ref (pixbuf);
+ break;
+ }
+ }
+
+ if (best == NULL)
+ best = gdk_pixbuf_scale_simple (GDK_PIXBUF (list->data), size, size, GDK_INTERP_BILINEAR);
+
+ return best;
+}
+
+static GdkPixbuf *
+icon_from_name (const gchar *name,
+ gint size)
+{
+ return gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+ name, size,
+ GTK_ICON_LOOKUP_FORCE_SIZE, NULL);
+}
+
+GdkPixbuf *
+hdy_gtk_window_get_icon_for_size (GtkWindow *window,
+ gint size)
+{
+ GtkWindowIconInfo *info;
+ const gchar *name;
+ g_autoptr (GList) default_icon_list = gtk_window_get_default_icon_list ();
+
+ info = ensure_icon_info (window);
+
+ if (info->icon_list != NULL)
+ return icon_from_list (info->icon_list, size);
+
+ name = gtk_window_get_icon_name (window);
+ if (name != NULL)
+ return icon_from_name (name, size);
+
+ if (gtk_window_get_transient_for (window) != NULL)
+ {
+ info = ensure_icon_info (gtk_window_get_transient_for (window));
+ if (info->icon_list)
+ return icon_from_list (info->icon_list, size);
+ }
+
+ if (default_icon_list != NULL)
+ return icon_from_list (default_icon_list, size);
+
+ if (gtk_window_get_default_icon_name () != NULL)
+ return icon_from_name (gtk_window_get_default_icon_name (), size);
+
+ return NULL;
+}
+
+GdkWindowState
+hdy_gtk_window_get_state (GtkWindow *window)
+{
+ GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (window));
+
+ return gdk_window ? gdk_window_get_state (gdk_window) : 0;
+}
diff --git a/subprojects/libhandy/src/gtkprogresstracker.c b/subprojects/libhandy/src/gtkprogresstracker.c
new file mode 100644
index 0000000..72d2013
--- /dev/null
+++ b/subprojects/libhandy/src/gtkprogresstracker.c
@@ -0,0 +1,248 @@
+/*
+ * Copyright © 2016 Endless Mobile 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: Matthew Watson <mattdangerw@gmail.com>
+ */
+
+#include "gtkprogresstrackerprivate.h"
+/* #include "gtkprivate.h" */
+/* #include "gtkcsseasevalueprivate.h" */
+
+#include <math.h>
+#include <string.h>
+
+#include "hdy-animation-private.h"
+
+/*
+ * Progress tracker is small helper for tracking progress through gtk
+ * animations. It's a simple zero-initable struct, meant to be thrown in a
+ * widget's private data without the need for setup or teardown.
+ *
+ * Progress tracker will handle translating frame clock timestamps to a
+ * fractional progress value for interpolating between animation targets.
+ *
+ * Progress tracker will use the GTK_SLOWDOWN environment variable to control
+ * the speed of animations. This can be useful for debugging.
+ */
+
+static gdouble gtk_slowdown = 1.0;
+
+/**
+ * gtk_progress_tracker_init_copy:
+ * @source: The source progress tracker
+ * @dest: The destination progress tracker
+ *
+ * Copy all progress tracker state from the source tracker to dest tracker.
+ **/
+void
+gtk_progress_tracker_init_copy (GtkProgressTracker *source,
+ GtkProgressTracker *dest)
+{
+ memcpy (dest, source, sizeof (GtkProgressTracker));
+}
+
+/**
+ * gtk_progress_tracker_start:
+ * @tracker: The progress tracker
+ * @duration: Animation duration in us
+ * @delay: Animation delay in us
+ * @iteration_count: Number of iterations to run the animation, must be >= 0
+ *
+ * Begins tracking progress for a new animation. Clears all previous state.
+ **/
+void
+gtk_progress_tracker_start (GtkProgressTracker *tracker,
+ guint64 duration,
+ gint64 delay,
+ gdouble iteration_count)
+{
+ tracker->is_running = TRUE;
+ tracker->last_frame_time = 0;
+ tracker->duration = duration;
+ tracker->iteration = - delay / (gdouble) duration;
+ tracker->iteration_count = iteration_count;
+}
+
+/**
+ * gtk_progress_tracker_finish:
+ * @tracker: The progress tracker
+ *
+ * Stops running the current animation.
+ **/
+void
+gtk_progress_tracker_finish (GtkProgressTracker *tracker)
+{
+ tracker->is_running = FALSE;
+}
+
+/**
+ * gtk_progress_tracker_advance_frame:
+ * @tracker: The progress tracker
+ * @frame_time: The current frame time, usually from the frame clock.
+ *
+ * Increments the progress of the animation forward a frame. If no animation has
+ * been started, does nothing.
+ **/
+void
+gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker,
+ guint64 frame_time)
+{
+ gdouble delta;
+
+ if (!tracker->is_running)
+ return;
+
+ if (tracker->last_frame_time == 0)
+ {
+ tracker->last_frame_time = frame_time;
+ return;
+ }
+
+ if (frame_time < tracker->last_frame_time)
+ {
+ g_warning ("Progress tracker frame set backwards, ignoring.");
+ return;
+ }
+
+ delta = (frame_time - tracker->last_frame_time) / gtk_slowdown / tracker->duration;
+ tracker->last_frame_time = frame_time;
+ tracker->iteration += delta;
+}
+
+/**
+ * gtk_progress_tracker_skip_frame:
+ * @tracker: The progress tracker
+ * @frame_time: The current frame time, usually from the frame clock.
+ *
+ * Does not update the progress of the animation forward, but records the frame
+ * to calculate future deltas. Calling this each frame will effectively pause
+ * the animation.
+ **/
+void
+gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker,
+ guint64 frame_time)
+{
+ if (!tracker->is_running)
+ return;
+
+ tracker->last_frame_time = frame_time;
+}
+
+/**
+ * gtk_progress_tracker_get_state:
+ * @tracker: The progress tracker
+ *
+ * Returns whether the tracker is before, during or after the currently started
+ * animation. The tracker will only ever be in the before state if the animation
+ * was started with a delay. If no animation has been started, returns
+ * %GTK_PROGRESS_STATE_AFTER.
+ *
+ * Returns: A GtkProgressState
+ **/
+GtkProgressState
+gtk_progress_tracker_get_state (GtkProgressTracker *tracker)
+{
+ if (!tracker->is_running || tracker->iteration > tracker->iteration_count)
+ return GTK_PROGRESS_STATE_AFTER;
+ if (tracker->iteration < 0)
+ return GTK_PROGRESS_STATE_BEFORE;
+ return GTK_PROGRESS_STATE_DURING;
+}
+
+/**
+ * gtk_progress_tracker_get_iteration:
+ * @tracker: The progress tracker
+ *
+ * Returns the fractional number of cycles the animation has completed. For
+ * example, it you started an animation with iteration-count of 2 and are half
+ * way through the second animation, this returns 1.5.
+ *
+ * Returns: The current iteration.
+ **/
+gdouble
+gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker)
+{
+ return tracker->is_running ? CLAMP (tracker->iteration, 0.0, tracker->iteration_count) : 1.0;
+}
+
+/**
+ * gtk_progress_tracker_get_iteration_cycle:
+ * @tracker: The progress tracker
+ *
+ * Returns an integer index of the current iteration cycle tracker is
+ * progressing through. Handles edge cases, such as an iteration value of 2.0
+ * which could be considered the end of the second iteration of the beginning of
+ * the third, in the same way as gtk_progress_tracker_get_progress().
+ *
+ * Returns: The integer count of the current animation cycle.
+ **/
+guint64
+gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker)
+{
+ gdouble iteration = gtk_progress_tracker_get_iteration (tracker);
+
+ /* Some complexity here. We want an iteration of 0.0 to always map to 0 (start
+ * of the first iteration), but an iteration of 1.0 to also map to 0 (end of
+ * first iteration) and 2.0 to 1 (end of the second iteration).
+ */
+ if (iteration == 0.0)
+ return 0;
+
+ return (guint64) ceil (iteration) - 1;
+}
+
+/**
+ * gtk_progress_tracker_get_progress:
+ * @tracker: The progress tracker
+ * @reversed: If progress should be reversed.
+ *
+ * Gets the progress through the current animation iteration, from [0, 1]. Use
+ * to interpolate between animation targets. If reverse is true each iteration
+ * will begin at 1 and end at 0.
+ *
+ * Returns: The progress value.
+ **/
+gdouble
+gtk_progress_tracker_get_progress (GtkProgressTracker *tracker,
+ gboolean reversed)
+{
+ gdouble progress, iteration;
+ guint64 iteration_cycle;
+
+ iteration = gtk_progress_tracker_get_iteration (tracker);
+ iteration_cycle = gtk_progress_tracker_get_iteration_cycle (tracker);
+
+ progress = iteration - iteration_cycle;
+ return reversed ? 1.0 - progress : progress;
+}
+
+/**
+ * gtk_progress_tracker_get_ease_out_cubic:
+ * @tracker: The progress tracker
+ * @reversed: If progress should be reversed before applying the ease function.
+ *
+ * Applies a simple ease out cubic function to the result of
+ * gtk_progress_tracker_get_progress().
+ *
+ * Returns: The eased progress value.
+ **/
+gdouble
+gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker,
+ gboolean reversed)
+{
+ gdouble progress = gtk_progress_tracker_get_progress (tracker, reversed);
+ return hdy_ease_out_cubic (progress);
+}
diff --git a/subprojects/libhandy/src/gtkprogresstrackerprivate.h b/subprojects/libhandy/src/gtkprogresstrackerprivate.h
new file mode 100644
index 0000000..fcce609
--- /dev/null
+++ b/subprojects/libhandy/src/gtkprogresstrackerprivate.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2016 Endless Mobile 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: Matthew Watson <mattdangerw@gmail.com>
+ */
+
+#ifndef __GTK_PROGRESS_TRACKER_PRIVATE_H__
+#define __GTK_PROGRESS_TRACKER_PRIVATE_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ GTK_PROGRESS_STATE_BEFORE,
+ GTK_PROGRESS_STATE_DURING,
+ GTK_PROGRESS_STATE_AFTER,
+} GtkProgressState;
+
+typedef struct _GtkProgressTracker GtkProgressTracker;
+
+struct _GtkProgressTracker
+{
+ gboolean is_running;
+ guint64 last_frame_time;
+ guint64 duration;
+ gdouble iteration;
+ gdouble iteration_count;
+};
+
+void gtk_progress_tracker_init_copy (GtkProgressTracker *source,
+ GtkProgressTracker *dest);
+
+void gtk_progress_tracker_start (GtkProgressTracker *tracker,
+ guint64 duration,
+ gint64 delay,
+ gdouble iteration_count);
+
+void gtk_progress_tracker_finish (GtkProgressTracker *tracker);
+
+void gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker,
+ guint64 frame_time);
+
+void gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker,
+ guint64 frame_time);
+
+GtkProgressState gtk_progress_tracker_get_state (GtkProgressTracker *tracker);
+
+gdouble gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker);
+
+guint64 gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker);
+
+gdouble gtk_progress_tracker_get_progress (GtkProgressTracker *tracker,
+ gboolean reverse);
+
+gdouble gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker,
+ gboolean reverse);
+
+G_END_DECLS
+
+#endif /* __GTK_PROGRESS_TRACKER_PRIVATE_H__ */
diff --git a/subprojects/libhandy/src/handy.gresources.xml b/subprojects/libhandy/src/handy.gresources.xml
new file mode 100644
index 0000000..b96444b
--- /dev/null
+++ b/subprojects/libhandy/src/handy.gresources.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/sm/puri/handy">
+ <file preprocess="xml-stripblanks">icons/avatar-default-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/hdy-expander-arrow-symbolic.svg</file>
+ <file compressed="true">themes/Adwaita.css</file>
+ <file compressed="true">themes/Adwaita-dark.css</file>
+ <file compressed="true">themes/fallback.css</file>
+ <file compressed="true">themes/HighContrast.css</file>
+ <file compressed="true">themes/HighContrastInverse.css</file>
+ <file compressed="true">themes/shared.css</file>
+ </gresource>
+ <gresource prefix="/sm/puri/handy/ui">
+ <file preprocess="xml-stripblanks">hdy-action-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-carousel.ui</file>
+ <file preprocess="xml-stripblanks">hdy-combo-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-expander-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-keypad.ui</file>
+ <file preprocess="xml-stripblanks">hdy-keypad-button.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-group.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-page.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-window.ui</file>
+ <file preprocess="xml-stripblanks">hdy-search-bar.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file>
+ </gresource>
+</gresources>
diff --git a/subprojects/libhandy/src/handy.h b/subprojects/libhandy/src/handy.h
new file mode 100644
index 0000000..1ea48a7
--- /dev/null
+++ b/subprojects/libhandy/src/handy.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#if !GTK_CHECK_VERSION(3, 22, 0)
+# error "libhandy requires gtk+-3.0 >= 3.22.0"
+#endif
+
+#if !GLIB_CHECK_VERSION(2, 50, 0)
+# error "libhandy requires glib-2.0 >= 2.50.0"
+#endif
+
+#define _HANDY_INSIDE
+
+#include "hdy-version.h"
+#include "hdy-action-row.h"
+#include "hdy-animation.h"
+#include "hdy-application-window.h"
+#include "hdy-avatar.h"
+#include "hdy-carousel.h"
+#include "hdy-carousel-indicator-dots.h"
+#include "hdy-carousel-indicator-lines.h"
+#include "hdy-clamp.h"
+#include "hdy-combo-row.h"
+#include "hdy-deck.h"
+#include "hdy-deprecation-macros.h"
+#include "hdy-enum-value-object.h"
+#include "hdy-expander-row.h"
+#include "hdy-header-bar.h"
+#include "hdy-header-group.h"
+#include "hdy-keypad.h"
+#include "hdy-leaflet.h"
+#include "hdy-main.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-preferences-group.h"
+#include "hdy-preferences-page.h"
+#include "hdy-preferences-row.h"
+#include "hdy-preferences-window.h"
+#include "hdy-search-bar.h"
+#include "hdy-squeezer.h"
+#include "hdy-swipe-group.h"
+#include "hdy-swipe-tracker.h"
+#include "hdy-swipeable.h"
+#include "hdy-title-bar.h"
+#include "hdy-types.h"
+#include "hdy-value-object.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-bar.h"
+#include "hdy-view-switcher-title.h"
+#include "hdy-window.h"
+#include "hdy-window-handle.h"
+
+#undef _HANDY_INSIDE
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-action-row.c b/subprojects/libhandy/src/hdy-action-row.c
new file mode 100644
index 0000000..95108ae
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.c
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-action-row.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-action-row
+ * @short_description: A #GtkListBox row used to present actions.
+ * @Title: HdyActionRow
+ *
+ * The #HdyActionRow widget can have a title, a subtitle and an icon. The row
+ * can receive additional widgets at its end, or prefix widgets at its start.
+ *
+ * It is convenient to present a preference and its related actions.
+ *
+ * #HdyActionRow is unactivatable by default, giving it an activatable widget
+ * will automatically make it activatable, but unsetting it won't change the
+ * row's activatability.
+ *
+ * # HdyActionRow as GtkBuildable
+ *
+ * The GtkWindow implementation of the GtkBuildable interface supports setting a
+ * child at its end by omitting the “type” attribute of a &lt;child&gt; element.
+ *
+ * It also supports setting a child as a prefix widget by specifying “prefix” as
+ * the “type” attribute of a &lt;child&gt; element.
+ *
+ * # CSS nodes
+ *
+ * #HdyActionRow has a main CSS node with name row.
+ *
+ * It contains the subnode box.header for its main horizontal box, and box.title
+ * for the vertical box containing the title and subtitle labels.
+ *
+ * It contains subnodes label.title and label.subtitle representing respectively
+ * the title label and subtitle label.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct
+{
+ GtkBox *header;
+ GtkImage *image;
+ GtkBox *prefixes;
+ GtkLabel *subtitle;
+ GtkBox *suffixes;
+ GtkLabel *title;
+ GtkBox *title_box;
+
+ GtkWidget *previous_parent;
+
+ gboolean use_underline;
+ GtkWidget *activatable_widget;
+} HdyActionRowPrivate;
+
+static void hdy_action_row_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyActionRow, hdy_action_row, HDY_TYPE_PREFERENCES_ROW,
+ G_ADD_PRIVATE (HdyActionRow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_action_row_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_ACTIVATABLE_WIDGET,
+ PROP_SUBTITLE,
+ PROP_USE_UNDERLINE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_ACTIVATED,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+row_activated_cb (HdyActionRow *self,
+ GtkListBoxRow *row)
+{
+ /* No need to use GTK_LIST_BOX_ROW() for a pointer comparison. */
+ if ((GtkListBoxRow *) self == row)
+ hdy_action_row_activate (self);
+}
+
+static void
+parent_cb (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self));
+
+ if (priv->previous_parent != NULL) {
+ g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self);
+ priv->previous_parent = NULL;
+ }
+
+ if (parent == NULL || !GTK_IS_LIST_BOX (parent))
+ return;
+
+ priv->previous_parent = parent;
+ g_signal_connect_swapped (parent, "row-activated", G_CALLBACK (row_activated_cb), self);
+}
+
+static void
+update_subtitle_visibility (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->subtitle),
+ gtk_label_get_text (priv->subtitle) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->subtitle), "") != 0);
+}
+
+static void
+hdy_action_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_action_row_get_icon_name (self));
+ break;
+ case PROP_ACTIVATABLE_WIDGET:
+ g_value_set_object (value, (GObject *) hdy_action_row_get_activatable_widget (self));
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_action_row_get_subtitle (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_action_row_get_use_underline (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_action_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_action_row_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_ACTIVATABLE_WIDGET:
+ hdy_action_row_set_activatable_widget (self, (GtkWidget*) g_value_get_object (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_action_row_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_action_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_action_row_dispose (GObject *object)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->previous_parent != NULL) {
+ g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self);
+ priv->previous_parent = NULL;
+ }
+
+ G_OBJECT_CLASS (hdy_action_row_parent_class)->dispose (object);
+}
+
+static void
+hdy_action_row_show_all (GtkWidget *widget)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (widget);
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->prefixes),
+ (GtkCallback) gtk_widget_show_all,
+ NULL);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->suffixes),
+ (GtkCallback) gtk_widget_show_all,
+ NULL);
+
+ GTK_WIDGET_CLASS (hdy_action_row_parent_class)->show_all (widget);
+}
+
+static void
+hdy_action_row_destroy (GtkWidget *widget)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (widget);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->header) {
+ gtk_widget_destroy (GTK_WIDGET (priv->header));
+ priv->header = NULL;
+ }
+
+ hdy_action_row_set_activatable_widget (self, NULL);
+
+ priv->prefixes = NULL;
+ priv->suffixes = NULL;
+
+ GTK_WIDGET_CLASS (hdy_action_row_parent_class)->destroy (widget);
+}
+
+static void
+hdy_action_row_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ /* When constructing the widget, we want the box to be added as the child of
+ * the GtkListBoxRow, as an implementation detail.
+ */
+ if (priv->header == NULL)
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->add (container, child);
+ else {
+ gtk_container_add (GTK_CONTAINER (priv->suffixes), child);
+ gtk_widget_show (GTK_WIDGET (priv->suffixes));
+ }
+}
+
+static void
+hdy_action_row_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->header))
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->remove (container, child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes))
+ gtk_container_remove (GTK_CONTAINER (priv->prefixes), child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->suffixes), child);
+}
+
+typedef struct {
+ HdyActionRow *row;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (data->row);
+
+ if (widget != (GtkWidget *) priv->image &&
+ widget != (GtkWidget *) priv->prefixes &&
+ widget != (GtkWidget *) priv->suffixes &&
+ widget != (GtkWidget *) priv->title_box)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_action_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.row = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ if (priv->prefixes)
+ GTK_CONTAINER_GET_CLASS (priv->prefixes)->forall (GTK_CONTAINER (priv->prefixes), include_internals, for_non_internal_child, &data);
+ if (priv->suffixes)
+ GTK_CONTAINER_GET_CLASS (priv->suffixes)->forall (GTK_CONTAINER (priv->suffixes), include_internals, for_non_internal_child, &data);
+ if (priv->header)
+ GTK_CONTAINER_GET_CLASS (priv->header)->forall (GTK_CONTAINER (priv->header), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_action_row_activate_real (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->activatable_widget)
+ gtk_widget_mnemonic_activate (priv->activatable_widget, FALSE);
+
+ g_signal_emit (self, signals[SIGNAL_ACTIVATED], 0);
+}
+
+static void
+hdy_action_row_class_init (HdyActionRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_action_row_get_property;
+ object_class->set_property = hdy_action_row_set_property;
+ object_class->dispose = hdy_action_row_dispose;
+
+ widget_class->destroy = hdy_action_row_destroy;
+ widget_class->show_all = hdy_action_row_show_all;
+
+ container_class->add = hdy_action_row_add;
+ container_class->remove = hdy_action_row_remove;
+ container_class->forall = hdy_action_row_forall;
+
+ klass->activate = hdy_action_row_activate_real;
+
+ /**
+ * HdyActionRow:icon-name:
+ *
+ * The icon name for this row.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyActionRow:activatable-widget:
+ *
+ * The activatable widget for this row.
+ *
+ * Since: 0.0.7
+ */
+ props[PROP_ACTIVATABLE_WIDGET] =
+ g_param_spec_object ("activatable-widget",
+ _("Activatable widget"),
+ _("The widget to be activated when the row is activated"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyActionRow:subtitle:
+ *
+ * The subtitle for this row.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("Subtitle"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyActionRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title and subtitle labels
+ * indicates a mnemonic.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyActionRow::activated:
+ * @self: The #HdyActionRow instance
+ *
+ * This signal is emitted after the row has been activated.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_ACTIVATED] =
+ g_signal_new ("activated",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-action-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, header);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, prefixes);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, subtitle);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, suffixes);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title_box);
+}
+
+static gboolean
+string_is_not_empty (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ const gchar *string = g_value_get_string (from_value);
+
+ g_value_set_boolean (to_value, string != NULL && g_strcmp0 (string, "") != 0);
+
+ return TRUE;
+}
+
+static void
+hdy_action_row_init (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ g_object_bind_property_full (self, "title", priv->title, "visible", G_BINDING_SYNC_CREATE,
+ string_is_not_empty, NULL, NULL, NULL);
+
+ update_subtitle_visibility (self);
+
+ g_signal_connect (self, "notify::parent", G_CALLBACK (parent_cb), NULL);
+
+}
+
+static void
+hdy_action_row_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (buildable);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->header == NULL || !type)
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+ else if (type && strcmp (type, "prefix") == 0)
+ hdy_action_row_add_prefix (self, GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type);
+}
+
+static void
+hdy_action_row_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+ iface->add_child = hdy_action_row_buildable_add_child;
+}
+
+/**
+ * hdy_action_row_new:
+ *
+ * Creates a new #HdyActionRow.
+ *
+ * Returns: a new #HdyActionRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_action_row_new (void)
+{
+ return g_object_new (HDY_TYPE_ACTION_ROW, NULL);
+}
+
+/**
+ * hdy_action_row_get_subtitle:
+ * @self: a #HdyActionRow
+ *
+ * Gets the subtitle for @self.
+ *
+ * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL.
+ *
+ * Since: 0.0.6
+ */
+const gchar *
+hdy_action_row_get_subtitle (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return gtk_label_get_text (priv->subtitle);
+}
+
+/**
+ * hdy_action_row_set_subtitle:
+ * @self: a #HdyActionRow
+ * @subtitle: (nullable): the subtitle
+ *
+ * Sets the subtitle for @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_subtitle (HdyActionRow *self,
+ const gchar *subtitle)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_text (priv->subtitle), subtitle) == 0)
+ return;
+
+ gtk_label_set_text (priv->subtitle, subtitle);
+ gtk_widget_set_visible (GTK_WIDGET (priv->subtitle),
+ subtitle != NULL && g_strcmp0 (subtitle, "") != 0);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_action_row_get_icon_name:
+ * @self: a #HdyActionRow
+ *
+ * Gets the icon name for @self.
+ *
+ * Returns: the icon name for @self.
+ *
+ * Since: 0.0.6
+ */
+const gchar *
+hdy_action_row_get_icon_name (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+ const gchar *icon_name;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_image_get_icon_name (priv->image, &icon_name, NULL);
+
+ return icon_name;
+}
+
+/**
+ * hdy_action_row_set_icon_name:
+ * @self: a #HdyActionRow
+ * @icon_name: the icon name
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_icon_name (HdyActionRow *self,
+ const gchar *icon_name)
+{
+ HdyActionRowPrivate *priv;
+ const gchar *old_icon_name;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_image_get_icon_name (priv->image, &old_icon_name, NULL);
+ if (g_strcmp0 (old_icon_name, icon_name) == 0)
+ return;
+
+ gtk_image_set_from_icon_name (priv->image, icon_name, GTK_ICON_SIZE_INVALID);
+ gtk_widget_set_visible (GTK_WIDGET (priv->image),
+ icon_name != NULL && g_strcmp0 (icon_name, "") != 0);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_action_row_get_activatable_widget:
+ * @self: a #HdyActionRow
+ *
+ * Gets the widget activated when @self is activated.
+ *
+ * Returns: (nullable) (transfer none): the widget activated when @self is
+ * activated, or %NULL if none has been set.
+ *
+ * Since: 0.0.7
+ */
+GtkWidget *
+hdy_action_row_get_activatable_widget (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return priv->activatable_widget;
+}
+
+static void
+activatable_widget_weak_notify (gpointer data,
+ GObject *where_the_object_was)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (data);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ priv->activatable_widget = NULL;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]);
+}
+
+/**
+ * hdy_action_row_set_activatable_widget:
+ * @self: a #HdyActionRow
+ * @widget: (nullable): the target #GtkWidget, or %NULL to unset
+ *
+ * Sets the widget to activate when @self is activated, either by clicking
+ * on it, by calling hdy_action_row_activate(), or via mnemonics in the title or
+ * the subtitle. See the “use_underline” property to enable mnemonics.
+ *
+ * The target widget will be activated by emitting the
+ * GtkWidget::mnemonic-activate signal on it.
+ *
+ * Since: 0.0.7
+ */
+void
+hdy_action_row_set_activatable_widget (HdyActionRow *self,
+ GtkWidget *widget)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+ g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->activatable_widget == widget)
+ return;
+
+ if (priv->activatable_widget)
+ g_object_weak_unref (G_OBJECT (priv->activatable_widget),
+ activatable_widget_weak_notify,
+ self);
+
+ priv->activatable_widget = widget;
+
+ if (priv->activatable_widget != NULL) {
+ g_object_weak_ref (G_OBJECT (priv->activatable_widget),
+ activatable_widget_weak_notify,
+ self);
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), TRUE);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]);
+}
+
+/**
+ * hdy_action_row_get_use_underline:
+ * @self: a #HdyActionRow
+ *
+ * Gets whether an embedded underline in the text of the title and subtitle
+ * labels indicates a mnemonic. See hdy_action_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title and subtitle labels
+ * indicates the mnemonic accelerator keys.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_action_row_get_use_underline (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), FALSE);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return priv->use_underline;
+}
+
+/**
+ * hdy_action_row_set_use_underline:
+ * @self: a #HdyActionRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title and subtitle labels indicates
+ * the next character should be used for the mnemonic accelerator key.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_use_underline (HdyActionRow *self,
+ gboolean use_underline)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->use_underline == !!use_underline)
+ return;
+
+ priv->use_underline = !!use_underline;
+ hdy_preferences_row_set_use_underline (HDY_PREFERENCES_ROW (self), priv->use_underline);
+ gtk_label_set_use_underline (priv->title, priv->use_underline);
+ gtk_label_set_use_underline (priv->subtitle, priv->use_underline);
+ gtk_label_set_mnemonic_widget (priv->title, GTK_WIDGET (self));
+ gtk_label_set_mnemonic_widget (priv->subtitle, GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]);
+}
+
+/**
+ * hdy_action_row_add_prefix:
+ * @self: a #HdyActionRow
+ * @widget: the prefix widget
+ *
+ * Adds a prefix widget to @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_add_prefix (HdyActionRow *self,
+ GtkWidget *widget)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->prefixes));
+}
+
+void
+hdy_action_row_activate (HdyActionRow *self)
+{
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ HDY_ACTION_ROW_GET_CLASS (self)->activate (self);
+}
diff --git a/subprojects/libhandy/src/hdy-action-row.h b/subprojects/libhandy/src/hdy-action-row.h
new file mode 100644
index 0000000..7b5dd53
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include "hdy-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_ACTION_ROW (hdy_action_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyActionRow, hdy_action_row, HDY, ACTION_ROW, HdyPreferencesRow)
+
+/**
+ * HdyActionRowClass
+ * @parent_class: The parent class
+ * @activate: Activates the row to trigger its main action.
+ */
+struct _HdyActionRowClass
+{
+ GtkListBoxRowClass parent_class;
+
+ void (*activate) (HdyActionRow *self);
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_action_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_action_row_get_subtitle (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_subtitle (HdyActionRow *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_action_row_get_icon_name (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_icon_name (HdyActionRow *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_action_row_get_activatable_widget (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_activatable_widget (HdyActionRow *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_action_row_get_use_underline (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_use_underline (HdyActionRow *self,
+ gboolean use_underline);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_add_prefix (HdyActionRow *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_activate (HdyActionRow *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-action-row.ui b/subprojects/libhandy/src/hdy-action-row.ui
new file mode 100644
index 0000000..ff54c15
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyActionRow" parent="HdyPreferencesRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox" id="header">
+ <property name="can_focus">False</property>
+ <property name="spacing">12</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="header"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="prefixes">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="no_show_all">True</property>
+ <property name="pixel_size">32</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="title_box">
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="no_show_all">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="title"/>
+ </style>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="label" bind-source="HdyActionRow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="subtitle"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="suffixes">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-animation-private.h b/subprojects/libhandy/src/hdy-animation-private.h
new file mode 100644
index 0000000..f31002a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation-private.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-animation.h"
+
+G_BEGIN_DECLS
+
+gdouble hdy_lerp (gdouble a, gdouble b, gdouble t);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-animation.c b/subprojects/libhandy/src/hdy-animation.c
new file mode 100644
index 0000000..ce5bf64
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-animation-private.h"
+
+/**
+ * SECTION:hdy-animation
+ * @short_description: Animation helpers
+ * @title: Animation Helpers
+ *
+ * Animation helpers.
+ *
+ * Since: 0.0.11
+ */
+
+/**
+ * hdy_get_enable_animations:
+ * @widget: a #GtkWidget
+ *
+ * Returns whether animations are enabled for that widget. This should be used
+ * when implementing an animated widget to know whether to animate it or not.
+ *
+ * Returns: %TRUE if animations are enabled for @widget.
+ *
+ * Since: 0.0.11
+ */
+gboolean
+hdy_get_enable_animations (GtkWidget *widget)
+{
+ gboolean enable_animations = TRUE;
+
+ g_assert (GTK_IS_WIDGET (widget));
+
+ g_object_get (gtk_widget_get_settings (widget),
+ "gtk-enable-animations", &enable_animations,
+ NULL);
+
+ return enable_animations;
+}
+
+/**
+ * hdy_lerp: (skip)
+ * @a: the start
+ * @b: the end
+ * @t: the interpolation rate
+ *
+ * Computes the linear interpolation between @a and @b for @t.
+ *
+ * Returns: the linear interpolation between @a and @b for @t.
+ *
+ * Since: 0.0.11
+ */
+gdouble
+hdy_lerp (gdouble a, gdouble b, gdouble t)
+{
+ return a * (1.0 - t) + b * t;
+}
+
+/* From clutter-easing.c, based on Robert Penner's
+ * infamous easing equations, MIT license.
+ */
+
+/**
+ * hdy_ease_out_cubic:
+ * @t: the term
+ *
+ * Computes the ease out for @t.
+ *
+ * Returns: the ease out for @t.
+ *
+ * Since: 0.0.11
+ */
+gdouble
+hdy_ease_out_cubic (gdouble t)
+{
+ gdouble p = t - 1;
+ return p * p * p + 1;
+}
diff --git a/subprojects/libhandy/src/hdy-animation.h b/subprojects/libhandy/src/hdy-animation.h
new file mode 100644
index 0000000..5af34c0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_get_enable_animations (GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_ease_out_cubic (gdouble t);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-application-window.c b/subprojects/libhandy/src/hdy-application-window.c
new file mode 100644
index 0000000..d3979cf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-application-window.c
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-application-window.h"
+#include "hdy-window-mixin-private.h"
+
+/**
+ * SECTION:hdy-application-window
+ * @short_description: A freeform application window.
+ * @title: HdyApplicationWindow
+ * @See_also: #HdyHeaderBar, #HdyWindow, #HdyWindowHandle
+ *
+ * HdyApplicationWindow is a #GtkApplicationWindow subclass providing the same
+ * features as #HdyWindow.
+ *
+ * See #HdyWindow for details.
+ *
+ * Using gtk_application_set_app_menu() and gtk_application_set_menubar() is
+ * not supported and may result in visual glitches.
+ *
+ * Since: 1.0
+ */
+
+typedef struct
+{
+ HdyWindowMixin *mixin;
+} HdyApplicationWindowPrivate;
+
+static void hdy_application_window_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyApplicationWindow, hdy_application_window, GTK_TYPE_APPLICATION_WINDOW,
+ G_ADD_PRIVATE (HdyApplicationWindow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_application_window_buildable_init))
+
+#define HDY_GET_WINDOW_MIXIN(obj) (((HdyApplicationWindowPrivate *) hdy_application_window_get_instance_private (HDY_APPLICATION_WINDOW (obj)))->mixin)
+
+static void
+hdy_application_window_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_application_window_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_application_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container),
+ include_internals,
+ callback,
+ callback_data);
+}
+
+static gboolean
+hdy_application_window_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr);
+}
+
+static void
+hdy_application_window_destroy (GtkWidget *widget)
+{
+ hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget));
+}
+
+static void
+hdy_application_window_finalize (GObject *object)
+{
+ HdyApplicationWindow *self = (HdyApplicationWindow *)object;
+ HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self);
+
+ g_clear_object (&priv->mixin);
+
+ G_OBJECT_CLASS (hdy_application_window_parent_class)->finalize (object);
+}
+
+static void
+hdy_application_window_class_init (HdyApplicationWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_application_window_finalize;
+ widget_class->draw = hdy_application_window_draw;
+ widget_class->destroy = hdy_application_window_destroy;
+ container_class->add = hdy_application_window_add;
+ container_class->remove = hdy_application_window_remove;
+ container_class->forall = hdy_application_window_forall;
+}
+
+static void
+hdy_application_window_init (HdyApplicationWindow *self)
+{
+ HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self);
+
+ priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self),
+ GTK_WINDOW_CLASS (hdy_application_window_parent_class));
+
+ gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (self), FALSE);
+}
+
+static void
+hdy_application_window_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable),
+ builder,
+ child,
+ type);
+}
+
+static void
+hdy_application_window_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_application_window_buildable_add_child;
+}
+
+/**
+ * hdy_application_window_new:
+ *
+ * Creates a new #HdyApplicationWindow.
+ *
+ * Returns: (transfer full): a newly created #HdyApplicationWindow
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_application_window_new (void)
+{
+ return g_object_new (HDY_TYPE_APPLICATION_WINDOW,
+ NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-application-window.h b/subprojects/libhandy/src/hdy-application-window.h
new file mode 100644
index 0000000..ed01eb1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-application-window.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_APPLICATION_WINDOW (hdy_application_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyApplicationWindow, hdy_application_window, HDY, APPLICATION_WINDOW, GtkApplicationWindow)
+
+struct _HdyApplicationWindowClass
+{
+ GtkApplicationWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_application_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-avatar.c b/subprojects/libhandy/src/hdy-avatar.c
new file mode 100644
index 0000000..9dcdcdf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-avatar.c
@@ -0,0 +1,811 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ * Copyright (C) 2020 Felipe Borges
+ *
+ * Authors:
+ * Felipe Borges <felipeborges@gnome.org>
+ * Julian Sparber <julian@sparber.net>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ */
+
+#include "config.h"
+#include <math.h>
+
+#include "hdy-avatar.h"
+#include "hdy-cairo-private.h"
+
+#define NUMBER_OF_COLORS 14
+/**
+ * SECTION:hdy-avatar
+ * @short_description: A widget displaying an image, with a generated fallback.
+ * @Title: HdyAvatar
+ *
+ * #HdyAvatar is a widget to display a round avatar.
+ * A provided image is made round before displaying, if no image is given this
+ * widget generates a round fallback with the initials of the #HdyAvatar:text
+ * on top of a colord background.
+ * The color is picked based on the hash of the #HdyAvatar:text.
+ * If #HdyAvatar:show-initials is set to %FALSE, `avatar-default-symbolic` is
+ * shown in place of the initials.
+ * Use hdy_avatar_set_image_load_func () to set a custom image.
+ * Create a #HdyAvatarImageLoadFunc similar to this example:
+ *
+ * |[<!-- language="C" -->
+ * static GdkPixbuf *
+ * image_load_func (gint size, gpointer user_data)
+ * {
+ * g_autoptr (GError) error = NULL;
+ * g_autoptr (GdkPixbuf) pixbuf = NULL;
+ * g_autofree gchar *file = gtk_file_chooser_get_filename ("avatar.png");
+ * gint width, height;
+ *
+ * gdk_pixbuf_get_file_info (file, &width, &height);
+ *
+ * pixbuf = gdk_pixbuf_new_from_file_at_scale (file,
+ * (width <= height) ? size : -1,
+ * (width >= height) ? size : -1,
+ * TRUE,
+ * error);
+ * if (error != NULL) {
+ * g_critical ("Failed to create pixbuf from file: %s", error->message);
+ *
+ * return NULL;
+ * }
+ *
+ * return pixbuf;
+ * }
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyAvatar has a single CSS node with name avatar.
+ *
+ */
+
+struct _HdyAvatar
+{
+ GtkDrawingArea parent_instance;
+
+ gchar *icon_name;
+ gchar *text;
+ PangoLayout *layout;
+ gboolean show_initials;
+ guint color_class;
+ gint size;
+ cairo_surface_t *round_image;
+
+ HdyAvatarImageLoadFunc load_image_func;
+ gpointer load_image_func_target;
+ GDestroyNotify load_image_func_target_destroy_notify;
+};
+
+G_DEFINE_TYPE (HdyAvatar, hdy_avatar, GTK_TYPE_DRAWING_AREA);
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_TEXT,
+ PROP_SHOW_INITIALS,
+ PROP_SIZE,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+static cairo_surface_t *
+round_image (GdkPixbuf *pixbuf,
+ gdouble size)
+{
+ g_autoptr (cairo_surface_t) surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
+ g_autoptr (cairo_t) cr = cairo_create (surface);
+
+ /* Clip a circle */
+ cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0, 2 * G_PI);
+ cairo_clip (cr);
+ cairo_new_path (cr);
+
+ gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0);
+ cairo_paint (cr);
+
+ return g_steal_pointer (&surface);
+}
+
+static gchar *
+extract_initials_from_text (const gchar *text)
+{
+ GString *initials;
+ g_autofree gchar *p = g_utf8_strup (text, -1);
+ g_autofree gchar *normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE);
+ gunichar unichar;
+ gchar *q = NULL;
+
+ if (normalized == NULL)
+ return NULL;
+
+ initials = g_string_new ("");
+
+ unichar = g_utf8_get_char (normalized);
+ g_string_append_unichar (initials, unichar);
+
+ q = g_utf8_strrchr (normalized, -1, ' ');
+ if (q != NULL && g_utf8_next_char (q) != NULL) {
+ q = g_utf8_next_char (q);
+
+ unichar = g_utf8_get_char (q);
+ g_string_append_unichar (initials, unichar);
+ }
+
+ return g_string_free (initials, FALSE);
+}
+
+static void
+update_custom_image (HdyAvatar *self)
+{
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ gint scale_factor;
+ gint size;
+ gboolean was_custom = FALSE;
+
+ if (self->round_image != NULL) {
+ g_clear_pointer (&self->round_image, cairo_surface_destroy);
+ was_custom = TRUE;
+ }
+
+ if (self->load_image_func != NULL) {
+ scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
+ size = MIN (gtk_widget_get_allocated_width (GTK_WIDGET (self)),
+ gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+ pixbuf = self->load_image_func (size * scale_factor, self->load_image_func_target);
+ if (pixbuf != NULL) {
+ self->round_image = round_image (pixbuf, (gdouble) size * scale_factor);
+ cairo_surface_set_device_scale (self->round_image, scale_factor, scale_factor);
+ }
+ }
+
+ if (was_custom || self->round_image != NULL)
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+set_class_color (HdyAvatar *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ g_autofree GRand *rand = NULL;
+ g_autofree gchar *new_class = NULL;
+ g_autofree gchar *old_class = g_strdup_printf ("color%d", self->color_class);
+
+ gtk_style_context_remove_class (context, old_class);
+
+ if (self->text == NULL || strlen (self->text) == 0) {
+ /* Use a random color if we don't have a text */
+ rand = g_rand_new ();
+ self->color_class = g_rand_int_range (rand, 1, NUMBER_OF_COLORS);
+ } else {
+ self->color_class = (g_str_hash (self->text) % NUMBER_OF_COLORS) + 1;
+ }
+
+ new_class = g_strdup_printf ("color%d", self->color_class);
+ gtk_style_context_add_class (context, new_class);
+}
+
+static void
+set_class_contrasted (HdyAvatar *self, gint size)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ if (size < 25)
+ gtk_style_context_add_class (context, "contrasted");
+ else
+ gtk_style_context_remove_class (context, "contrasted");
+}
+
+static void
+clear_pango_layout (HdyAvatar *self)
+{
+ g_clear_object (&self->layout);
+}
+
+static void
+ensure_pango_layout (HdyAvatar *self)
+{
+ g_autofree gchar *initials = NULL;
+
+ if (self->layout != NULL || self->text == NULL || strlen (self->text) == 0)
+ return;
+
+ initials = extract_initials_from_text (self->text);
+ self->layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), initials);
+}
+
+static void
+set_font_size (HdyAvatar *self,
+ gint size)
+{
+ GtkStyleContext *context;
+ PangoFontDescription *font_desc;
+ gint width, height;
+ gdouble padding;
+ gdouble sqr_size;
+ gdouble max_size;
+ gdouble new_font_size;
+
+ if (self->round_image != NULL || self->layout == NULL)
+ return;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ gtk_style_context_get (context, gtk_style_context_get_state (context),
+ "font", &font_desc, NULL);
+
+ pango_layout_set_font_description (self->layout, font_desc);
+ pango_layout_get_pixel_size (self->layout, &width, &height);
+
+ /* This is the size of the biggest square fitting inside the circle */
+ sqr_size = (gdouble)size / 1.4142;
+ /* The padding has to be a function of the overall size.
+ * The 0.4 is how steep the linear function grows and the -5 is just
+ * an adjustment for smaller sizes which doesn't have a big impact on bigger sizes.
+ * Make also sure we don't have a negative padding */
+ padding = MAX (size * 0.4 - 5, 0);
+ max_size = sqr_size - padding;
+ new_font_size = (gdouble)height * (max_size / (gdouble)width);
+
+ font_desc = pango_font_description_copy (font_desc);
+ pango_font_description_set_absolute_size (font_desc,
+ CLAMP (new_font_size, 0, max_size) * PANGO_SCALE);
+ pango_layout_set_font_description (self->layout, font_desc);
+ pango_font_description_free (font_desc);
+}
+
+static void
+hdy_avatar_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ switch (property_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_avatar_get_icon_name (self));
+ break;
+
+ case PROP_TEXT:
+ g_value_set_string (value, hdy_avatar_get_text (self));
+ break;
+
+ case PROP_SHOW_INITIALS:
+ g_value_set_boolean (value, hdy_avatar_get_show_initials (self));
+ break;
+
+ case PROP_SIZE:
+ g_value_set_int (value, hdy_avatar_get_size (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_avatar_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ switch (property_id) {
+ case PROP_ICON_NAME:
+ hdy_avatar_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_TEXT:
+ hdy_avatar_set_text (self, g_value_get_string (value));
+ break;
+
+ case PROP_SHOW_INITIALS:
+ hdy_avatar_set_show_initials (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SIZE:
+ hdy_avatar_set_size (self, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_avatar_finalize (GObject *object)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ g_clear_pointer (&self->icon_name, g_free);
+ g_clear_pointer (&self->text, g_free);
+ g_clear_pointer (&self->round_image, cairo_surface_destroy);
+ g_clear_object (&self->layout);
+
+ if (self->load_image_func_target_destroy_notify != NULL)
+ self->load_image_func_target_destroy_notify (self->load_image_func_target);
+
+ G_OBJECT_CLASS (hdy_avatar_parent_class)->finalize (object);
+}
+
+static gboolean
+hdy_avatar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyAvatar *self = HDY_AVATAR (widget);
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gint width = gtk_widget_get_allocated_width (widget);
+ gint height = gtk_widget_get_allocated_height (widget);
+ gint size = MIN (width, height);
+ gdouble x = (gdouble)(width - size) / 2.0;
+ gdouble y = (gdouble)(height - size) / 2.0;
+ const gchar *icon_name;
+ gint scale;
+ GdkRGBA color;
+ g_autoptr (GtkIconInfo) icon = NULL;
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autoptr (cairo_surface_t) surface = NULL;
+
+ set_class_contrasted (HDY_AVATAR (widget), size);
+
+ gtk_render_frame (context, cr, x, y, size, size);
+
+ if (self->round_image) {
+ cairo_set_source_surface (cr, self->round_image, x, y);
+ cairo_paint (cr);
+
+ return FALSE;
+ }
+
+ gtk_render_background (context, cr, x, y, size, size);
+ ensure_pango_layout (HDY_AVATAR (widget));
+
+ if (self->show_initials && self->layout != NULL) {
+ set_font_size (HDY_AVATAR (widget), size);
+ pango_layout_get_pixel_size (self->layout, &width, &height);
+
+ gtk_render_layout (context, cr,
+ ((gdouble)(size - width) / 2.0) + x,
+ ((gdouble)(size - height) / 2.0) + y,
+ self->layout);
+
+ return FALSE;
+ }
+
+ icon_name = self->icon_name && *self->icon_name != '\0' ?
+ self->icon_name : "avatar-default-symbolic";
+ scale = gtk_widget_get_scale_factor (widget);
+ icon = gtk_icon_theme_lookup_icon_for_scale (gtk_icon_theme_get_default (),
+ icon_name,
+ size / 2, scale,
+ GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
+ if (icon == NULL) {
+ g_critical ("Failed to load icon `%s'", icon_name);
+
+ return FALSE;
+ }
+
+ gtk_style_context_get_color (context, gtk_style_context_get_state (context), &color);
+ pixbuf = gtk_icon_info_load_symbolic (icon, &color, NULL, NULL, NULL, NULL, &error);
+ if (error != NULL) {
+ g_critical ("Failed to load icon `%s': %s", icon_name, error->message);
+
+ return FALSE;
+ }
+
+ surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale,
+ gtk_widget_get_window (widget));
+
+ width = cairo_image_surface_get_width (surface);
+ height = cairo_image_surface_get_height (surface);
+ gtk_render_icon_surface (context, cr, surface,
+ (((gdouble)size - ((gdouble)width / (gdouble)scale)) / 2.0) + x,
+ (((gdouble)size - ((gdouble)height / (gdouble)scale)) / 2.0) + y);
+
+ return FALSE;
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_avatar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdyAvatar *self = HDY_AVATAR (widget);
+
+ if (minimum)
+ *minimum = self->size;
+ if (natural)
+ *natural = self->size;
+}
+
+static void
+hdy_avatar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static GtkSizeRequestMode
+hdy_avatar_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+hdy_avatar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkAllocation clip;
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ GTK_WIDGET_CLASS (hdy_avatar_parent_class)->size_allocate (widget, allocation);
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_avatar_class_init (HdyAvatarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_avatar_finalize;
+
+ object_class->set_property = hdy_avatar_set_property;
+ object_class->get_property = hdy_avatar_get_property;
+
+ widget_class->draw = hdy_avatar_draw;
+ widget_class->get_request_mode = hdy_avatar_get_request_mode;
+ widget_class->get_preferred_width = hdy_avatar_get_preferred_width;
+ widget_class->get_preferred_height = hdy_avatar_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_avatar_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_avatar_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_avatar_size_allocate;
+
+ /**
+ * HdyAvatar:size:
+ *
+ * The avatar size of the avatar.
+ */
+ props[PROP_SIZE] =
+ g_param_spec_int ("size",
+ "Size",
+ "The size of the avatar",
+ -1, INT_MAX, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:icon-name:
+ *
+ * The name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ * If no name is set, the avatar-default-symbolic icon will be used.
+ * If the name doesn't match a valid icon, it is an error and no icon will be
+ * displayed.
+ * If the icon theme is changed, the image will be updated automatically.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon name",
+ "The name of the icon from the icon theme",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:text:
+ *
+ * The text used for the initials and for generating the color.
+ * If #HdyAvatar:show-initials is %FALSE it's only used to generate the color.
+ */
+ props[PROP_TEXT] =
+ g_param_spec_string ("text",
+ "Text",
+ "The text used to generate the color and the initials",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:show_initials:
+ *
+ * Whether to show the initials or the fallback icon on the generated avatar.
+ */
+ props[PROP_SHOW_INITIALS] =
+ g_param_spec_boolean ("show-initials",
+ "Show initials",
+ "Whether to show the initials",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "avatar");
+}
+
+static void
+hdy_avatar_init (HdyAvatar *self)
+{
+ set_class_color (self);
+ g_signal_connect (self, "notify::scale-factor", G_CALLBACK (update_custom_image), NULL);
+ g_signal_connect (self, "size-allocate", G_CALLBACK (update_custom_image), NULL);
+ g_signal_connect (self, "screen-changed", G_CALLBACK (clear_pango_layout), NULL);
+}
+
+/**
+ * hdy_avatar_new:
+ * @size: The size of the avatar
+ * @text: (nullable): The text used to generate the color and initials if
+ * @show_initials is %TRUE. The color is selected at random if @text is empty.
+ * @show_initials: whether to show the initials or the fallback icon on
+ * top of the color generated based on @text.
+ *
+ * Creates a new #HdyAvatar.
+ *
+ * Returns: the newly created #HdyAvatar
+ */
+GtkWidget *
+hdy_avatar_new (gint size,
+ const gchar *text,
+ gboolean show_initials)
+{
+ return g_object_new (HDY_TYPE_AVATAR,
+ "size", size,
+ "text", text,
+ "show-initials", show_initials,
+ NULL);
+}
+
+/**
+ * hdy_avatar_get_icon_name:
+ * @self: a #HdyAvatar
+ *
+ * Gets the name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ *
+ * Returns: (nullable) (transfer none): the name of the icon from the icon theme.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_avatar_get_icon_name (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
+
+ return self->icon_name;
+}
+
+/**
+ * hdy_avatar_set_icon_name:
+ * @self: a #HdyAvatar
+ * @icon_name: (nullable): the name of the icon from the icon theme
+ *
+ * Sets the name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ * If no name is set, the avatar-default-symbolic icon will be used.
+ * If the name doesn't match a valid icon, it is an error and no icon will be
+ * displayed.
+ * If the icon theme is changed, the image will be updated automatically.
+ *
+ * Since: 1.0
+ */
+void
+hdy_avatar_set_icon_name (HdyAvatar *self,
+ const gchar *icon_name)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (g_strcmp0 (self->icon_name, icon_name) == 0)
+ return;
+
+ g_clear_pointer (&self->icon_name, g_free);
+ self->icon_name = g_strdup (icon_name);
+
+ if (!self->round_image &&
+ (!self->show_initials || self->layout == NULL))
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_avatar_get_text:
+ * @self: a #HdyAvatar
+ *
+ * Get the text used to generate the fallback initials and color
+ *
+ * Returns: (nullable) (transfer none): returns the text used to generate
+ * the fallback initials. This is the internal string used by
+ * the #HdyAvatar, and must not be modified.
+ */
+const gchar *
+hdy_avatar_get_text (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
+
+ return self->text;
+}
+
+/**
+ * hdy_avatar_set_text:
+ * @self: a #HdyAvatar
+ * @text: (nullable): the text used to get the initials and color
+ *
+ * Set the text used to generate the fallback initials color
+ */
+void
+hdy_avatar_set_text (HdyAvatar *self,
+ const gchar *text)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (g_strcmp0 (self->text, text) == 0)
+ return;
+
+ g_clear_pointer (&self->text, g_free);
+ self->text = g_strdup (text);
+
+ clear_pango_layout (self);
+ set_class_color (self);
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TEXT]);
+}
+
+/**
+ * hdy_avatar_get_show_initials:
+ * @self: a #HdyAvatar
+ *
+ * Returns whether initials are used for the fallback or the icon.
+ *
+ * Returns: %TRUE if the initials are used for the fallback.
+ */
+gboolean
+hdy_avatar_get_show_initials (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), FALSE);
+
+ return self->show_initials;
+}
+
+/**
+ * hdy_avatar_set_show_initials:
+ * @self: a #HdyAvatar
+ * @show_initials: whether the initials should be shown on the fallback avatar
+ * or the icon.
+ *
+ * Sets whether the initials should be shown on the fallback avatar or the icon.
+ */
+void
+hdy_avatar_set_show_initials (HdyAvatar *self,
+ gboolean show_initials)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (self->show_initials == show_initials)
+ return;
+
+ self->show_initials = show_initials;
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_INITIALS]);
+}
+
+/**
+ * hdy_avatar_set_image_load_func:
+ * @self: a #HdyAvatar
+ * @load_image: (closure user_data) (nullable): callback to set a custom image
+ * @user_data: (nullable): user data passed to @load_image
+ * @destroy: (nullable): destroy notifier for @user_data
+ *
+ * A callback which is called when the custom image need to be reloaded for some
+ * reason (e.g. scale-factor changes).
+ */
+void
+hdy_avatar_set_image_load_func (HdyAvatar *self,
+ HdyAvatarImageLoadFunc load_image,
+ gpointer user_data,
+ GDestroyNotify destroy)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+ g_return_if_fail (user_data != NULL || (user_data == NULL && destroy == NULL));
+
+ if (self->load_image_func_target_destroy_notify != NULL)
+ self->load_image_func_target_destroy_notify (self->load_image_func_target);
+
+ self->load_image_func = load_image;
+ self->load_image_func_target = user_data;
+ self->load_image_func_target_destroy_notify = destroy;
+
+ update_custom_image (self);
+}
+
+/**
+ * hdy_avatar_get_size:
+ * @self: a #HdyAvatar
+ *
+ * Returns the size of the avatar.
+ *
+ * Returns: the size of the avatar.
+ */
+gint
+hdy_avatar_get_size (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), 0);
+
+ return self->size;
+}
+
+/**
+ * hdy_avatar_set_size:
+ * @self: a #HdyAvatar
+ * @size: The size to be used for the avatar
+ *
+ * Sets the size of the avatar.
+ */
+void
+hdy_avatar_set_size (HdyAvatar *self,
+ gint size)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+ g_return_if_fail (size >= -1);
+
+ if (self->size == size)
+ return;
+
+ self->size = size;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SIZE]);
+}
diff --git a/subprojects/libhandy/src/hdy-avatar.h b/subprojects/libhandy/src/hdy-avatar.h
new file mode 100644
index 0000000..54f3787
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-avatar.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_AVATAR (hdy_avatar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyAvatar, hdy_avatar, HDY, AVATAR, GtkDrawingArea)
+
+/**
+ * HdyAvatarImageLoadFunc:
+ * @size: the required size of the avatar
+ * @user_data: (closure): user data
+ *
+ * The returned #GdkPixbuf is expected to be square with width and height set
+ * to @size. The image is cropped to a circle without any scaling or transformation.
+ *
+ * Returns: (nullable) (transfer full): the #GdkPixbuf to use as a custom avatar
+ * or %NULL to fallback to the generated avatar.
+ */
+typedef GdkPixbuf *(*HdyAvatarImageLoadFunc) (gint size,
+ gpointer user_data);
+
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_avatar_new (gint size,
+ const gchar *text,
+ gboolean show_initials);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_avatar_get_icon_name (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_icon_name (HdyAvatar *self,
+ const gchar *icon_name);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_avatar_get_text (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_text (HdyAvatar *self,
+ const gchar *text);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_avatar_get_show_initials (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_show_initials (HdyAvatar *self,
+ gboolean show_initials);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_image_load_func (HdyAvatar *self,
+ HdyAvatarImageLoadFunc load_image,
+ gpointer user_data,
+ GDestroyNotify destroy);
+HDY_AVAILABLE_IN_ALL
+gint hdy_avatar_get_size (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_size (HdyAvatar *self,
+ gint size);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-cairo-private.h b/subprojects/libhandy/src/hdy-cairo-private.h
new file mode 100644
index 0000000..d064f04
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-cairo-private.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib.h>
+#include <cairo/cairo.h>
+
+G_BEGIN_DECLS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_t, cairo_destroy)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_surface_t, cairo_surface_destroy)
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-box-private.h b/subprojects/libhandy/src/hdy-carousel-box-private.h
new file mode 100644
index 0000000..98d3435
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-box-private.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_BOX (hdy_carousel_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyCarouselBox, hdy_carousel_box, HDY, CAROUSEL_BOX, GtkContainer)
+
+GtkWidget *hdy_carousel_box_new (void);
+
+void hdy_carousel_box_insert (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position);
+void hdy_carousel_box_reorder (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position);
+
+gboolean hdy_carousel_box_is_animating (HdyCarouselBox *self);
+void hdy_carousel_box_stop_animation (HdyCarouselBox *self);
+
+void hdy_carousel_box_scroll_to (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint64 duration);
+
+guint hdy_carousel_box_get_n_pages (HdyCarouselBox *self);
+gdouble hdy_carousel_box_get_distance (HdyCarouselBox *self);
+
+gdouble hdy_carousel_box_get_position (HdyCarouselBox *self);
+void hdy_carousel_box_set_position (HdyCarouselBox *self,
+ gdouble position);
+
+guint hdy_carousel_box_get_spacing (HdyCarouselBox *self);
+void hdy_carousel_box_set_spacing (HdyCarouselBox *self,
+ guint spacing);
+
+guint hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self);
+void hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self,
+ guint reveal_duration);
+
+GtkWidget *hdy_carousel_box_get_nth_child (HdyCarouselBox *self,
+ guint n);
+
+gdouble *hdy_carousel_box_get_snap_points (HdyCarouselBox *self,
+ gint *n_snap_points);
+void hdy_carousel_box_get_range (HdyCarouselBox *self,
+ gdouble *lower,
+ gdouble *upper);
+gdouble hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self);
+GtkWidget *hdy_carousel_box_get_page_at_position (HdyCarouselBox *self,
+ gdouble position);
+gint hdy_carousel_box_get_current_page_index (HdyCarouselBox *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-box.c b/subprojects/libhandy/src/hdy-carousel-box.c
new file mode 100644
index 0000000..1e0355f
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-box.c
@@ -0,0 +1,1768 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-carousel-box-private.h"
+
+#include <math.h>
+
+/**
+ * PRIVATE:hdy-carousel-box
+ * @short_description: Scrolling box used in #HdyCarousel
+ * @title: HdyCarouselBox
+ * @See_also: #HdyCarousel
+ * @stability: Private
+ *
+ * The #HdyCarouselBox object is meant to be used exclusively as part of the
+ * #HdyCarousel implementation.
+ *
+ * Since: 1.0
+ */
+
+typedef struct _HdyCarouselBoxAnimation HdyCarouselBoxAnimation;
+
+struct _HdyCarouselBoxAnimation
+{
+ gint64 start_time;
+ gint64 end_time;
+ gdouble start_value;
+ gdouble end_value;
+};
+
+typedef struct _HdyCarouselBoxChildInfo HdyCarouselBoxChildInfo;
+
+struct _HdyCarouselBoxChildInfo
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ gint position;
+ gboolean visible;
+ gdouble size;
+ gdouble snap_point;
+ gboolean adding;
+ gboolean removing;
+
+ gboolean shift_position;
+ HdyCarouselBoxAnimation resize_animation;
+
+ cairo_surface_t *surface;
+ cairo_region_t *dirty_region;
+};
+
+struct _HdyCarouselBox
+{
+ GtkContainer parent_instance;
+
+ HdyCarouselBoxAnimation animation;
+ HdyCarouselBoxChildInfo *destination_child;
+ GList *children;
+
+ gint child_width;
+ gint child_height;
+
+ gdouble distance;
+ gdouble position;
+ guint spacing;
+ GtkOrientation orientation;
+ guint reveal_duration;
+
+ guint tick_cb_id;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselBox, hdy_carousel_box, GTK_TYPE_CONTAINER,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+ PROP_0,
+ PROP_N_PAGES,
+ PROP_POSITION,
+ PROP_SPACING,
+ PROP_REVEAL_DURATION,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_REVEAL_DURATION + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_ANIMATION_STOPPED,
+ SIGNAL_POSITION_SHIFTED,
+ SIGNAL_LAST_SIGNAL,
+};
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static HdyCarouselBoxChildInfo *
+find_child_info (HdyCarouselBox *self,
+ GtkWidget *widget)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (widget == info->widget)
+ return info;
+ }
+
+ return NULL;
+}
+
+static gint
+find_child_index (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gboolean count_removing)
+{
+ GList *l;
+ gint i;
+
+ i = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->removing && !count_removing)
+ continue;
+
+ if (widget == info->widget)
+ return i;
+
+ i++;
+ }
+
+ return -1;
+}
+
+static GList *
+get_nth_link (HdyCarouselBox *self,
+ gint n)
+{
+
+ GList *l;
+ gint i;
+
+ i = n;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->removing)
+ continue;
+
+ if (i-- == 0)
+ return l;
+ }
+
+ return NULL;
+}
+
+static HdyCarouselBoxChildInfo *
+find_child_info_by_window (HdyCarouselBox *self,
+ GdkWindow *window)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (window == info->window)
+ return info;
+ }
+
+ return NULL;
+}
+
+static HdyCarouselBoxChildInfo *
+get_closest_child_at (HdyCarouselBox *self,
+ gdouble position,
+ gboolean count_adding,
+ gboolean count_removing)
+{
+ GList *l;
+ HdyCarouselBoxChildInfo *closest_child = NULL;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (child->adding && !count_adding)
+ continue;
+
+ if (child->removing && !count_removing)
+ continue;
+
+ if (!closest_child ||
+ ABS (closest_child->snap_point - position) >
+ ABS (child->snap_point - position))
+ closest_child = child;
+ }
+
+ return closest_child;
+}
+
+static void
+free_child_info (HdyCarouselBoxChildInfo *info)
+{
+ if (info->surface)
+ cairo_surface_destroy (info->surface);
+ if (info->dirty_region)
+ cairo_region_destroy (info->dirty_region);
+ g_free (info);
+}
+
+static void
+invalidate_handler_cb (GdkWindow *window,
+ cairo_region_t *region)
+{
+ gpointer user_data;
+ HdyCarouselBox *self;
+ HdyCarouselBoxChildInfo *info;
+
+ gdk_window_get_user_data (window, &user_data);
+ g_assert (HDY_IS_CAROUSEL_BOX (user_data));
+ self = HDY_CAROUSEL_BOX (user_data);
+
+ info = find_child_info_by_window (self, window);
+
+ if (!info->dirty_region)
+ info->dirty_region = cairo_region_create ();
+
+ cairo_region_union (info->dirty_region, region);
+}
+
+static void
+register_window (HdyCarouselBoxChildInfo *info,
+ HdyCarouselBox *self)
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ GdkWindowAttr attributes;
+ GtkAllocation allocation;
+ gint attributes_mask;
+
+ if (info->removing)
+ return;
+
+ widget = GTK_WIDGET (self);
+ gtk_widget_get_allocation (info->widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, window);
+ gtk_widget_set_parent_window (info->widget, window);
+
+ gdk_window_set_user_data (window, self);
+
+ gdk_window_show (window);
+
+ info->window = window;
+
+ gdk_window_set_invalidate_handler (window, invalidate_handler_cb);
+}
+
+static void
+unregister_window (HdyCarouselBoxChildInfo *info,
+ HdyCarouselBox *self)
+{
+ if (!info->widget)
+ return;
+
+ gtk_widget_set_parent_window (info->widget, NULL);
+ gtk_widget_unregister_window (GTK_WIDGET (self), info->window);
+ gdk_window_destroy (info->window);
+ info->window = NULL;
+}
+
+static gdouble
+get_animation_value (HdyCarouselBoxAnimation *animation,
+ GdkFrameClock *frame_clock)
+{
+ gint64 frame_time, duration;
+ gdouble t;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+ frame_time = MIN (frame_time, animation->end_time);
+
+ duration = animation->end_time - animation->start_time;
+ t = (gdouble) (frame_time - animation->start_time) / duration;
+ t = hdy_ease_out_cubic (t);
+
+ return hdy_lerp (animation->start_value, animation->end_value, t);
+}
+
+static gboolean
+animate_position (HdyCarouselBox *self,
+ GdkFrameClock *frame_clock)
+{
+ gint64 frame_time;
+ gdouble value;
+
+ if (!hdy_carousel_box_is_animating (self))
+ return G_SOURCE_REMOVE;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ self->animation.end_value = self->destination_child->snap_point;
+ value = get_animation_value (&self->animation, frame_clock);
+ hdy_carousel_box_set_position (self, value);
+
+ if (frame_time >= self->animation.end_time) {
+ self->animation.start_time = 0;
+ self->animation.end_time = 0;
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void update_windows (HdyCarouselBox *self);
+
+static void
+complete_child_animation (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ update_windows (self);
+
+ if (child->adding)
+ child->adding = FALSE;
+
+ if (child->removing) {
+ self->children = g_list_remove (self->children, child);
+
+ free_child_info (child);
+ }
+}
+
+static gboolean
+animate_child_size (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child,
+ GdkFrameClock *frame_clock,
+ gdouble *delta)
+{
+ gint64 frame_time;
+ gdouble d, new_value;
+
+ if (child->resize_animation.start_time == 0)
+ return G_SOURCE_REMOVE;
+
+ new_value = get_animation_value (&child->resize_animation, frame_clock);
+ d = new_value - child->size;
+
+ child->size += d;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (delta)
+ *delta = d;
+
+ if (frame_time >= child->resize_animation.end_time) {
+ child->resize_animation.start_time = 0;
+ child->resize_animation.end_time = 0;
+ complete_child_animation (self, child);
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+set_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ gdouble lower, upper;
+
+ hdy_carousel_box_get_range (self, &lower, &upper);
+
+ position = CLAMP (position, lower, upper);
+
+ self->position = position;
+ update_windows (self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ g_autoptr (GList) children = NULL;
+ GList *l;
+ gboolean should_continue;
+ gdouble position_shift;
+
+ should_continue = G_SOURCE_REMOVE;
+
+ position_shift = 0;
+
+ children = g_list_copy (self->children);
+ for (l = children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+ gdouble delta;
+ gboolean shift;
+
+ delta = 0;
+ shift = child->shift_position;
+
+ should_continue |= animate_child_size (self, child, frame_clock, &delta);
+
+ if (shift)
+ position_shift += delta;
+ }
+
+ update_windows (self);
+
+ if (position_shift != 0) {
+ set_position (self, self->position + position_shift);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, position_shift);
+ }
+
+ should_continue |= animate_position (self, frame_clock);
+
+ update_windows (self);
+
+ if (!should_continue)
+ self->tick_cb_id = 0;
+
+ return should_continue;
+}
+
+static void
+update_shift_position_flag (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ HdyCarouselBoxChildInfo *closest_child;
+ gint animating_index, closest_index;
+
+ /* We want to still shift position when the active child is being removed */
+ closest_child = get_closest_child_at (self, self->position, FALSE, TRUE);
+
+ if (!closest_child)
+ return;
+
+ animating_index = g_list_index (self->children, child);
+ closest_index = g_list_index (self->children, closest_child);
+
+ child->shift_position = (closest_index >= animating_index);
+}
+
+static void
+animate_child (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child,
+ gdouble value,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (child->resize_animation.start_time > 0) {
+ child->resize_animation.start_time = 0;
+ child->resize_animation.end_time = 0;
+ }
+
+ update_shift_position_flag (self, child);
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)) ||
+ duration <= 0 ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gdouble delta = value - child->size;
+
+ child->size = value;
+
+ if (child->shift_position) {
+ set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+ }
+
+ complete_child_animation (self, child);
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gdouble delta = value - child->size;
+
+ child->size = value;
+
+ if (child->shift_position) {
+ set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+ }
+
+ complete_child_animation (self, child);
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ child->resize_animation.start_value = child->size;
+ child->resize_animation.end_value = value;
+
+ child->resize_animation.start_time = frame_time / 1000;
+ child->resize_animation.end_time = child->resize_animation.start_time + duration;
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL);
+}
+
+static gboolean
+hdy_carousel_box_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->adding || info->removing)
+ continue;
+
+ if (!info->visible)
+ continue;
+
+ if (info->dirty_region && !info->removing) {
+ g_autoptr (cairo_t) surface_cr = NULL;
+ GtkAllocation child_alloc;
+
+ if (!info->surface) {
+ gint width, height;
+
+ width = gdk_window_get_width (info->window);
+ height = gdk_window_get_height (info->window);
+
+ info->surface = gdk_window_create_similar_surface (info->window,
+ CAIRO_CONTENT_COLOR_ALPHA,
+ width, height);
+ }
+
+ gtk_widget_get_allocation (info->widget, &child_alloc);
+
+ surface_cr = cairo_create (info->surface);
+
+ gdk_cairo_region (surface_cr, info->dirty_region);
+ cairo_clip (surface_cr);
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ cairo_translate (surface_cr, 0, -info->position);
+ else
+ cairo_translate (surface_cr, -info->position, 0);
+
+ cairo_save (surface_cr);
+ cairo_set_source_rgba (surface_cr, 0, 0, 0, 0);
+ cairo_set_operator (surface_cr, CAIRO_OPERATOR_SOURCE);
+ cairo_paint (surface_cr);
+ cairo_restore (surface_cr);
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self), info->widget, surface_cr);
+
+ cairo_region_destroy (info->dirty_region);
+ info->dirty_region = NULL;
+ }
+
+ if (!info->surface)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ cairo_set_source_surface (cr, info->surface, 0, info->position);
+ else
+ cairo_set_source_surface (cr, info->surface, info->position, 0);
+ cairo_paint (cr);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ GList *children;
+
+ if (minimum)
+ *minimum = 0;
+ if (natural)
+ *natural = 0;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+ if (natural_baseline)
+ *natural_baseline = -1;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ gint child_min, child_nat;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ if (for_size < 0)
+ gtk_widget_get_preferred_height (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat);
+ } else {
+ if (for_size < 0)
+ gtk_widget_get_preferred_width (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat);
+ }
+
+ if (minimum)
+ *minimum = MAX (*minimum, child_min);
+ if (natural)
+ *natural = MAX (*natural, child_nat);
+ }
+}
+
+static void
+hdy_carousel_box_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_width_for_height (GtkWidget *widget,
+ gint for_height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ measure (widget, GTK_ORIENTATION_HORIZONTAL, for_height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_height_for_width (GtkWidget *widget,
+ gint for_width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ measure (widget, GTK_ORIENTATION_VERTICAL, for_width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+invalidate_cache_for_child (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ cairo_rectangle_int_t rect;
+
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = self->child_width;
+ rect.height = self->child_height;
+
+ if (child->surface)
+ g_clear_pointer (&child->surface, cairo_surface_destroy);
+
+ if (child->dirty_region)
+ cairo_region_destroy (child->dirty_region);
+ child->dirty_region = cairo_region_create_rectangle (&rect);
+}
+
+static void
+invalidate_drawing_cache (HdyCarouselBox *self)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child_info = l->data;
+
+ invalidate_cache_for_child (self, child_info);
+ }
+}
+
+static void
+update_windows (HdyCarouselBox *self)
+{
+ GList *children;
+ GtkAllocation alloc;
+ gdouble x, y, offset;
+ gboolean is_rtl;
+ gdouble snap_point;
+
+ snap_point = 0;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ child_info->snap_point = snap_point + child_info->size - 1;
+
+ snap_point += child_info->size;
+ }
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+ return;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ x = alloc.x;
+ y = alloc.y;
+
+ is_rtl = (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL);
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ offset = (self->distance * self->position) - (alloc.height - self->child_height) / 2.0;
+ else if (is_rtl)
+ offset = -(self->distance * self->position) + (alloc.width - self->child_width) / 2.0;
+ else
+ offset = (self->distance * self->position) - (alloc.width - self->child_width) / 2.0;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ y -= offset;
+ else
+ x -= offset;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ if (!child_info->removing) {
+ if (!gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL) {
+ child_info->position = y;
+ child_info->visible = child_info->position < alloc.height &&
+ child_info->position + self->child_height > 0;
+ gdk_window_move (child_info->window, alloc.x, alloc.y + child_info->position);
+ } else {
+ child_info->position = x;
+ child_info->visible = child_info->position < alloc.width &&
+ child_info->position + self->child_width > 0;
+ gdk_window_move (child_info->window, alloc.x + child_info->position, alloc.y);
+ }
+
+ if (!child_info->visible)
+ invalidate_cache_for_child (self, child_info);
+ }
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ y += self->distance * child_info->size;
+ else if (is_rtl)
+ x -= self->distance * child_info->size;
+ else
+ x += self->distance * child_info->size;
+ }
+}
+
+static void
+hdy_carousel_box_map (GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->map (widget);
+
+ gtk_widget_queue_draw (GTK_WIDGET (widget));
+}
+
+static void
+hdy_carousel_box_realize (GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->realize (widget);
+
+ g_list_foreach (self->children, (GFunc) register_window, self);
+
+ gtk_widget_queue_allocate (widget);
+}
+
+static void
+hdy_carousel_box_unrealize (GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+
+ g_list_foreach (self->children, (GFunc) unregister_window, self);
+
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_carousel_box_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ gint size, width, height;
+ GList *children;
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ size = 0;
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ gint min, nat;
+ gint child_size;
+
+ if (child_info->removing)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gtk_widget_get_preferred_width_for_height (child, allocation->height,
+ &min, &nat);
+ if (gtk_widget_get_hexpand (child))
+ child_size = MAX (min, allocation->width);
+ else
+ child_size = MAX (min, nat);
+ } else {
+ gtk_widget_get_preferred_height_for_width (child, allocation->width,
+ &min, &nat);
+ if (gtk_widget_get_vexpand (child))
+ child_size = MAX (min, allocation->height);
+ else
+ child_size = MAX (min, nat);
+ }
+
+ size = MAX (size, child_size);
+ }
+
+ self->distance = size + self->spacing;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ width = size;
+ height = allocation->height;
+ } else {
+ width = allocation->width;
+ height = size;
+ }
+
+ if (width != self->child_width || height != self->child_height)
+ invalidate_drawing_cache (self);
+
+ self->child_width = width;
+ self->child_height = height;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+ continue;
+
+ gdk_window_resize (child_info->window, width, height);
+ }
+
+ update_windows (self);
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ GtkAllocation alloc;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = height;
+ gtk_widget_size_allocate (child, &alloc);
+ }
+
+ invalidate_drawing_cache (self);
+ gtk_widget_set_clip (widget, allocation);
+}
+
+static void
+hdy_carousel_box_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+
+ hdy_carousel_box_insert (self, widget, -1);
+}
+
+static void
+shift_position (HdyCarouselBox *self,
+ gdouble delta)
+{
+ hdy_carousel_box_set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+}
+
+static void
+hdy_carousel_box_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+ HdyCarouselBoxChildInfo *info;
+
+ info = find_child_info (self, widget);
+ if (!info)
+ return;
+
+ info->removing = TRUE;
+
+ gtk_widget_unparent (widget);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (container)))
+ unregister_window (info, self);
+
+ info->widget = NULL;
+
+ if (!gtk_widget_in_destruction (GTK_WIDGET (container)))
+ animate_child (self, info, 0, self->reveal_duration);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+hdy_carousel_box_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+ g_autoptr (GList) children = NULL;
+ GList *l;
+
+ children = g_list_copy (self->children);
+ for (l = children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (!child->removing)
+ (* callback) (child->widget, callback_data);
+ }
+}
+
+static void
+hdy_carousel_box_finalize (GObject *object)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ if (self->tick_cb_id > 0)
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+
+ g_list_free_full (self->children, (GDestroyNotify) free_child_info);
+
+ G_OBJECT_CLASS (hdy_carousel_box_parent_class)->finalize (object);
+}
+
+static void
+hdy_carousel_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ switch (prop_id) {
+ case PROP_N_PAGES:
+ g_value_set_uint (value, hdy_carousel_box_get_n_pages (self));
+ break;
+
+ case PROP_POSITION:
+ g_value_set_double (value, hdy_carousel_box_get_position (self));
+ break;
+
+ case PROP_SPACING:
+ g_value_set_uint (value, hdy_carousel_box_get_spacing (self));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ g_value_set_uint (value, hdy_carousel_box_get_reveal_duration (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ switch (prop_id) {
+ case PROP_POSITION:
+ hdy_carousel_box_set_position (self, g_value_get_double (value));
+ break;
+
+ case PROP_SPACING:
+ hdy_carousel_box_set_spacing (self, g_value_get_uint (value));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ hdy_carousel_box_set_reveal_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_box_class_init (HdyCarouselBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_carousel_box_finalize;
+ object_class->get_property = hdy_carousel_box_get_property;
+ object_class->set_property = hdy_carousel_box_set_property;
+ widget_class->draw = hdy_carousel_box_draw;
+ widget_class->get_preferred_width = hdy_carousel_box_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_box_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_carousel_box_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_carousel_box_get_preferred_height_for_width;
+ widget_class->map = hdy_carousel_box_map;
+ widget_class->realize = hdy_carousel_box_realize;
+ widget_class->unrealize = hdy_carousel_box_unrealize;
+ widget_class->size_allocate = hdy_carousel_box_size_allocate;
+ container_class->add = hdy_carousel_box_add;
+ container_class->remove = hdy_carousel_box_remove;
+ container_class->forall = hdy_carousel_box_forall;
+
+ /**
+ * HdyCarouselBox:n-pages:
+ *
+ * The number of pages in a #HdyCarouselBox
+ *
+ * Since: 1.0
+ */
+ props[PROP_N_PAGES] =
+ g_param_spec_uint ("n-pages",
+ _("Number of pages"),
+ _("Number of pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:position:
+ *
+ * Current scrolling position, unitless. 1 matches 1 page.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POSITION] =
+ g_param_spec_double ("position",
+ _("Position"),
+ _("Current scrolling position"),
+ 0,
+ G_MAXDOUBLE,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:spacing:
+ *
+ * Spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SPACING] =
+ g_param_spec_uint ("spacing",
+ _("Spacing"),
+ _("Spacing between pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:reveal-duration:
+ *
+ * Duration of the animation used when adding or removing pages, in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVEAL_DURATION] =
+ g_param_spec_uint ("reveal-duration",
+ _("Reveal duration"),
+ _("Page reveal duration"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyCarouselBox::animation-stopped:
+ * @self: The #HdyCarouselBox instance
+ *
+ * This signal is emitted after an animation has been stopped. If animations
+ * are disabled, the signal is emitted as well.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_ANIMATION_STOPPED] =
+ g_signal_new ("animation-stopped",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * HdyCarouselBox::position-shifted:
+ * @self: The #HdyCarouselBox instance
+ * @delta: The amount to shift the position by
+ *
+ * This signal is emitted when position has been programmatically shifted.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_POSITION_SHIFTED] =
+ g_signal_new ("position-shifted",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_DOUBLE);
+}
+
+static void
+hdy_carousel_box_init (HdyCarouselBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ self->orientation = GTK_ORIENTATION_HORIZONTAL;
+ self->reveal_duration = 0;
+
+ gtk_widget_set_has_window (widget, FALSE);
+}
+
+/**
+ * hdy_carousel_box_new:
+ *
+ * Create a new #HdyCarouselBox widget.
+ *
+ * Returns: The newly created #HdyCarouselBox widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_BOX, NULL);
+}
+
+/**
+ * hdy_carousel_box_insert:
+ * @self: a #HdyCarouselBox
+ * @widget: a widget to add
+ * @position: the position to insert @widget in.
+ *
+ * Inserts @widget into @self at position @position.
+ *
+ * If position is -1, or larger than the number of pages, @widget will be
+ * appended to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_insert (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyCarouselBoxChildInfo *info;
+ GList *prev_link;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ info = g_new0 (HdyCarouselBoxChildInfo, 1);
+ info->widget = widget;
+ info->size = 0;
+ info->adding = TRUE;
+
+ if (gtk_widget_get_realized (GTK_WIDGET (self)))
+ register_window (info, self);
+
+ if (position >= 0)
+ prev_link = get_nth_link (self, position);
+ else
+ prev_link = NULL;
+
+ self->children = g_list_insert_before (self->children, prev_link, info);
+
+ gtk_widget_set_parent (widget, GTK_WIDGET (self));
+
+ update_windows (self);
+
+ animate_child (self, info, 1, self->reveal_duration);
+
+ invalidate_drawing_cache (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+/**
+ * hdy_carousel_box_reorder:
+ * @self: a #HdyCarouselBox
+ * @widget: a widget to add
+ * @position: the position to move @widget to.
+ *
+ * Moves @widget into position @position.
+ *
+ * If position is -1, or larger than the number of pages, @widget will be moved
+ * to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_reorder (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyCarouselBoxChildInfo *info, *prev_info;
+ GList *link, *prev_link;
+ gint old_position;
+ gdouble closest_point, old_point, new_point;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ closest_point = hdy_carousel_box_get_closest_snap_point (self);
+
+ info = find_child_info (self, widget);
+ link = g_list_find (self->children, info);
+ old_position = g_list_position (self->children, link);
+
+ if (position == old_position)
+ return;
+
+ old_point = ((HdyCarouselBoxChildInfo *) link->data)->snap_point;
+
+ if (position < 0 || position >= hdy_carousel_box_get_n_pages (self))
+ prev_link = g_list_last (self->children);
+ else
+ prev_link = get_nth_link (self, position);
+
+ prev_info = prev_link->data;
+ new_point = prev_info->snap_point;
+ if (new_point > old_point)
+ new_point -= prev_info->size;
+
+ self->children = g_list_remove_link (self->children, link);
+ self->children = g_list_insert_before (self->children, prev_link, link->data);
+
+ if (closest_point == old_point)
+ shift_position (self, new_point - old_point);
+ else if (old_point > closest_point && closest_point >= new_point)
+ shift_position (self, info->size);
+ else if (new_point >= closest_point && closest_point > old_point)
+ shift_position (self, -info->size);
+}
+
+/**
+ * hdy_carousel_box_is_animating:
+ * @self: a #HdyCarouselBox
+ *
+ * Get whether @self is animating position.
+ *
+ * Returns: %TRUE if an animation is running
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_box_is_animating (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), FALSE);
+
+ return (self->animation.start_time != 0);
+}
+
+/**
+ * hdy_carousel_box_stop_animation:
+ * @self: a #HdyCarouselBox
+ *
+ * Stops a running animation. If there's no animation running, does nothing.
+ *
+ * It does not reset position to a non-transient value automatically.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_stop_animation (HdyCarouselBox *self)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->animation.start_time == 0)
+ return;
+
+ self->animation.start_time = 0;
+ self->animation.end_time = 0;
+}
+
+/**
+ * hdy_carousel_box_scroll_to:
+ * @self: a #HdyCarouselBox
+ * @widget: a child of @self
+ * @duration: animation duration in milliseconds
+ *
+ * Scrolls to @widget position over the next @duration milliseconds using
+ * easeOutCubic interpolator.
+ *
+ * If an animation was already running, it will be cancelled automatically.
+ *
+ * @duration can be 0, in that case the position will be
+ * changed immediately.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_scroll_to (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+ gdouble position;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+ g_return_if_fail (duration >= 0);
+
+ child = find_child_info (self, widget);
+ position = child->snap_point;
+
+ hdy_carousel_box_stop_animation (self);
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ hdy_carousel_box_set_position (self, position);
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ hdy_carousel_box_set_position (self, position);
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->destination_child = child;
+
+ self->animation.start_value = self->position;
+ self->animation.end_value = position;
+
+ self->animation.start_time = frame_time / 1000;
+ self->animation.end_time = self->animation.start_time + duration;
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL);
+}
+
+/**
+ * hdy_carousel_box_get_n_pages:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the number of pages in @self.
+ *
+ * Returns: The number of pages in @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_n_pages (HdyCarouselBox *self)
+{
+ GList *l;
+ guint n_pages;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ n_pages = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (!child->removing)
+ n_pages++;
+ }
+
+ return n_pages;
+}
+
+/**
+ * hdy_carousel_box_get_distance:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets swiping distance between two adjacent children in pixels.
+ *
+ * Returns: The swiping distance in pixels
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_distance (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->distance;
+}
+
+/**
+ * hdy_carousel_box_get_position:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets current scroll position in @self. It's unitless, 1 matches 1 page.
+ *
+ * Returns: The scroll position
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_position (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->position;
+}
+
+/**
+ * hdy_carousel_box_set_position:
+ * @self: a #HdyCarouselBox
+ * @position: the new position value
+ *
+ * Sets current scroll position in @self, unitless, 1 matches 1 page.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ GList *l;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ set_position (self, position);
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (child->adding || child->removing)
+ update_shift_position_flag (self, child);
+ }
+}
+
+/**
+ * hdy_carousel_box_get_spacing:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets spacing between pages in pixels.
+ *
+ * Returns: Spacing between pages
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_spacing (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->spacing;
+}
+
+/**
+ * hdy_carousel_box_set_spacing:
+ * @self: a #HdyCarouselBox
+ * @spacing: the new spacing value
+ *
+ * Sets spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_spacing (HdyCarouselBox *self,
+ guint spacing)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->spacing == spacing)
+ return;
+
+ self->spacing = spacing;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+/**
+ * hdy_carousel_box_get_reveal_duration:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Returns: Page reveal duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->reveal_duration;
+}
+
+/**
+ * hdy_carousel_box_set_reveal_duration:
+ * @self: a #HdyCarouselBox
+ * @reveal_duration: the new reveal duration value
+ *
+ * Sets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self,
+ guint reveal_duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->reveal_duration == reveal_duration)
+ return;
+
+ self->reveal_duration = reveal_duration;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]);
+}
+
+/**
+ * hdy_carousel_box_get_nth_child:
+ * @self: a #HdyCarouselBox
+ * @n: the child index
+ *
+ * Retrieves @n-th child widget of @self.
+ *
+ * Returns: The @n-th child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_get_nth_child (HdyCarouselBox *self,
+ guint n)
+{
+ HdyCarouselBoxChildInfo *info;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+ g_return_val_if_fail (n < hdy_carousel_box_get_n_pages (self), NULL);
+
+ info = get_nth_link (self, n)->data;
+
+ return info->widget;
+}
+
+/**
+ * hdy_carousel_box_get_snap_points:
+ * @self: a #HdyCarouselBox
+ * @n_snap_points: (out)
+ *
+ * Gets the snap points of @self, representing the points between each page,
+ * before the first page and after the last page.
+ *
+ * Returns: (array length=n_snap_points) (transfer full): the snap points of @self
+ *
+ * Since: 1.0
+ */
+gdouble *
+hdy_carousel_box_get_snap_points (HdyCarouselBox *self,
+ gint *n_snap_points)
+{
+ guint i, n_pages;
+ gdouble *points;
+ GList *l;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+
+ n_pages = MAX (g_list_length (self->children), 1);
+
+ points = g_new0 (gdouble, n_pages);
+
+ i = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ points[i++] = info->snap_point;
+ }
+
+ if (n_snap_points)
+ *n_snap_points = n_pages;
+
+ return points;
+}
+
+/**
+ * hdy_carousel_box_get_range:
+ * @self: a #HdyCarouselBox
+ * @lower: (out) (optional): location to store the lowest possible position, or %NULL
+ * @upper: (out) (optional): location to store the maximum possible position, or %NULL
+ *
+ * Gets the range of possible positions.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_get_range (HdyCarouselBox *self,
+ gdouble *lower,
+ gdouble *upper)
+{
+ GList *l;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ l = g_list_last (self->children);
+ child = l ? l->data : NULL;
+
+ if (lower)
+ *lower = 0;
+
+ if (upper)
+ *upper = child ? child->snap_point : 0;
+}
+
+/**
+ * hdy_carousel_box_get_closest_snap_point:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the snap point closest to the current position.
+ *
+ * Returns: the closest snap point.
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self)
+{
+ HdyCarouselBoxChildInfo *closest_child;
+
+ closest_child = get_closest_child_at (self, self->position, TRUE, TRUE);
+
+ if (!closest_child)
+ return 0;
+
+ return closest_child->snap_point;
+}
+
+/**
+ * hdy_carousel_box_get_page_at_position:
+ * @self: a #HdyCarouselBox
+ * @position: a scroll position
+ *
+ * Gets the page closest to @position. For example, if @position matches
+ * the current position, the returned widget will match the currently
+ * displayed page.
+ *
+ * Returns: the closest page.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_get_page_at_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ gdouble lower, upper;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+
+ hdy_carousel_box_get_range (self, &lower, &upper);
+
+ position = CLAMP (position, lower, upper);
+
+ child = get_closest_child_at (self, position, TRUE, FALSE);
+
+ return child->widget;
+}
+
+/**
+ * hdy_carousel_box_get_current_page_index:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the index of the currently displayed page.
+ *
+ * Returns: the index of the current page.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_carousel_box_get_current_page_index (HdyCarouselBox *self)
+{
+ GtkWidget *child;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ child = hdy_carousel_box_get_page_at_position (self, self->position);
+
+ return find_child_index (self, child, FALSE);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.c b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c
new file mode 100644
index 0000000..5bbc541
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel-indicator-dots.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define DOTS_RADIUS 3
+#define DOTS_RADIUS_SELECTED 4
+#define DOTS_OPACITY 0.3
+#define DOTS_OPACITY_SELECTED 0.9
+#define DOTS_SPACING 7
+#define DOTS_MARGIN 6
+
+/**
+ * SECTION:hdy-carousel-indicator-dots
+ * @short_description: A dots indicator for #HdyCarousel
+ * @title: HdyCarouselIndicatorDots
+ * @See_also: #HdyCarousel, #HdyCarouselIndicatorLines
+ *
+ * The #HdyCarouselIndicatorDots widget can be used to show a set of dots for each
+ * page of a given #HdyCarousel. The dot representing the carousel's active page
+ * is larger and more opaque than the others, the transition to the active and
+ * inactive state is gradual to match the carousel's position.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarouselIndicatorDots has a single CSS node with name carouselindicatordots.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarouselIndicatorDots
+{
+ GtkDrawingArea parent_instance;
+
+ HdyCarousel *carousel;
+ GtkOrientation orientation;
+
+ guint tick_cb_id;
+ guint64 end_time;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, GTK_TYPE_DRAWING_AREA,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_CAROUSEL,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_CAROUSEL + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint64 frame_time;
+
+ g_assert (self->tick_cb_id > 0);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (frame_time >= self->end_time ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ self->tick_cb_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_animation (HdyCarouselIndicatorDots *self)
+{
+ if (self->tick_cb_id == 0)
+ return;
+
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+ self->tick_cb_id = 0;
+}
+
+static void
+animate (HdyCarouselIndicatorDots *self,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->end_time = MAX (self->end_time, frame_time / 1000 + duration);
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ animation_cb,
+ NULL, NULL);
+}
+
+static GdkRGBA
+get_color (GtkWidget *widget)
+{
+ GtkStyleContext *context;
+ GtkStateFlags flags;
+ GdkRGBA color;
+
+ context = gtk_widget_get_style_context (widget);
+ flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_color (context, flags, &color);
+
+ return color;
+}
+
+static void
+draw_dots (GtkWidget *widget,
+ cairo_t *cr,
+ GtkOrientation orientation,
+ gdouble position,
+ gdouble *sizes,
+ guint n_pages)
+{
+ GdkRGBA color;
+ gint i, widget_length, widget_thickness;
+ gdouble x, y, indicator_length, dot_size, full_size;
+ gdouble current_position, remaining_progress;
+
+ color = get_color (widget);
+ dot_size = 2 * DOTS_RADIUS_SELECTED + DOTS_SPACING;
+
+ indicator_length = 0;
+ for (i = 0; i < n_pages; i++)
+ indicator_length += dot_size * sizes[i];
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ widget_length = gtk_widget_get_allocated_width (widget);
+ widget_thickness = gtk_widget_get_allocated_height (widget);
+ } else {
+ widget_length = gtk_widget_get_allocated_height (widget);
+ widget_thickness = gtk_widget_get_allocated_width (widget);
+ }
+
+ /* Ensure the indicators are aligned to pixel grid when not animating */
+ full_size = round (indicator_length / dot_size) * dot_size;
+ if ((widget_length - (gint) full_size) % 2 == 0)
+ widget_length--;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_translate (cr, (widget_length - indicator_length) / 2.0, widget_thickness / 2);
+ else
+ cairo_translate (cr, widget_thickness / 2, (widget_length - indicator_length) / 2.0);
+
+ x = 0;
+ y = 0;
+
+ current_position = 0;
+ remaining_progress = 1;
+
+ for (i = 0; i < n_pages; i++) {
+ gdouble progress, radius, opacity;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ x += dot_size * sizes[i] / 2.0;
+ else
+ y += dot_size * sizes[i] / 2.0;
+
+ current_position += sizes[i];
+
+ progress = CLAMP (current_position - position, 0, remaining_progress);
+ remaining_progress -= progress;
+
+ radius = hdy_lerp (DOTS_RADIUS, DOTS_RADIUS_SELECTED, progress) * sizes[i];
+ opacity = hdy_lerp (DOTS_OPACITY, DOTS_OPACITY_SELECTED, progress) * sizes[i];
+
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * opacity);
+ cairo_arc (cr, x, y, radius, 0, 2 * G_PI);
+ cairo_fill (cr);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ x += dot_size * sizes[i] / 2.0;
+ else
+ y += dot_size * sizes[i] / 2.0;
+ }
+}
+
+static void
+n_pages_changed_cb (HdyCarouselIndicatorDots *self)
+{
+ animate (self, hdy_carousel_get_reveal_duration (self->carousel));
+}
+
+static void
+hdy_carousel_indicator_dots_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint size = 0;
+
+ if (orientation == self->orientation) {
+ gint n_pages = 0;
+ if (self->carousel)
+ n_pages = hdy_carousel_get_n_pages (self->carousel);
+
+ size = MAX (0, (2 * DOTS_RADIUS_SELECTED + DOTS_SPACING) * n_pages - DOTS_SPACING);
+ } else {
+ size = 2 * DOTS_RADIUS_SELECTED;
+ }
+
+ size += 2 * DOTS_MARGIN;
+
+ if (minimum)
+ *minimum = size;
+
+ if (natural)
+ *natural = size;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_carousel_indicator_dots_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_indicator_dots_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static gboolean
+hdy_carousel_indicator_dots_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint i, n_points;
+ gdouble position;
+ g_autofree gdouble *points = NULL;
+ g_autofree gdouble *sizes = NULL;
+
+ if (!self->carousel)
+ return GDK_EVENT_PROPAGATE;
+
+ points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points);
+ position = hdy_carousel_get_position (self->carousel);
+
+ if (n_points < 2)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ position = points[n_points - 1] - position;
+
+ sizes = g_new0 (gdouble, n_points);
+
+ sizes[0] = points[0] + 1;
+ for (i = 1; i < n_points; i++)
+ sizes[i] = points[i] - points[i - 1];
+
+ draw_dots (widget, cr, self->orientation, position, sizes, n_points);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ hdy_carousel_indicator_dots_set_carousel (self, NULL);
+
+ G_OBJECT_CLASS (hdy_carousel_indicator_dots_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_indicator_dots_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ g_value_set_object (value, hdy_carousel_indicator_dots_get_carousel (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_dots_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ hdy_carousel_indicator_dots_set_carousel (self, g_value_get_object (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_dots_class_init (HdyCarouselIndicatorDotsClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_indicator_dots_get_property;
+ object_class->set_property = hdy_carousel_indicator_dots_set_property;
+
+ widget_class->get_preferred_width = hdy_carousel_indicator_dots_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_indicator_dots_get_preferred_height;
+ widget_class->draw = hdy_carousel_indicator_dots_draw;
+
+ /**
+ * HdyCarouselIndicatorDots:carousel:
+ *
+ * The #HdyCarousel the indicator uses.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAROUSEL] =
+ g_param_spec_object ("carousel",
+ _("Carousel"),
+ _("Carousel"),
+ HDY_TYPE_CAROUSEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "carouselindicatordots");
+}
+
+static void
+hdy_carousel_indicator_dots_init (HdyCarouselIndicatorDots *self)
+{
+}
+
+/**
+ * hdy_carousel_indicator_dots_new:
+ *
+ * Create a new #HdyCarouselIndicatorDots widget.
+ *
+ * Returns: (transfer full): The newly created #HdyCarouselIndicatorDots widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_indicator_dots_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_DOTS, NULL);
+}
+
+/**
+ * hdy_carousel_indicator_dots_get_carousel:
+ * @self: a #HdyCarouselIndicatorDots
+ *
+ * Get the #HdyCarousel the indicator uses.
+ *
+ * See: hdy_carousel_indicator_dots_set_carousel()
+ *
+ * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+
+HdyCarousel *
+hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self), NULL);
+
+ return self->carousel;
+}
+
+/**
+ * hdy_carousel_indicator_dots_set_carousel:
+ * @self: a #HdyCarouselIndicatorDots
+ * @carousel: (nullable): a #HdyCarousel
+ *
+ * Sets the #HdyCarousel to use.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self,
+ HdyCarousel *carousel)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self));
+ g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL);
+
+ if (self->carousel == carousel)
+ return;
+
+ if (self->carousel) {
+ stop_animation (self);
+ g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self);
+ g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self);
+ }
+
+ g_set_object (&self->carousel, carousel);
+
+ if (self->carousel) {
+ g_signal_connect_object (self->carousel, "notify::position",
+ G_CALLBACK (gtk_widget_queue_draw), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->carousel, "notify::n-pages",
+ G_CALLBACK (n_pages_changed_cb), self,
+ G_CONNECT_SWAPPED);
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.h b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h
new file mode 100644
index 0000000..032886e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-carousel.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_INDICATOR_DOTS (hdy_carousel_indicator_dots_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, HDY, CAROUSEL_INDICATOR_DOTS, GtkDrawingArea)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_indicator_dots_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyCarousel *hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self,
+ HdyCarousel *carousel);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.c b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c
new file mode 100644
index 0000000..fba9b38
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel-indicator-lines.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define LINE_WIDTH 3
+#define LINE_LENGTH 35
+#define LINE_SPACING 5
+#define LINE_OPACITY 0.3
+#define LINE_OPACITY_ACTIVE 0.9
+#define LINE_MARGIN 2
+
+/**
+ * SECTION:hdy-carousel-indicator-lines
+ * @short_description: A lines indicator for #HdyCarousel
+ * @title: HdyCarouselIndicatorLines
+ * @See_also: #HdyCarousel, #HdyCarouselIndicatorDots
+ *
+ * The #HdyCarouselIndicatorLines widget can be used to show a set of thin and long
+ * rectangles for each page of a given #HdyCarousel. The carousel's active page
+ * is shown with another rectangle that moves between them to match the
+ * carousel's position.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarouselIndicatorLines has a single CSS node with name carouselindicatorlines.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarouselIndicatorLines
+{
+ GtkDrawingArea parent_instance;
+
+ HdyCarousel *carousel;
+ GtkOrientation orientation;
+
+ guint tick_cb_id;
+ guint64 end_time;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, GTK_TYPE_DRAWING_AREA,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_CAROUSEL,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_CAROUSEL + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint64 frame_time;
+
+ g_assert (self->tick_cb_id > 0);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (frame_time >= self->end_time ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ self->tick_cb_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_animation (HdyCarouselIndicatorLines *self)
+{
+ if (self->tick_cb_id == 0)
+ return;
+
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+ self->tick_cb_id = 0;
+}
+
+static void
+animate (HdyCarouselIndicatorLines *self,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->end_time = MAX (self->end_time, frame_time / 1000 + duration);
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ animation_cb,
+ NULL, NULL);
+}
+
+static GdkRGBA
+get_color (GtkWidget *widget)
+{
+ GtkStyleContext *context;
+ GtkStateFlags flags;
+ GdkRGBA color;
+
+ context = gtk_widget_get_style_context (widget);
+ flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_color (context, flags, &color);
+
+ return color;
+}
+
+static void
+draw_lines (GtkWidget *widget,
+ cairo_t *cr,
+ GtkOrientation orientation,
+ gdouble position,
+ gdouble *sizes,
+ guint n_pages)
+{
+ GdkRGBA color;
+ gint i, widget_length, widget_thickness;
+ gdouble indicator_length, full_size, line_size, pos;
+
+ color = get_color (widget);
+
+ line_size = LINE_LENGTH + LINE_SPACING;
+ indicator_length = 0;
+ for (i = 0; i < n_pages; i++)
+ indicator_length += line_size * sizes[i];
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ widget_length = gtk_widget_get_allocated_width (widget);
+ widget_thickness = gtk_widget_get_allocated_height (widget);
+ } else {
+ widget_length = gtk_widget_get_allocated_height (widget);
+ widget_thickness = gtk_widget_get_allocated_width (widget);
+ }
+
+ /* Ensure the indicators are aligned to pixel grid when not animating */
+ full_size = round (indicator_length / line_size) * line_size;
+ if ((widget_length - (gint) full_size) % 2 == 0)
+ widget_length--;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ cairo_translate (cr, (widget_length - indicator_length) / 2.0, (widget_thickness - LINE_WIDTH) / 2);
+ cairo_scale (cr, 1, LINE_WIDTH);
+ } else {
+ cairo_translate (cr, (widget_thickness - LINE_WIDTH) / 2, (widget_length - indicator_length) / 2.0);
+ cairo_scale (cr, LINE_WIDTH, 1);
+ }
+
+ pos = 0;
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * LINE_OPACITY);
+ for (i = 0; i < n_pages; i++) {
+ gdouble length;
+
+ length = (LINE_LENGTH + LINE_SPACING) * sizes[i] - LINE_SPACING;
+
+ if (length > 0) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_rectangle (cr, LINE_SPACING / 2.0 + pos, 0, length, 1);
+ else
+ cairo_rectangle (cr, 0, LINE_SPACING / 2.0 + pos, 1, length);
+ }
+
+ cairo_fill (cr);
+
+ pos += (LINE_LENGTH + LINE_SPACING) * sizes[i];
+ }
+
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * LINE_OPACITY_ACTIVE);
+
+ pos = LINE_SPACING / 2.0 + position * (LINE_LENGTH + LINE_SPACING);
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_rectangle (cr, pos, 0, LINE_LENGTH, 1);
+ else
+ cairo_rectangle (cr, 0, pos, 1, LINE_LENGTH);
+ cairo_fill (cr);
+}
+
+static void
+n_pages_changed_cb (HdyCarouselIndicatorLines *self)
+{
+ animate (self, hdy_carousel_get_reveal_duration (self->carousel));
+}
+
+static void
+hdy_carousel_indicator_lines_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint size = 0;
+
+ if (orientation == self->orientation) {
+ gint n_pages = 0;
+ if (self->carousel)
+ n_pages = hdy_carousel_get_n_pages (self->carousel);
+
+ size = MAX (0, (LINE_LENGTH + LINE_SPACING) * n_pages - LINE_SPACING);
+ } else {
+ size = LINE_WIDTH;
+ }
+
+ size += 2 * LINE_MARGIN;
+
+ if (minimum)
+ *minimum = size;
+
+ if (natural)
+ *natural = size;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_carousel_indicator_lines_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_indicator_lines_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static gboolean
+hdy_carousel_indicator_lines_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint i, n_points;
+ gdouble position;
+ g_autofree gdouble *points = NULL;
+ g_autofree gdouble *sizes = NULL;
+
+ if (!self->carousel)
+ return GDK_EVENT_PROPAGATE;
+
+ points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points);
+ position = hdy_carousel_get_position (self->carousel);
+
+ if (n_points < 2)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ position = points[n_points - 1] - position;
+
+ sizes = g_new0 (gdouble, n_points);
+
+ sizes[0] = points[0] + 1;
+ for (i = 1; i < n_points; i++)
+ sizes[i] = points[i] - points[i - 1];
+
+ draw_lines (widget, cr, self->orientation, position, sizes, n_points);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ hdy_carousel_indicator_lines_set_carousel (self, NULL);
+
+ G_OBJECT_CLASS (hdy_carousel_indicator_lines_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_indicator_lines_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ g_value_set_object (value, hdy_carousel_indicator_lines_get_carousel (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_lines_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ hdy_carousel_indicator_lines_set_carousel (self, g_value_get_object (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_lines_class_init (HdyCarouselIndicatorLinesClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_indicator_lines_get_property;
+ object_class->set_property = hdy_carousel_indicator_lines_set_property;
+
+ widget_class->get_preferred_width = hdy_carousel_indicator_lines_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_indicator_lines_get_preferred_height;
+ widget_class->draw = hdy_carousel_indicator_lines_draw;
+
+ /**
+ * HdyCarouselIndicatorLines:carousel:
+ *
+ * The #HdyCarousel the indicator uses.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAROUSEL] =
+ g_param_spec_object ("carousel",
+ _("Carousel"),
+ _("Carousel"),
+ HDY_TYPE_CAROUSEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "carouselindicatorlines");
+}
+
+static void
+hdy_carousel_indicator_lines_init (HdyCarouselIndicatorLines *self)
+{
+}
+
+/**
+ * hdy_carousel_indicator_lines_new:
+ *
+ * Create a new #HdyCarouselIndicatorLines widget.
+ *
+ * Returns: (transfer full): The newly created #HdyCarouselIndicatorLines widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_indicator_lines_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_LINES, NULL);
+}
+
+/**
+ * hdy_carousel_indicator_lines_get_carousel:
+ * @self: a #HdyCarouselIndicatorLines
+ *
+ * Get the #HdyCarousel the indicator uses.
+ *
+ * See: hdy_carousel_indicator_lines_set_carousel()
+ *
+ * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+
+HdyCarousel *
+hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self), NULL);
+
+ return self->carousel;
+}
+
+/**
+ * hdy_carousel_indicator_lines_set_carousel:
+ * @self: a #HdyCarouselIndicatorLines
+ * @carousel: (nullable): a #HdyCarousel
+ *
+ * Sets the #HdyCarousel to use.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self,
+ HdyCarousel *carousel)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self));
+ g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL);
+
+ if (self->carousel == carousel)
+ return;
+
+ if (self->carousel) {
+ stop_animation (self);
+ g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self);
+ g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self);
+ }
+
+ g_set_object (&self->carousel, carousel);
+
+ if (self->carousel) {
+ g_signal_connect_object (self->carousel, "notify::position",
+ G_CALLBACK (gtk_widget_queue_draw), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->carousel, "notify::n-pages",
+ G_CALLBACK (n_pages_changed_cb), self,
+ G_CONNECT_SWAPPED);
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.h b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h
new file mode 100644
index 0000000..baae57d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-carousel.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_INDICATOR_LINES (hdy_carousel_indicator_lines_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, HDY, CAROUSEL_INDICATOR_LINES, GtkDrawingArea)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_indicator_lines_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyCarousel *hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self,
+ HdyCarousel *carousel);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel.c b/subprojects/libhandy/src/hdy-carousel.c
new file mode 100644
index 0000000..7d8db55
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.c
@@ -0,0 +1,1099 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-carousel-box-private.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define DEFAULT_DURATION 250
+
+/**
+ * SECTION:hdy-carousel
+ * @short_description: A paginated scrolling widget.
+ * @title: HdyCarousel
+ * @See_also: #HdyCarouselIndicatorDots, #HdyCarouselIndicatorLines
+ *
+ * The #HdyCarousel widget can be used to display a set of pages with
+ * swipe-based navigation between them.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarousel has a single CSS node with name carousel.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarousel
+{
+ GtkEventBox parent_instance;
+
+ HdyCarouselBox *scrolling_box;
+
+ HdySwipeTracker *tracker;
+
+ GtkOrientation orientation;
+ guint animation_duration;
+
+ gulong scroll_timeout_id;
+ gboolean can_scroll;
+};
+
+static void hdy_carousel_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarousel, hdy_carousel, GTK_TYPE_EVENT_BOX,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_carousel_swipeable_init))
+
+enum {
+ PROP_0,
+ PROP_N_PAGES,
+ PROP_POSITION,
+ PROP_INTERACTIVE,
+ PROP_SPACING,
+ PROP_ANIMATION_DURATION,
+ PROP_ALLOW_MOUSE_DRAG,
+ PROP_REVEAL_DURATION,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_REVEAL_DURATION + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_PAGE_CHANGED,
+ SIGNAL_LAST_SIGNAL,
+};
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+
+static void
+hdy_carousel_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+ GtkWidget *child;
+
+ child = hdy_carousel_box_get_nth_child (self->scrolling_box, index);
+
+ hdy_carousel_box_scroll_to (self->scrolling_box, child, duration);
+}
+
+static void
+begin_swipe_cb (HdySwipeTracker *tracker,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdyCarousel *self)
+{
+ hdy_carousel_box_stop_animation (self->scrolling_box);
+}
+
+static void
+update_swipe_cb (HdySwipeTracker *tracker,
+ gdouble progress,
+ HdyCarousel *self)
+{
+ hdy_carousel_box_set_position (self->scrolling_box, progress);
+}
+
+static void
+end_swipe_cb (HdySwipeTracker *tracker,
+ gint64 duration,
+ gdouble to,
+ HdyCarousel *self)
+{
+ GtkWidget *child;
+
+ child = hdy_carousel_box_get_page_at_position (self->scrolling_box, to);
+ hdy_carousel_box_scroll_to (self->scrolling_box, child, duration);
+}
+
+static HdySwipeTracker *
+hdy_carousel_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return self->tracker;
+}
+
+static gdouble
+hdy_carousel_get_distance (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_distance (self->scrolling_box);
+}
+
+static gdouble *
+hdy_carousel_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_snap_points (self->scrolling_box,
+ n_snap_points);
+}
+
+static gdouble
+hdy_carousel_get_progress (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_get_position (self);
+}
+
+static gdouble
+hdy_carousel_get_cancel_progress (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_closest_snap_point (self->scrolling_box);
+}
+
+static void
+notify_n_pages_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+notify_position_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
+
+static void
+notify_spacing_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+static void
+notify_reveal_duration_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]);
+}
+
+static void
+animation_stopped_cb (HdyCarousel *self,
+ HdyCarouselBox *box)
+{
+ gint index;
+
+ index = hdy_carousel_box_get_current_page_index (self->scrolling_box);
+
+ g_signal_emit (self, signals[SIGNAL_PAGE_CHANGED], 0, index);
+}
+
+static void
+position_shifted_cb (HdyCarousel *self,
+ gdouble delta,
+ HdyCarouselBox *box)
+{
+ hdy_swipe_tracker_shift_position (self->tracker, delta);
+}
+
+/* Copied from GtkOrientable. Orientable widgets are supposed
+ * to do this manually via a private GTK function. */
+static void
+set_orientable_style_classes (GtkOrientable *orientable)
+{
+ GtkStyleContext *context;
+ GtkOrientation orientation;
+
+ g_return_if_fail (GTK_IS_ORIENTABLE (orientable));
+ g_return_if_fail (GTK_IS_WIDGET (orientable));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (orientable));
+ orientation = gtk_orientable_get_orientation (orientable);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_HORIZONTAL);
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_VERTICAL);
+ }
+ else
+ {
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_VERTICAL);
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_HORIZONTAL);
+ }
+}
+
+static void
+update_orientation (HdyCarousel *self)
+{
+ gboolean reversed;
+
+ if (!self->scrolling_box)
+ return;
+
+ reversed = self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ g_object_set (self->scrolling_box, "orientation", self->orientation, NULL);
+ g_object_set (self->tracker, "orientation", self->orientation,
+ "reversed", reversed, NULL);
+
+ set_orientable_style_classes (GTK_ORIENTABLE (self));
+ set_orientable_style_classes (GTK_ORIENTABLE (self->scrolling_box));
+}
+
+static gboolean
+scroll_timeout_cb (HdyCarousel *self)
+{
+ self->can_scroll = TRUE;
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+scroll_event_cb (HdyCarousel *self,
+ GdkEvent *event)
+{
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ GdkScrollDirection direction;
+ gdouble dx, dy;
+ gint index;
+ gboolean allow_vertical;
+ GtkOrientation orientation;
+ guint duration;
+
+ if (!self->can_scroll)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!hdy_carousel_get_interactive (self))
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type != GDK_SCROLL)
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device (event);
+ input_source = gdk_device_get_source (source_device);
+ if (input_source == GDK_SOURCE_TOUCHPAD)
+ return GDK_EVENT_PROPAGATE;
+
+ /* Mice often don't have easily accessible horizontal scrolling,
+ * hence allow vertical mouse scrolling regardless of orientation */
+ allow_vertical = (input_source == GDK_SOURCE_MOUSE);
+
+ if (gdk_event_get_scroll_direction (event, &direction)) {
+ dx = 0;
+ dy = 0;
+
+ switch (direction) {
+ case GDK_SCROLL_UP:
+ dy = -1;
+ break;
+ case GDK_SCROLL_DOWN:
+ dy = 1;
+ break;
+ case GDK_SCROLL_LEFT:
+ dy = -1;
+ break;
+ case GDK_SCROLL_RIGHT:
+ dy = 1;
+ break;
+ case GDK_SCROLL_SMOOTH:
+ g_assert_not_reached ();
+ default:
+ return GDK_EVENT_PROPAGATE;
+ }
+ } else {
+ gdk_event_get_scroll_deltas (event, &dx, &dy);
+ }
+
+ orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (self));
+ index = 0;
+
+ if (orientation == GTK_ORIENTATION_VERTICAL || allow_vertical) {
+ if (dy > 0)
+ index++;
+ else if (dy < 0)
+ index--;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL && index == 0) {
+ if (dx > 0)
+ index++;
+ else if (dx < 0)
+ index--;
+ }
+
+ if (index == 0)
+ return GDK_EVENT_PROPAGATE;
+
+ index += hdy_carousel_box_get_current_page_index (self->scrolling_box);
+ index = CLAMP (index, 0, (gint) hdy_carousel_get_n_pages (self) - 1);
+
+ hdy_carousel_scroll_to (self, hdy_carousel_box_get_nth_child (self->scrolling_box, index));
+
+ /* Don't allow the delay to go lower than 250ms */
+ duration = MIN (self->animation_duration, DEFAULT_DURATION);
+
+ self->can_scroll = FALSE;
+ g_timeout_add (duration, (GSourceFunc) scroll_timeout_cb, self);
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+hdy_carousel_destroy (GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (widget);
+
+ if (self->scrolling_box) {
+ gtk_widget_destroy (GTK_WIDGET (self->scrolling_box));
+ self->scrolling_box = NULL;
+ }
+
+ GTK_WIDGET_CLASS (hdy_carousel_parent_class)->destroy (widget);
+}
+
+static void
+hdy_carousel_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ HdyCarousel *self = HDY_CAROUSEL (widget);
+
+ update_orientation (self);
+}
+
+static void
+hdy_carousel_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (self->scrolling_box)
+ gtk_container_add (GTK_CONTAINER (self->scrolling_box), widget);
+ else
+ GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->add (container, widget);
+}
+
+static void
+hdy_carousel_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (self->scrolling_box)
+ gtk_container_remove (GTK_CONTAINER (self->scrolling_box), widget);
+ else
+ GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->remove (container, widget);
+}
+
+static void
+hdy_carousel_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (include_internals)
+ (* callback) (GTK_WIDGET (self->scrolling_box), callback_data);
+ else if (self->scrolling_box)
+ gtk_container_foreach (GTK_CONTAINER (self->scrolling_box),
+ callback, callback_data);
+}
+
+static void
+hdy_carousel_constructed (GObject *object)
+{
+ HdyCarousel *self = (HdyCarousel *)object;
+
+ update_orientation (self);
+
+ G_OBJECT_CLASS (hdy_carousel_parent_class)->constructed (object);
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarousel *self = (HdyCarousel *)object;
+
+ g_clear_object (&self->tracker);
+
+ if (self->scroll_timeout_id != 0) {
+ g_source_remove (self->scroll_timeout_id);
+ self->scroll_timeout_id = 0;
+ }
+
+ G_OBJECT_CLASS (hdy_carousel_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarousel *self = HDY_CAROUSEL (object);
+
+ switch (prop_id) {
+ case PROP_N_PAGES:
+ g_value_set_uint (value, hdy_carousel_get_n_pages (self));
+ break;
+
+ case PROP_POSITION:
+ g_value_set_double (value, hdy_carousel_get_position (self));
+ break;
+
+ case PROP_INTERACTIVE:
+ g_value_set_boolean (value, hdy_carousel_get_interactive (self));
+ break;
+
+ case PROP_SPACING:
+ g_value_set_uint (value, hdy_carousel_get_spacing (self));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ g_value_set_boolean (value, hdy_carousel_get_allow_mouse_drag (self));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ g_value_set_uint (value, hdy_carousel_get_reveal_duration (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ case PROP_ANIMATION_DURATION:
+ g_value_set_uint (value, hdy_carousel_get_animation_duration (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarousel *self = HDY_CAROUSEL (object);
+
+ switch (prop_id) {
+ case PROP_INTERACTIVE:
+ hdy_carousel_set_interactive (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SPACING:
+ hdy_carousel_set_spacing (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ANIMATION_DURATION:
+ hdy_carousel_set_animation_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ hdy_carousel_set_reveal_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ hdy_carousel_set_allow_mouse_drag (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ update_orientation (self);
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_carousel_switch_child;
+ iface->get_swipe_tracker = hdy_carousel_get_swipe_tracker;
+ iface->get_distance = hdy_carousel_get_distance;
+ iface->get_snap_points = hdy_carousel_get_snap_points;
+ iface->get_progress = hdy_carousel_get_progress;
+ iface->get_cancel_progress = hdy_carousel_get_cancel_progress;
+}
+
+static void
+hdy_carousel_class_init (HdyCarouselClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->constructed = hdy_carousel_constructed;
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_get_property;
+ object_class->set_property = hdy_carousel_set_property;
+ widget_class->destroy = hdy_carousel_destroy;
+ widget_class->direction_changed = hdy_carousel_direction_changed;
+ container_class->add = hdy_carousel_add;
+ container_class->remove = hdy_carousel_remove;
+ container_class->forall = hdy_carousel_forall;
+
+ /**
+ * HdyCarousel:n-pages:
+ *
+ * The number of pages in a #HdyCarousel
+ *
+ * Since: 1.0
+ */
+ props[PROP_N_PAGES] =
+ g_param_spec_uint ("n-pages",
+ _("Number of pages"),
+ _("Number of pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:position:
+ *
+ * Current scrolling position, unitless. 1 matches 1 page. Use
+ * hdy_carousel_scroll_to() for changing it.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POSITION] =
+ g_param_spec_double ("position",
+ _("Position"),
+ _("Current scrolling position"),
+ 0,
+ G_MAXDOUBLE,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:interactive:
+ *
+ * Whether the carousel can be navigated. This can be used to temporarily
+ * disable a #HdyCarousel to only allow navigating it in a certain state.
+ *
+ * Since: 1.0
+ */
+ props[PROP_INTERACTIVE] =
+ g_param_spec_boolean ("interactive",
+ _("Interactive"),
+ _("Whether the widget can be swiped"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:spacing:
+ *
+ * Spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SPACING] =
+ g_param_spec_uint ("spacing",
+ _("Spacing"),
+ _("Spacing between pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:animation-duration:
+ *
+ * Animation duration in milliseconds, used by hdy_carousel_scroll_to().
+ *
+ * Since: 1.0
+ */
+ props[PROP_ANIMATION_DURATION] =
+ g_param_spec_uint ("animation-duration",
+ _("Animation duration"),
+ _("Default animation duration"),
+ 0, G_MAXUINT, DEFAULT_DURATION,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:allow-mouse-drag:
+ *
+ * Sets whether the #HdyCarousel can be dragged with mouse pointer. If the
+ * value is %FALSE, dragging is only available on touch.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ALLOW_MOUSE_DRAG] =
+ g_param_spec_boolean ("allow-mouse-drag",
+ _("Allow mouse drag"),
+ _("Whether to allow dragging with mouse pointer"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:reveal-duration:
+ *
+ * Page reveal duration in milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVEAL_DURATION] =
+ g_param_spec_uint ("reveal-duration",
+ _("Reveal duration"),
+ _("Page reveal duration"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyCarousel::page-changed:
+ * @self: The #HdyCarousel instance
+ * @index: Current page
+ *
+ * This signal is emitted after a page has been changed. This can be used to
+ * implement "infinite scrolling" by connecting to this signal and amending
+ * the pages.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_PAGE_CHANGED] =
+ g_signal_new ("page-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_UINT);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-carousel.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyCarousel, scrolling_box);
+ gtk_widget_class_bind_template_callback (widget_class, scroll_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_n_pages_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_position_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_spacing_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_reveal_duration_cb);
+ gtk_widget_class_bind_template_callback (widget_class, animation_stopped_cb);
+ gtk_widget_class_bind_template_callback (widget_class, position_shifted_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "carousel");
+}
+
+static void
+hdy_carousel_init (HdyCarousel *self)
+{
+ g_type_ensure (HDY_TYPE_CAROUSEL_BOX);
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->animation_duration = DEFAULT_DURATION;
+
+ self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self));
+ hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, TRUE);
+
+ g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0);
+
+ self->can_scroll = TRUE;
+}
+
+/**
+ * hdy_carousel_new:
+ *
+ * Create a new #HdyCarousel widget.
+ *
+ * Returns: The newly created #HdyCarousel widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL, NULL);
+}
+
+/**
+ * hdy_carousel_prepend:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ *
+ * Prepends @child to @self
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_prepend (HdyCarousel *self,
+ GtkWidget *widget)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_insert (self->scrolling_box, widget, 0);
+}
+
+/**
+ * hdy_carousel_insert:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ * @position: the position to insert @child in.
+ *
+ * Inserts @child into @self at position @position.
+ *
+ * If position is -1, or larger than the number of pages,
+ * @child will be appended to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_insert (HdyCarousel *self,
+ GtkWidget *widget,
+ gint position)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_insert (self->scrolling_box, widget, position);
+}
+/**
+ * hdy_carousel_reorder:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ * @position: the position to move @child to.
+ *
+ * Moves @child into position @position.
+ *
+ * If position is -1, or larger than the number of pages, @child will be moved
+ * to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_reorder (HdyCarousel *self,
+ GtkWidget *child,
+ gint position)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+
+ hdy_carousel_box_reorder (self->scrolling_box, child, position);
+}
+
+/**
+ * hdy_carousel_scroll_to:
+ * @self: a #HdyCarousel
+ * @widget: a child of @self
+ *
+ * Scrolls to @widget position with an animation.
+ * #HdyCarousel:animation-duration property can be used for controlling the
+ * duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_scroll_to (HdyCarousel *self,
+ GtkWidget *widget)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_scroll_to_full (self, widget, self->animation_duration);
+}
+
+/**
+ * hdy_carousel_scroll_to_full:
+ * @self: a #HdyCarousel
+ * @widget: a child of @self
+ * @duration: animation duration in milliseconds
+ *
+ * Scrolls to @widget position with an animation.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_scroll_to_full (HdyCarousel *self,
+ GtkWidget *widget,
+ gint64 duration)
+{
+ GList *children;
+ gint n;
+
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ children = gtk_container_get_children (GTK_CONTAINER (self->scrolling_box));
+ n = g_list_index (children, widget);
+ g_list_free (children);
+
+ hdy_carousel_box_scroll_to (self->scrolling_box, widget,
+ duration);
+ hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self), n, duration);
+}
+
+/**
+ * hdy_carousel_get_n_pages:
+ * @self: a #HdyCarousel
+ *
+ * Gets the number of pages in @self.
+ *
+ * Returns: The number of pages in @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_n_pages (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_n_pages (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_get_position:
+ * @self: a #HdyCarousel
+ *
+ * Gets current scroll position in @self. It's unitless, 1 matches 1 page.
+ *
+ * Returns: The scroll position
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_get_position (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_position (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_get_interactive
+ * @self: a #HdyCarousel
+ *
+ * Gets whether @self can be navigated.
+ *
+ * Returns: %TRUE if @self can be swiped
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_get_interactive (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE);
+
+ return hdy_swipe_tracker_get_enabled (self->tracker);
+}
+
+/**
+ * hdy_carousel_set_interactive
+ * @self: a #HdyCarousel
+ * @interactive: whether @self can be swiped.
+ *
+ * Sets whether @self can be navigated. This can be used to temporarily disable
+ * a #HdyCarousel to only allow swiping in a certain state.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_interactive (HdyCarousel *self,
+ gboolean interactive)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ interactive = !!interactive;
+
+ if (hdy_swipe_tracker_get_enabled (self->tracker) == interactive)
+ return;
+
+ hdy_swipe_tracker_set_enabled (self->tracker, interactive);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERACTIVE]);
+}
+
+/**
+ * hdy_carousel_get_spacing:
+ * @self: a #HdyCarousel
+ *
+ * Gets spacing between pages in pixels.
+ *
+ * Returns: Spacing between pages
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_spacing (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_spacing (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_set_spacing:
+ * @self: a #HdyCarousel
+ * @spacing: the new spacing value
+ *
+ * Sets spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_spacing (HdyCarousel *self,
+ guint spacing)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_set_spacing (self->scrolling_box, spacing);
+}
+
+/**
+ * hdy_carousel_get_animation_duration:
+ * @self: a #HdyCarousel
+ *
+ * Gets animation duration used by hdy_carousel_scroll_to().
+ *
+ * Returns: Animation duration in milliseconds
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_animation_duration (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return self->animation_duration;
+}
+
+/**
+ * hdy_carousel_set_animation_duration:
+ * @self: a #HdyCarousel
+ * @duration: animation duration in milliseconds
+ *
+ * Sets animation duration used by hdy_carousel_scroll_to().
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_animation_duration (HdyCarousel *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ if (self->animation_duration == duration)
+ return;
+
+ self->animation_duration = duration;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ANIMATION_DURATION]);
+}
+
+/**
+ * hdy_carousel_get_allow_mouse_drag:
+ * @self: a #HdyCarousel
+ *
+ * Sets whether @self can be dragged with mouse pointer
+ *
+ * Returns: %TRUE if @self can be dragged with mouse
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_get_allow_mouse_drag (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE);
+
+ return hdy_swipe_tracker_get_allow_mouse_drag (self->tracker);
+}
+
+/**
+ * hdy_carousel_set_allow_mouse_drag:
+ * @self: a #HdyCarousel
+ * @allow_mouse_drag: whether @self can be dragged with mouse pointer
+ *
+ * Sets whether @self can be dragged with mouse pointer. If @allow_mouse_drag
+ * is %FALSE, dragging is only available on touch.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_allow_mouse_drag (HdyCarousel *self,
+ gboolean allow_mouse_drag)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ allow_mouse_drag = !!allow_mouse_drag;
+
+ if (hdy_carousel_get_allow_mouse_drag (self) == allow_mouse_drag)
+ return;
+
+ hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, allow_mouse_drag);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * hdy_carousel_get_reveal_duration:
+ * @self: a #HdyCarousel
+ *
+ * Gets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Returns: Page reveal duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_reveal_duration (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_reveal_duration (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_set_reveal_duration:
+ * @self: a #HdyCarousel
+ * @reveal_duration: the new reveal duration value
+ *
+ * Sets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_reveal_duration (HdyCarousel *self,
+ guint reveal_duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_set_reveal_duration (self->scrolling_box, reveal_duration);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel.h b/subprojects/libhandy/src/hdy-carousel.h
new file mode 100644
index 0000000..4318b65
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL (hdy_carousel_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarousel, hdy_carousel, HDY, CAROUSEL, GtkEventBox)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_prepend (HdyCarousel *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_insert (HdyCarousel *self,
+ GtkWidget *child,
+ gint position);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_reorder (HdyCarousel *self,
+ GtkWidget *child,
+ gint position);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_scroll_to (HdyCarousel *self,
+ GtkWidget *widget);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_scroll_to_full (HdyCarousel *self,
+ GtkWidget *widget,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_n_pages (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_carousel_get_position (HdyCarousel *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_carousel_get_interactive (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_interactive (HdyCarousel *self,
+ gboolean interactive);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_spacing (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_spacing (HdyCarousel *self,
+ guint spacing);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_animation_duration (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_animation_duration (HdyCarousel *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_carousel_get_allow_mouse_drag (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_allow_mouse_drag (HdyCarousel *self,
+ gboolean allow_mouse_drag);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_reveal_duration (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_reveal_duration (HdyCarousel *self,
+ guint reveal_duration);
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel.ui b/subprojects/libhandy/src/hdy-carousel.ui
new file mode 100644
index 0000000..c9bf553
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyCarousel" parent="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="orientation">horizontal</property>
+ <signal name="scroll-event" handler="scroll_event_cb"/>
+ <child>
+ <object class="HdyCarouselBox" id="scrolling_box">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <signal name="notify::n-pages" handler="notify_n_pages_cb" swapped="true"/>
+ <signal name="notify::position" handler="notify_position_cb" swapped="true"/>
+ <signal name="notify::spacing" handler="notify_spacing_cb" swapped="true"/>
+ <signal name="notify::reveal-duration" handler="notify_reveal_duration_cb" swapped="true"/>
+ <signal name="animation-stopped" handler="animation_stopped_cb" swapped="true"/>
+ <signal name="position-shifted" handler="position_shifted_cb" swapped="true"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-clamp.c b/subprojects/libhandy/src/hdy-clamp.c
new file mode 100644
index 0000000..9cb9f23
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-clamp.c
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-clamp.h"
+
+#include <glib/gi18n-lib.h>
+#include <math.h>
+
+#include "hdy-animation-private.h"
+
+/**
+ * SECTION:hdy-clamp
+ * @short_description: A container constraining its child to a given size.
+ * @Title: HdyClamp
+ *
+ * The #HdyClamp widget constraints the size of the widget it contains to a
+ * given maximum size. It will constrain the width if it is horizontal, or the
+ * height if it is vertical. The expansion of the child from its minimum to its
+ * maximum size is eased out for a smooth transition.
+ *
+ * If the child requires more than the requested maximum size, it will be
+ * allocated the minimum size it can fit in instead.
+ *
+ * # CSS nodes
+ *
+ * #HdyClamp has a single CSS node with name clamp. The node will get the style
+ * classes .large when its child reached its maximum size, .small when the clamp
+ * allocates its full size to its child, .medium in-between, or none if it
+ * didn't compute its size yet.
+ *
+ * Since: 1.0
+ */
+
+#define HDY_EASE_OUT_TAN_CUBIC 3
+
+enum {
+ PROP_0,
+ PROP_MAXIMUM_SIZE,
+ PROP_TIGHTENING_THRESHOLD,
+
+ /* Overridden properties */
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_TIGHTENING_THRESHOLD + 1,
+};
+
+struct _HdyClamp
+{
+ GtkBin parent_instance;
+
+ gint maximum_size;
+ gint tightening_threshold;
+
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdyClamp, hdy_clamp, GTK_TYPE_BIN,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static void
+set_orientation (HdyClamp *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_clamp_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyClamp *self = HDY_CLAMP (object);
+
+ switch (prop_id) {
+ case PROP_MAXIMUM_SIZE:
+ g_value_set_int (value, hdy_clamp_get_maximum_size (self));
+ break;
+ case PROP_TIGHTENING_THRESHOLD:
+ g_value_set_int (value, hdy_clamp_get_tightening_threshold (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_clamp_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyClamp *self = HDY_CLAMP (object);
+
+ switch (prop_id) {
+ case PROP_MAXIMUM_SIZE:
+ hdy_clamp_set_maximum_size (self, g_value_get_int (value));
+ break;
+ case PROP_TIGHTENING_THRESHOLD:
+ hdy_clamp_set_tightening_threshold (self, g_value_get_int (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+/**
+ * get_child_size:
+ * @self: a #HdyClamp
+ * @for_size: the size of the clamp
+ * @child_minimum: the minimum size reachable by the child, and hence by @self
+ * @child_maximum: the maximum size @self will ever allocate its child
+ * @lower_threshold: the threshold below which @self will allocate its full size to its child
+ * @upper_threshold: the threshold up from which @self will allocate its maximum size to its child
+ *
+ * Measures the child's extremes, the clamp's thresholds, and returns size to
+ * allocate to the child.
+ *
+ * If the clamp is horizontal, all values are widths, otherwise they are
+ * heights.
+ */
+static gint
+get_child_size (HdyClamp *self,
+ gint for_size,
+ gint *child_minimum,
+ gint *child_maximum,
+ gint *lower_threshold,
+ gint *upper_threshold)
+{
+ GtkBin *bin = GTK_BIN (self);
+ GtkWidget *child;
+ gint min = 0, max = 0, lower = 0, upper = 0;
+ gdouble amplitude, progress;
+
+ child = gtk_bin_get_child (bin);
+ if (child == NULL)
+ return 0;
+
+ if (gtk_widget_get_visible (child)) {
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+ gtk_widget_get_preferred_width (child, &min, NULL);
+ else
+ gtk_widget_get_preferred_height (child, &min, NULL);
+ }
+
+ lower = MAX (MIN (self->tightening_threshold, self->maximum_size), min);
+ max = MAX (lower, self->maximum_size);
+ amplitude = max - lower;
+ upper = HDY_EASE_OUT_TAN_CUBIC * amplitude + lower;
+
+ if (child_minimum)
+ *child_minimum = min;
+ if (child_maximum)
+ *child_maximum = max;
+ if (lower_threshold)
+ *lower_threshold = lower;
+ if (upper_threshold)
+ *upper_threshold = upper;
+
+ if (for_size < 0)
+ return 0;
+
+ if (for_size <= lower)
+ return for_size;
+
+ if (for_size >= upper)
+ return max;
+
+ progress = (double) (for_size - lower) / (double) (upper - lower);
+
+ return hdy_ease_out_cubic (progress) * amplitude + lower;
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_clamp_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+ GtkBin *bin = GTK_BIN (widget);
+ GtkWidget *child;
+ gint child_size;
+
+ if (minimum)
+ *minimum = 0;
+ if (natural)
+ *natural = 0;
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+ if (natural_baseline)
+ *natural_baseline = -1;
+
+ child = gtk_bin_get_child (bin);
+ if (!(child && gtk_widget_get_visible (child)))
+ return;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (self->orientation == orientation) {
+ gtk_widget_get_preferred_width (child, minimum, natural);
+
+ return;
+ }
+
+ child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL);
+
+ gtk_widget_get_preferred_width_for_height (child,
+ child_size,
+ minimum,
+ natural);
+ } else {
+ if (self->orientation == orientation) {
+ gtk_widget_get_preferred_height (child, minimum, natural);
+
+ return;
+ }
+
+ child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL);
+
+ gtk_widget_get_preferred_height_and_baseline_for_width (child,
+ child_size,
+ minimum,
+ natural,
+ minimum_baseline,
+ natural_baseline);
+ }
+}
+
+static GtkSizeRequestMode
+hdy_clamp_get_request_mode (GtkWidget *widget)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+
+ return self->orientation == GTK_ORIENTATION_HORIZONTAL ?
+ GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH :
+ GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+}
+
+static void
+hdy_clamp_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_height_and_baseline_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_clamp_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+ GtkBin *bin = GTK_BIN (widget);
+ GtkAllocation child_allocation;
+ gint baseline;
+ GtkWidget *child;
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gint child_maximum = 0, lower_threshold = 0;
+ gint child_clamped_size;
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ child = gtk_bin_get_child (bin);
+ if (!(child && gtk_widget_get_visible (child))) {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+
+ return;
+ }
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_allocation.width = get_child_size (self, allocation->width, NULL, &child_maximum, &lower_threshold, NULL);
+ child_allocation.height = allocation->height;
+
+ child_clamped_size = child_allocation.width;
+ }
+ else {
+ child_allocation.width = allocation->width;
+ child_allocation.height = get_child_size (self, allocation->height, NULL, &child_maximum, &lower_threshold, NULL);
+
+ child_clamped_size = child_allocation.height;
+ }
+
+ if (child_clamped_size >= child_maximum) {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_add_class (context, "large");
+ } else if (child_clamped_size <= lower_threshold) {
+ gtk_style_context_add_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+ } else {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_add_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+ }
+
+ if (!gtk_widget_get_has_window (widget)) {
+ /* This always center the child on the side of the orientation. */
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_allocation.x = allocation->x + (allocation->width - child_allocation.width) / 2;
+ child_allocation.y = allocation->y;
+ } else {
+ child_allocation.x = allocation->x;
+ child_allocation.y = allocation->y + (allocation->height - child_allocation.height) / 2;
+ }
+ }
+ else {
+ child_allocation.x = 0;
+ child_allocation.y = 0;
+ }
+
+ baseline = gtk_widget_get_allocated_baseline (widget);
+ gtk_widget_size_allocate_with_baseline (child, &child_allocation, baseline);
+}
+
+static void
+hdy_clamp_class_init (HdyClampClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_clamp_get_property;
+ object_class->set_property = hdy_clamp_set_property;
+
+ widget_class->get_request_mode = hdy_clamp_get_request_mode;
+ widget_class->get_preferred_width = hdy_clamp_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_clamp_get_preferred_width_for_height;
+ widget_class->get_preferred_height = hdy_clamp_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_clamp_get_preferred_height_for_width;
+ widget_class->get_preferred_height_and_baseline_for_width = hdy_clamp_get_preferred_height_and_baseline_for_width;
+ widget_class->size_allocate = hdy_clamp_size_allocate;
+
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyClamp:maximum-size:
+ *
+ * The maximum size to allocate to the child. It is the width if the clamp is
+ * horizontal, or the height if it is vertical.
+ *
+ * Since: 1.0
+ */
+ props[PROP_MAXIMUM_SIZE] =
+ g_param_spec_int ("maximum-size",
+ _("Maximum size"),
+ _("The maximum size allocated to the child"),
+ 0, G_MAXINT, 600,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyClamp:tightening-threshold:
+ *
+ * The size starting from which the clamp will tighten its grip on the child,
+ * slowly allocating less and less of the available size up to the maximum
+ * allocated size. Below that threshold and below the maximum width, the child
+ * will be allocated all the available size.
+ *
+ * If the threshold is greater than the maximum size to allocate to the child,
+ * the child will be allocated all the width up to the maximum.
+ * If the threshold is lower than the minimum size to allocate to the child,
+ * that size will be used as the tightening threshold.
+ *
+ * Effectively, tightening the grip on the child before it reaches its maximum
+ * size makes transitions to and from the maximum size smoother when resizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TIGHTENING_THRESHOLD] =
+ g_param_spec_int ("tightening-threshold",
+ _("Tightening threshold"),
+ _("The size from which the clamp will tighten its grip on the child"),
+ 0, G_MAXINT, 400,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "clamp");
+}
+
+static void
+hdy_clamp_init (HdyClamp *self)
+{
+ self->maximum_size = 600;
+ self->tightening_threshold = 400;
+}
+
+/**
+ * hdy_clamp_new:
+ *
+ * Creates a new #HdyClamp.
+ *
+ * Returns: a new #HdyClamp
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_clamp_new (void)
+{
+ return g_object_new (HDY_TYPE_CLAMP, NULL);
+}
+
+/**
+ * hdy_clamp_get_maximum_size:
+ * @self: a #HdyClamp
+ *
+ * Gets the maximum size to allocate to the contained child. It is the width if
+ * @self is horizontal, or the height if it is vertical.
+ *
+ * Returns: the maximum width to allocate to the contained child.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_clamp_get_maximum_size (HdyClamp *self)
+{
+ g_return_val_if_fail (HDY_IS_CLAMP (self), 0);
+
+ return self->maximum_size;
+}
+
+/**
+ * hdy_clamp_set_maximum_size:
+ * @self: a #HdyClamp
+ * @maximum_size: the maximum size
+ *
+ * Sets the maximum size to allocate to the contained child. It is the width if
+ * @self is horizontal, or the height if it is vertical.
+ *
+ * Since: 1.0
+ */
+void
+hdy_clamp_set_maximum_size (HdyClamp *self,
+ gint maximum_size)
+{
+ g_return_if_fail (HDY_IS_CLAMP (self));
+
+ if (self->maximum_size == maximum_size)
+ return;
+
+ self->maximum_size = maximum_size;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAXIMUM_SIZE]);
+}
+
+/**
+ * hdy_clamp_get_tightening_threshold:
+ * @self: a #HdyClamp
+ *
+ * Gets the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Returns: the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_clamp_get_tightening_threshold (HdyClamp *self)
+{
+ g_return_val_if_fail (HDY_IS_CLAMP (self), 0);
+
+ return self->tightening_threshold;
+}
+
+/**
+ * hdy_clamp_set_tightening_threshold:
+ * @self: a #HdyClamp
+ * @tightening_threshold: the tightening threshold
+ *
+ * Sets the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_clamp_set_tightening_threshold (HdyClamp *self,
+ gint tightening_threshold)
+{
+ g_return_if_fail (HDY_IS_CLAMP (self));
+
+ if (self->tightening_threshold == tightening_threshold)
+ return;
+
+ self->tightening_threshold = tightening_threshold;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIGHTENING_THRESHOLD]);
+}
diff --git a/subprojects/libhandy/src/hdy-clamp.h b/subprojects/libhandy/src/hdy-clamp.h
new file mode 100644
index 0000000..46ad6dd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-clamp.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CLAMP (hdy_clamp_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyClamp, hdy_clamp, HDY, CLAMP, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_clamp_new (void);
+HDY_AVAILABLE_IN_ALL
+gint hdy_clamp_get_maximum_size (HdyClamp *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_clamp_set_maximum_size (HdyClamp *self,
+ gint maximum_size);
+HDY_AVAILABLE_IN_ALL
+gint hdy_clamp_get_tightening_threshold (HdyClamp *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_clamp_set_tightening_threshold (HdyClamp *self,
+ gint tightening_threshold);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-combo-row.c b/subprojects/libhandy/src/hdy-combo-row.c
new file mode 100644
index 0000000..e9cc6bf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.c
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-combo-row.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-combo-row
+ * @short_description: A #GtkListBox row used to choose from a list of items.
+ * @Title: HdyComboRow
+ *
+ * The #HdyComboRow widget allows the user to choose from a list of valid
+ * choices. The row displays the selected choice. When activated, the row
+ * displays a popover which allows the user to make a new choice.
+ *
+ * The #HdyComboRow uses the model-view pattern; the list of valid choices
+ * is specified in the form of a #GListModel, and the display of the choices can
+ * be adapted to the data in the model via widget creation functions.
+ *
+ * #HdyComboRow is #GtkListBoxRow:activatable if a model is set.
+ *
+ * # CSS nodes
+ *
+ * #HdyComboRow has a main CSS node with name row.
+ *
+ * Its popover has the node name popover with the .combo style class, it
+ * contains a #GtkScrolledWindow, which in turn contains a #GtkListBox, both are
+ * accessible via their regular nodes.
+ *
+ * A checkmark of node and style class image.checkmark in the popover denotes
+ * the current item.
+ *
+ * Since: 0.0.6
+ */
+
+/*
+ * This was mostly inspired by code from the display panel from GNOME Settings.
+ */
+
+typedef struct
+{
+ HdyComboRowGetNameFunc func;
+ gpointer func_data;
+ GDestroyNotify func_data_destroy;
+} HdyComboRowGetName;
+
+typedef struct
+{
+ GtkBox *current;
+ GtkImage *image;
+ GtkListBox *list;
+ GtkPopover *popover;
+ gint selected_index;
+ gboolean use_subtitle;
+ HdyComboRowGetName *get_name;
+
+ GListModel *bound_model;
+ GtkListBoxCreateWidgetFunc create_list_widget_func;
+ GtkListBoxCreateWidgetFunc create_current_widget_func;
+ gpointer create_widget_func_data;
+ GDestroyNotify create_widget_func_data_free_func;
+ /* This is owned by create_widget_func_data, which is ultimately owned by the
+ * list box, and hence should not be destroyed manually.
+ */
+ HdyComboRowGetName *get_name_internal;
+} HdyComboRowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyComboRow, hdy_combo_row, HDY_TYPE_ACTION_ROW)
+
+enum {
+ PROP_0,
+ PROP_SELECTED_INDEX,
+ PROP_USE_SUBTITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static GtkWidget *
+create_list_label (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data;
+ g_autofree gchar *name = get_name->func (item, get_name->func_data);
+
+ return g_object_new (GTK_TYPE_LABEL,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "label", name,
+ "max-width-chars", 20,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ "xalign", 0.0,
+ NULL);
+}
+
+static GtkWidget *
+create_current_label (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data;
+ g_autofree gchar *name = NULL;
+
+ if (get_name->func)
+ name = get_name->func (item, get_name->func_data);
+
+ return g_object_new (GTK_TYPE_LABEL,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "halign", GTK_ALIGN_END,
+ "label", name,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ "xalign", 0.0,
+ NULL);
+}
+
+static void
+create_list_widget_data_free (gpointer user_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (priv->create_widget_func_data_free_func)
+ priv->create_widget_func_data_free_func (priv->create_widget_func_data);
+}
+
+static GtkWidget *
+create_list_widget (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+ GtkWidget *checkmark = g_object_new (GTK_TYPE_IMAGE,
+ "halign", GTK_ALIGN_START,
+ "icon-name", "emblem-ok-symbolic",
+ "valign", GTK_ALIGN_CENTER,
+ NULL);
+ GtkWidget *box = g_object_new (GTK_TYPE_BOX,
+ "child", priv->create_list_widget_func (item, priv->create_widget_func_data),
+ "child", checkmark,
+ "halign", GTK_ALIGN_START,
+ "spacing", 6,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ NULL);
+ GtkStyleContext *checkmark_context = gtk_widget_get_style_context (checkmark);
+
+ gtk_style_context_add_class (checkmark_context, "checkmark");
+
+ g_object_set_data (G_OBJECT (box), "checkmark", checkmark);
+
+ return box;
+}
+
+static void
+get_name_free (HdyComboRowGetName *get_name)
+{
+ if (get_name == NULL)
+ return;
+
+ if (get_name->func_data_destroy)
+ get_name->func_data_destroy (get_name->func_data);
+ get_name->func = NULL;
+ get_name->func_data = NULL;
+ get_name->func_data_destroy = NULL;
+
+ g_free (get_name);
+}
+
+static void
+update (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+ g_autoptr(GObject) item = NULL;
+ g_autofree gchar *name = NULL;
+ GtkWidget *widget;
+ guint n_items = priv->bound_model ? g_list_model_get_n_items (priv->bound_model) : 0;
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->current), !priv->use_subtitle);
+ gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), n_items > 0);
+ gtk_widget_set_visible (GTK_WIDGET (priv->image), n_items > 1);
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), n_items > 1);
+
+ if (n_items == 0) {
+ g_assert (priv->selected_index == -1);
+
+ return;
+ }
+
+ g_assert (priv->selected_index >= 0 && priv->selected_index <= n_items);
+
+ {
+ g_autoptr (GList) rows = gtk_container_get_children (GTK_CONTAINER (priv->list));
+ GList *l;
+ int i = 0;
+
+ for (l = rows; l; l = l->next) {
+ GtkWidget *row = GTK_WIDGET (l->data);
+ GtkWidget *box = gtk_bin_get_child (GTK_BIN (row));
+
+ gtk_widget_set_visible (GTK_WIDGET (g_object_get_data (G_OBJECT (box), "checkmark")),
+ priv->selected_index == i++);
+ }
+ }
+
+ item = g_list_model_get_item (priv->bound_model, priv->selected_index);
+ if (priv->use_subtitle) {
+ if (priv->get_name != NULL && priv->get_name->func)
+ name = priv->get_name->func (item, priv->get_name->func_data);
+ else if (priv->get_name_internal != NULL && priv->get_name_internal->func)
+ name = priv->get_name_internal->func (item, priv->get_name_internal->func_data);
+ hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), name);
+ }
+ else {
+ widget = priv->create_current_widget_func (item, priv->create_widget_func_data);
+ gtk_container_add (GTK_CONTAINER (priv->current), widget);
+ }
+}
+
+static void
+bound_model_changed (GListModel *list,
+ guint index,
+ guint removed,
+ guint added,
+ gpointer user_data)
+{
+ gint new_idx;
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ /* Selection is in front of insertion/removal point, nothing to do */
+ if (priv->selected_index > 0 && priv->selected_index < index)
+ return;
+
+ if (priv->selected_index < index + removed) {
+ /* The item selected item was removed (or none is selected) */
+ new_idx = -1;
+ } else {
+ /* The item selected item was behind the insertion/removal */
+ new_idx = priv->selected_index + added - removed;
+ }
+
+ /* Select the first item if none is selected. */
+ if (new_idx == -1 && g_list_model_get_n_items (list) > 0)
+ new_idx = 0;
+
+ hdy_combo_row_set_selected_index (self, new_idx);
+}
+
+static void
+row_activated_cb (HdyComboRow *self,
+ GtkListBoxRow *row)
+{
+ hdy_combo_row_set_selected_index (self, gtk_list_box_row_get_index (row));
+}
+
+static void
+hdy_combo_row_activate (HdyActionRow *row)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (row);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (gtk_widget_get_visible (GTK_WIDGET (priv->image)))
+ gtk_popover_popup (priv->popover);
+}
+
+static void
+destroy_model (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (!priv->bound_model)
+ return;
+
+ /* Disconnect the bound model *before* releasing it. */
+ g_signal_handlers_disconnect_by_func (priv->bound_model, bound_model_changed, self);
+
+ /* Destroy the model and the user data. */
+ if (priv->list)
+ gtk_list_box_bind_model (priv->list, NULL, NULL, NULL, NULL);
+
+ priv->bound_model = NULL;
+ priv->create_list_widget_func = NULL;
+ priv->create_current_widget_func = NULL;
+ priv->create_widget_func_data = NULL;
+ priv->create_widget_func_data_free_func = NULL;
+}
+
+static void
+hdy_combo_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SELECTED_INDEX:
+ g_value_set_int (value, hdy_combo_row_get_selected_index (self));
+ break;
+ case PROP_USE_SUBTITLE:
+ g_value_set_boolean (value, hdy_combo_row_get_use_subtitle (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_combo_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SELECTED_INDEX:
+ hdy_combo_row_set_selected_index (self, g_value_get_int (value));
+ break;
+ case PROP_USE_SUBTITLE:
+ hdy_combo_row_set_use_subtitle (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_combo_row_dispose (GObject *object)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ destroy_model (self);
+ g_clear_pointer (&priv->get_name, get_name_free);
+
+ G_OBJECT_CLASS (hdy_combo_row_parent_class)->dispose (object);
+}
+
+typedef struct {
+ HdyComboRow *row;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (data->row);
+
+ if (widget != (GtkWidget *) priv->current &&
+ widget != (GtkWidget *) priv->image)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_combo_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (container);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.row = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_combo_row_class_init (HdyComboRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+ HdyActionRowClass *row_class = HDY_ACTION_ROW_CLASS (klass);
+
+ object_class->get_property = hdy_combo_row_get_property;
+ object_class->set_property = hdy_combo_row_set_property;
+ object_class->dispose = hdy_combo_row_dispose;
+
+ container_class->forall = hdy_combo_row_forall;
+
+ row_class->activate = hdy_combo_row_activate;
+
+ /**
+ * HdyComboRow:selected-index:
+ *
+ * The index of the selected item in its #GListModel.
+ *
+ * Since: 0.0.7
+ */
+ props[PROP_SELECTED_INDEX] =
+ g_param_spec_int ("selected-index",
+ _("Selected index"),
+ _("The index of the selected item"),
+ -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyComboRow:use-subtitle:
+ *
+ * %TRUE to set the current value as the subtitle.
+ *
+ * If you use a custom widget creation function, you will need to give the row
+ * a name conversion closure with hdy_combo_row_set_get_name_func().
+ *
+ * If %TRUE, you should not access HdyActionRow:subtitle.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_USE_SUBTITLE] =
+ g_param_spec_boolean ("use-subtitle",
+ _("Use subtitle"),
+ _("Set the current value as the subtitle"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-combo-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, current);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, list);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, popover);
+}
+
+static void
+hdy_combo_row_init (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ priv->selected_index = -1;
+
+ g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (gtk_widget_hide),
+ priv->popover, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (row_activated_cb),
+ self, G_CONNECT_SWAPPED);
+
+ update (self);
+}
+
+/**
+ * hdy_combo_row_new:
+ *
+ * Creates a new #HdyComboRow.
+ *
+ * Returns: a new #HdyComboRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_combo_row_new (void)
+{
+ return g_object_new (HDY_TYPE_COMBO_ROW, NULL);
+}
+
+/**
+ * hdy_combo_row_get_model:
+ * @self: a #HdyComboRow
+ *
+ * Gets the model bound to @self, or %NULL if none is bound.
+ *
+ * Returns: (transfer none) (nullable): the #GListModel bound to @self or %NULL
+ *
+ * Since: 0.0.6
+ */
+GListModel *
+hdy_combo_row_get_model (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), NULL);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->bound_model;
+}
+
+/**
+ * hdy_combo_row_bind_model:
+ * @self: a #HdyComboRow
+ * @model: (nullable): the #GListModel to be bound to @self
+ * @create_list_widget_func: (nullable) (scope call): a function that creates
+ * widgets for items to display in the list, or %NULL in case you also passed
+ * %NULL as @model
+ * @create_current_widget_func: (nullable) (scope call): a function that creates
+ * widgets for items to display as the selected item, or %NULL in case you
+ * also passed %NULL as @model
+ * @user_data: user data passed to @create_list_widget_func and
+ * @create_current_widget_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Binds @model to @self.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_bind_model (HdyComboRow *self,
+ GListModel *model,
+ GtkListBoxCreateWidgetFunc create_list_widget_func,
+ GtkListBoxCreateWidgetFunc create_current_widget_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+ g_return_if_fail (model == NULL || create_list_widget_func != NULL);
+ g_return_if_fail (model == NULL || create_current_widget_func != NULL);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ destroy_model (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL);
+ priv->selected_index = -1;
+
+ if (model == NULL) {
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+ return;
+ }
+
+ /* We don't need take a reference as the list box holds one for us. */
+ priv->bound_model = model;
+ priv->create_list_widget_func = create_list_widget_func;
+ priv->create_current_widget_func = create_current_widget_func;
+ priv->create_widget_func_data = user_data;
+ priv->create_widget_func_data_free_func = user_data_free_func;
+
+ g_signal_connect (priv->bound_model, "items-changed", G_CALLBACK (bound_model_changed), self);
+
+ if (g_list_model_get_n_items (priv->bound_model) > 0)
+ priv->selected_index = 0;
+
+ gtk_list_box_bind_model (priv->list, model, create_list_widget, self, create_list_widget_data_free);
+
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+}
+
+/**
+ * hdy_combo_row_bind_name_model:
+ * @self: a #HdyComboRow
+ * @model: (nullable): the #GListModel to be bound to @self
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Binds @model to @self.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * This is more convenient to use than hdy_combo_row_bind_model() if you want to
+ * represent items of the model with names.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_bind_name_model (HdyComboRow *self,
+ GListModel *model,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+ g_return_if_fail (model == NULL || get_name_func != NULL);
+
+ priv->get_name_internal = g_new0 (HdyComboRowGetName, 1);
+ priv->get_name_internal->func = get_name_func;
+ priv->get_name_internal->func_data = user_data;
+ priv->get_name_internal->func_data_destroy = user_data_free_func;
+
+ hdy_combo_row_bind_model (self, model, create_list_label, create_current_label, priv->get_name_internal, (GDestroyNotify) get_name_free);
+}
+
+/**
+ * hdy_combo_row_set_for_enum:
+ * @self: a #HdyComboRow
+ * @enum_type: the enumeration #GType to be bound to @self
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Creates a model for @enum_type and binds it to @self. The items of the model
+ * will be #HdyEnumValueObject objects.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * This is more convenient to use than hdy_combo_row_bind_name_model() if you
+ * want to represent values of an enumeration with names.
+ *
+ * See hdy_enum_value_row_name().
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_set_for_enum (HdyComboRow *self,
+ GType enum_type,
+ HdyComboRowGetEnumValueNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ g_autoptr (GListStore) store = g_list_store_new (HDY_TYPE_ENUM_VALUE_OBJECT);
+ /* g_autoptr for GEnumClass would require glib > 2.56 */
+ GEnumClass *enum_class = NULL;
+ gsize i;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ enum_class = g_type_class_ref (enum_type);
+ for (i = 0; i < enum_class->n_values; i++)
+ {
+ g_autoptr(HdyEnumValueObject) obj = hdy_enum_value_object_new (&enum_class->values[i]);
+
+ g_list_store_append (store, obj);
+ }
+
+ hdy_combo_row_bind_name_model (self, G_LIST_MODEL (store), (HdyComboRowGetNameFunc) get_name_func, user_data, user_data_free_func);
+ g_type_class_unref (enum_class);
+}
+
+/**
+ * hdy_combo_row_get_selected_index:
+ * @self: a #GtkListBoxRow
+ *
+ * Gets the index of the selected item in its #GListModel.
+ *
+ * Returns: the index of the selected item, or -1 if no item is selected
+ *
+ * Since: 0.0.7
+ */
+gint
+hdy_combo_row_get_selected_index (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), -1);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->selected_index;
+}
+
+/**
+ * hdy_combo_row_set_selected_index:
+ * @self: a #HdyComboRow
+ * @selected_index: the index of the selected item
+ *
+ * Sets the index of the selected item in its #GListModel.
+ *
+ * Since: 0.0.7
+ */
+void
+hdy_combo_row_set_selected_index (HdyComboRow *self,
+ gint selected_index)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (selected_index >= -1);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ g_return_if_fail (selected_index >= 0 || priv->bound_model == NULL || g_list_model_get_n_items (priv->bound_model) == 0);
+ g_return_if_fail (selected_index == -1 || (priv->bound_model != NULL && selected_index < g_list_model_get_n_items (priv->bound_model)));
+
+ if (priv->selected_index == selected_index)
+ return;
+
+ priv->selected_index = selected_index;
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+}
+
+/**
+ * hdy_combo_row_get_use_subtitle:
+ * @self: a #GtkListBoxRow
+ *
+ * Gets whether the current value of @self should be displayed as its subtitle.
+ *
+ * Returns: whether the current value of @self should be displayed as its subtitle
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_combo_row_get_use_subtitle (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), FALSE);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->use_subtitle;
+}
+
+/**
+ * hdy_combo_row_set_use_subtitle:
+ * @self: a #HdyComboRow
+ * @use_subtitle: %TRUE to set the current value as the subtitle
+ *
+ * Sets whether the current value of @self should be displayed as its subtitle.
+ *
+ * If %TRUE, you should not access HdyActionRow:subtitle.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_combo_row_set_use_subtitle (HdyComboRow *self,
+ gboolean use_subtitle)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ use_subtitle = !!use_subtitle;
+
+ if (priv->use_subtitle == use_subtitle)
+ return;
+
+ priv->use_subtitle = use_subtitle;
+ update (self);
+ if (!use_subtitle)
+ hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_SUBTITLE]);
+}
+
+/**
+ * hdy_combo_row_set_get_name_func:
+ * @self: a #HdyComboRow
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Sets a closure to convert items into names. See HdyComboRow:use-subtitle.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_combo_row_set_get_name_func (HdyComboRow *self,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ get_name_free (priv->get_name);
+ priv->get_name = g_new0 (HdyComboRowGetName, 1);
+ priv->get_name->func = get_name_func;
+ priv->get_name->func_data = user_data;
+ priv->get_name->func_data_destroy = user_data_free_func;
+}
+
+/**
+ * hdy_enum_value_row_name:
+ * @value: the value from the enum from which to get a name
+ * @user_data: (closure): unused user data
+ *
+ * This is a default implementation of #HdyComboRowGetEnumValueNameFunc to be
+ * used with hdy_combo_row_set_for_enum(). If the enumeration has a nickname, it
+ * will return it, otherwise it will return its name.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @value
+ *
+ * Since: 0.0.6
+ */
+gchar *
+hdy_enum_value_row_name (HdyEnumValueObject *value,
+ gpointer user_data)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL);
+
+ return g_strdup (hdy_enum_value_object_get_nick (value) != NULL ?
+ hdy_enum_value_object_get_nick (value) :
+ hdy_enum_value_object_get_name (value));
+}
diff --git a/subprojects/libhandy/src/hdy-combo-row.h b/subprojects/libhandy/src/hdy-combo-row.h
new file mode 100644
index 0000000..68657a0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enum-value-object.h"
+#include "hdy-action-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_COMBO_ROW (hdy_combo_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyComboRow, hdy_combo_row, HDY, COMBO_ROW, HdyActionRow)
+
+/**
+ * HdyComboRowGetNameFunc:
+ * @item: (type GObject): the item from the model from which to get a name
+ * @user_data: (closure): user data
+ *
+ * Called for combo rows that are bound to a #GListModel with
+ * hdy_combo_row_bind_name_model() for each item that gets added to the model.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @item
+ */
+typedef gchar * (*HdyComboRowGetNameFunc) (gpointer item,
+ gpointer user_data);
+
+/**
+ * HdyComboRowGetEnumValueNameFunc:
+ * @value: the value from the enum from which to get a name
+ * @user_data: (closure): user data
+ *
+ * Called for combo rows that are bound to an enumeration with
+ * hdy_combo_row_set_for_enum() for each value from that enumeration.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @value
+ */
+typedef gchar * (*HdyComboRowGetEnumValueNameFunc) (HdyEnumValueObject *value,
+ gpointer user_data);
+
+/**
+ * HdyComboRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyComboRowClass
+{
+ HdyActionRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_combo_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+GListModel *hdy_combo_row_get_model (HdyComboRow *self);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_bind_model (HdyComboRow *self,
+ GListModel *model,
+ GtkListBoxCreateWidgetFunc create_list_widget_func,
+ GtkListBoxCreateWidgetFunc create_current_widget_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_bind_name_model (HdyComboRow *self,
+ GListModel *model,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_for_enum (HdyComboRow *self,
+ GType enum_type,
+ HdyComboRowGetEnumValueNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+
+HDY_AVAILABLE_IN_ALL
+gint hdy_combo_row_get_selected_index (HdyComboRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_selected_index (HdyComboRow *self,
+ gint selected_index);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_combo_row_get_use_subtitle (HdyComboRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_use_subtitle (HdyComboRow *self,
+ gboolean use_subtitle);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_get_name_func (HdyComboRow *self,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+
+HDY_AVAILABLE_IN_ALL
+gchar *hdy_enum_value_row_name (HdyEnumValueObject *value,
+ gpointer user_data);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-combo-row.ui b/subprojects/libhandy/src/hdy-combo-row.ui
new file mode 100644
index 0000000..080a591
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyComboRow" parent="HdyActionRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox" id="current">
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="icon_name">pan-down-symbolic</property>
+ <property name="icon_size">1</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </template>
+ <object class="GtkPopover" id="popover">
+ <property name="position">bottom</property>
+ <property name="relative_to">image</property>
+ <style>
+ <class name="combo"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hscrollbar_policy">never</property>
+ <property name="max_content_height">400</property>
+ <property name="propagate_natural_width">True</property>
+ <property name="propagate_natural_height">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="selection_mode">none</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-css-private.h b/subprojects/libhandy/src/hdy-css-private.h
new file mode 100644
index 0000000..d8190b5
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-css-private.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void hdy_css_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural);
+
+void hdy_css_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-css.c b/subprojects/libhandy/src/hdy-css.c
new file mode 100644
index 0000000..7a056e2
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-css.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-css-private.h"
+
+void
+hdy_css_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ GtkStyleContext *style_context = gtk_widget_get_style_context (widget);
+ GtkStateFlags state_flags = gtk_widget_get_state_flags (widget);
+ GtkBorder border, margin, padding;
+ gint css_width, css_height;
+
+ /* Manually apply minimum sizes, the border, the padding and the margin as we
+ * can't use the private GtkGagdet.
+ */
+ gtk_style_context_get (style_context, state_flags,
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+ gtk_style_context_get_border (style_context, state_flags, &border);
+ gtk_style_context_get_margin (style_context, state_flags, &margin);
+ gtk_style_context_get_padding (style_context, state_flags, &padding);
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ *minimum = MAX (*minimum, css_height) +
+ border.top + margin.top + padding.top +
+ border.bottom + margin.bottom + padding.bottom;
+ *natural = MAX (*natural, css_height) +
+ border.top + margin.top + padding.top +
+ border.bottom + margin.bottom + padding.bottom;
+ } else {
+ *minimum = MAX (*minimum, css_width) +
+ border.left + margin.left + padding.left +
+ border.right + margin.right + padding.right;
+ *natural = MAX (*natural, css_width) +
+ border.left + margin.left + padding.left +
+ border.right + margin.right + padding.right;
+ }
+}
+
+void
+hdy_css_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkStyleContext *style_context;
+ GtkStateFlags state_flags;
+ GtkBorder border, margin, padding;
+
+ /* Manually apply the border, the padding and the margin as we can't use the
+ * private GtkGagdet.
+ */
+ style_context = gtk_widget_get_style_context (widget);
+ state_flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_border (style_context, state_flags, &border);
+ gtk_style_context_get_margin (style_context, state_flags, &margin);
+ gtk_style_context_get_padding (style_context, state_flags, &padding);
+ allocation->width -= border.left + border.right +
+ margin.left + margin.right +
+ padding.left + padding.right;
+ allocation->height -= border.top + border.bottom +
+ margin.top + margin.bottom +
+ padding.top + padding.bottom;
+ allocation->x += border.left + margin.left + padding.left;
+ allocation->y += border.top + margin.top + padding.top;
+}
diff --git a/subprojects/libhandy/src/hdy-deck.c b/subprojects/libhandy/src/hdy-deck.c
new file mode 100644
index 0000000..01dc45e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deck.c
@@ -0,0 +1,1103 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-deck.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-deck
+ * @short_description: A swipeable widget showing one of the visible children at a time.
+ * @Title: HdyDeck
+ *
+ * The #HdyDeck widget displays one of the visible children, similar to a
+ * #GtkStack. The children are strictly ordered and can be navigated using
+ * swipe gestures.
+ *
+ * The “over” and “under” stack the children one on top of the other, while the
+ * “slide” transition puts the children side by side. While navigating to a
+ * child on the side or below can be performed by swiping the current child
+ * away, navigating to an upper child requires dragging it from the edge where
+ * it resides. This doesn't affect non-dragging swipes.
+ *
+ * The “over” and “under” transitions can draw their shadow on top of the
+ * window's transparent areas, like the rounded corners. This is a side-effect
+ * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated
+ * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn
+ * beyond the rounded corners.
+ *
+ * # CSS nodes
+ *
+ * #HdyDeck has a single CSS node with name deck.
+ *
+ * Since: 1.0
+ */
+
+/**
+ * HdyDeckTransitionType:
+ * @HDY_DECK_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_DECK_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_DECK_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between children
+ * in a #HdyDeck widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_HHOMOGENEOUS,
+ PROP_VHOMOGENEOUS,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+
+ /* orientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ORIENTATION,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_NAME,
+ LAST_CHILD_PROP,
+};
+
+typedef struct
+{
+ HdyStackableBox *box;
+} HdyDeckPrivate;
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+static void hdy_deck_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyDeck, hdy_deck, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyDeck)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_deck_swipeable_init))
+
+#define HDY_GET_HELPER(obj) (((HdyDeckPrivate *) hdy_deck_get_instance_private (HDY_DECK (obj)))->box)
+
+/**
+ * hdy_deck_set_homogeneous:
+ * @self: a #HdyDeck
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyDeck to be homogeneous or not for the given orientation.
+ * If it is homogeneous, the #HdyDeck will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't, the deck may change width or height when a different child
+ * becomes visible.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_homogeneous (HdyDeck *self,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), TRUE, orientation, homogeneous);
+}
+
+/**
+ * hdy_deck_get_homogeneous:
+ * @self: a #HdyDeck
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given orientation.
+ * See hdy_deck_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given orientation.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_homogeneous (HdyDeck *self,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), TRUE, orientation);
+}
+
+/**
+ * hdy_deck_get_transition_type:
+ * @self: a #HdyDeck
+ *
+ * Gets the type of animation that will be used
+ * for transitions between children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 1.0
+ */
+HdyDeckTransitionType
+hdy_deck_get_transition_type (HdyDeck *self)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_val_if_fail (HDY_IS_DECK (self), HDY_DECK_TRANSITION_TYPE_OVER);
+
+ type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self));
+
+ switch (type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return HDY_DECK_TRANSITION_TYPE_OVER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return HDY_DECK_TRANSITION_TYPE_UNDER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ return HDY_DECK_TRANSITION_TYPE_SLIDE;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+/**
+ * hdy_deck_set_transition_type:
+ * @self: a #HdyDeck
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between children
+ * in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about to become
+ * current.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_transition_type (HdyDeck *self,
+ HdyDeckTransitionType transition)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_if_fail (HDY_IS_DECK (self));
+ g_return_if_fail (transition <= HDY_DECK_TRANSITION_TYPE_SLIDE);
+
+ switch (transition) {
+ case HDY_DECK_TRANSITION_TYPE_OVER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ break;
+
+ case HDY_DECK_TRANSITION_TYPE_UNDER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ break;
+
+ case HDY_DECK_TRANSITION_TYPE_SLIDE:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE;
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type);
+}
+
+/**
+ * hdy_deck_get_transition_duration:
+ * @self: a #HdyDeck
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_deck_get_transition_duration (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), 0);
+
+ return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_transition_duration:
+ * @self: a #HdyDeck
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_transition_duration (HdyDeck *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_deck_get_visible_child:
+ * @self: a #HdyDeck
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_visible_child (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_visible_child:
+ * @self: a #HdyDeck
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyDeck:transition-type and HdyDeck:transition-duration. The transition can
+ * be cancelled by the user, in which case visible child will change back to
+ * the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_visible_child (HdyDeck *self,
+ GtkWidget *visible_child)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child);
+}
+
+/**
+ * hdy_deck_get_visible_child_name:
+ * @self: a #HdyDeck
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_deck_get_visible_child_name (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_visible_child_name:
+ * @self: a #HdyDeck
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_deck_set_visible_child() for more details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_visible_child_name (HdyDeck *self,
+ const gchar *name)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name);
+}
+
+/**
+ * hdy_deck_get_transition_running:
+ * @self: a #HdyDeck
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_transition_running (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_interpolate_size:
+ * @self: a #HdyDeck
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyDeck:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_interpolate_size (HdyDeck *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size);
+}
+
+/**
+ * hdy_deck_get_interpolate_size:
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_interpolate_size (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_can_swipe_back:
+ * @self: a #HdyDeck
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_can_swipe_back (HdyDeck *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back);
+}
+
+/**
+ * hdy_deck_get_can_swipe_back
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_can_swipe_back (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_can_swipe_forward:
+ * @self: a #HdyDeck
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_can_swipe_forward (HdyDeck *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward);
+}
+
+/**
+ * hdy_deck_get_can_swipe_forward
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_can_swipe_forward (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_get_adjacent_child
+ * @self: a #HdyDeck
+ * @direction: the direction
+ *
+ * Gets the previous or next child, or %NULL if it doesn't exist. This will be
+ * the same widget hdy_deck_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_adjacent_child (HdyDeck *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_deck_navigate
+ * @self: a #HdyDeck
+ * @direction: the direction
+ *
+ * Switches to the previous or next child, similar to performing a swipe
+ * gesture to go in @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_navigate (HdyDeck *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_deck_get_child_by_name:
+ * @self: a #HdyDeck
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_child_by_name (HdyDeck *self,
+ const gchar *name)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name);
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_deck_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ hdy_stackable_box_measure (HDY_GET_HELPER (widget),
+ orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_deck_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_deck_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation);
+}
+
+static gboolean
+hdy_deck_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr);
+}
+
+static void
+hdy_deck_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction);
+}
+
+static void
+hdy_deck_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_add (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_deck_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_remove (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_deck_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data);
+}
+
+static void
+hdy_deck_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyDeck *self = HDY_DECK (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS:
+ g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS:
+ g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_deck_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_deck_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_deck_get_transition_type (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_deck_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_deck_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_deck_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_deck_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_deck_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_deck_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyDeck *self = HDY_DECK (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS:
+ hdy_deck_set_homogeneous (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS:
+ hdy_deck_set_homogeneous (self, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_deck_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_deck_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_deck_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_deck_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_deck_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_deck_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_deck_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_deck_finalize (GObject *object)
+{
+ HdyDeck *self = HDY_DECK (object);
+ HdyDeckPrivate *priv = hdy_deck_get_instance_private (self);
+
+ g_clear_object (&priv->box);
+
+ G_OBJECT_CLASS (hdy_deck_parent_class)->finalize (object);
+}
+
+static void
+hdy_deck_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_deck_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_deck_realize (GtkWidget *widget)
+{
+ hdy_stackable_box_realize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_unrealize (GtkWidget *widget)
+{
+ hdy_stackable_box_unrealize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_map (GtkWidget *widget)
+{
+ hdy_stackable_box_map (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_unmap (GtkWidget *widget)
+{
+ hdy_stackable_box_unmap (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration);
+}
+
+static HdySwipeTracker *
+hdy_deck_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_deck_get_distance (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble *
+hdy_deck_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points);
+}
+
+static gdouble
+hdy_deck_get_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_deck_get_cancel_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable));
+}
+
+static void
+hdy_deck_get_swipe_area (HdySwipeable *swipeable,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect);
+}
+
+static void
+hdy_deck_class_init (HdyDeckClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = (GtkWidgetClass*) klass;
+ GtkContainerClass *container_class = (GtkContainerClass*) klass;
+
+ object_class->get_property = hdy_deck_get_property;
+ object_class->set_property = hdy_deck_set_property;
+ object_class->finalize = hdy_deck_finalize;
+
+ widget_class->realize = hdy_deck_realize;
+ widget_class->unrealize = hdy_deck_unrealize;
+ widget_class->map = hdy_deck_map;
+ widget_class->unmap = hdy_deck_unmap;
+ widget_class->get_preferred_width = hdy_deck_get_preferred_width;
+ widget_class->get_preferred_height = hdy_deck_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_deck_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_deck_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_deck_size_allocate;
+ widget_class->draw = hdy_deck_draw;
+ widget_class->direction_changed = hdy_deck_direction_changed;
+
+ container_class->add = hdy_deck_add;
+ container_class->remove = hdy_deck_remove;
+ container_class->forall = hdy_deck_forall;
+ container_class->set_child_property = hdy_deck_set_child_property;
+ container_class->get_child_property = hdy_deck_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyDeck:hhomogeneous:
+ *
+ * Horizontally homogeneous sizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_HHOMOGENEOUS] =
+ g_param_spec_boolean ("hhomogeneous",
+ _("Horizontally homogeneous"),
+ _("Horizontally homogeneous sizing"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:vhomogeneous:
+ *
+ * Vertically homogeneous sizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VHOMOGENEOUS] =
+ g_param_spec_boolean ("vhomogeneous",
+ _("Vertically homogeneous"),
+ _("Vertically homogeneous sizing"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:visible-child:
+ *
+ * The widget currently visible.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:visible-child-name:
+ *
+ * The name of the widget currently visible.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-type:
+ *
+ * The type of animation that will be used for transitions between
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about
+ * to become current.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between children"),
+ HDY_TYPE_DECK_TRANSITION_TYPE, HDY_DECK_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-duration:
+ *
+ * The transition animation duration, in milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-running:
+ *
+ * Whether or not the transition is currently running.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ /**
+ * HdyDeck:interpolate-size:
+ *
+ * Whether or not the size should smoothly change when changing between
+ * differently sized children.
+ *
+ * Since: 1.0
+ */
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:can-swipe-back:
+ *
+ * Whether or not the deck allows switching to the previous child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:can-swipe-forward:
+ *
+ * Whether or not the deck allows switching to the next child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_NAME] =
+ g_param_spec_string ("name",
+ _("Name"),
+ _("The name of the child page"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "deck");
+}
+
+GtkWidget *
+hdy_deck_new (void)
+{
+ return g_object_new (HDY_TYPE_DECK, NULL);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (HdyDeck *self) { \
+ g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \
+}
+
+NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS);
+NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS);
+NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD);
+NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME);
+NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE);
+NOTIFY (notify_child_transition_duration_cb, PROP_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_running_cb, PROP_TRANSITION_RUNNING);
+NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE);
+NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK);
+NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD);
+
+static void
+notify_orientation_cb (HdyDeck *self)
+{
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_deck_init (HdyDeck *self)
+{
+ HdyDeckPrivate *priv = hdy_deck_get_instance_private (self);
+
+ priv->box = hdy_stackable_box_new (GTK_CONTAINER (self),
+ GTK_CONTAINER_CLASS (hdy_deck_parent_class),
+ FALSE);
+
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_deck_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_deck_switch_child;
+ iface->get_swipe_tracker = hdy_deck_get_swipe_tracker;
+ iface->get_distance = hdy_deck_get_distance;
+ iface->get_snap_points = hdy_deck_get_snap_points;
+ iface->get_progress = hdy_deck_get_progress;
+ iface->get_cancel_progress = hdy_deck_get_cancel_progress;
+ iface->get_swipe_area = hdy_deck_get_swipe_area;
+}
diff --git a/subprojects/libhandy/src/hdy-deck.h b/subprojects/libhandy/src/hdy-deck.h
new file mode 100644
index 0000000..3c7da18
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deck.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_DECK (hdy_deck_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyDeck, hdy_deck, HDY, DECK, GtkContainer)
+
+typedef enum {
+ HDY_DECK_TRANSITION_TYPE_OVER,
+ HDY_DECK_TRANSITION_TYPE_UNDER,
+ HDY_DECK_TRANSITION_TYPE_SLIDE,
+} HdyDeckTransitionType;
+
+/**
+ * HdyDeckClass
+ * @parent_class: The parent class
+ */
+struct _HdyDeckClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_new (void);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_visible_child (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_visible_child (HdyDeck *self,
+ GtkWidget *visible_child);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_deck_get_visible_child_name (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_visible_child_name (HdyDeck *self,
+ const gchar *name);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_homogeneous (HdyDeck *self,
+ GtkOrientation orientation);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_homogeneous (HdyDeck *self,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HDY_AVAILABLE_IN_ALL
+HdyDeckTransitionType hdy_deck_get_transition_type (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_transition_type (HdyDeck *self,
+ HdyDeckTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_deck_get_transition_duration (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_transition_duration (HdyDeck *self,
+ guint duration);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_transition_running (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_interpolate_size (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_interpolate_size (HdyDeck *self,
+ gboolean interpolate_size);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_can_swipe_back (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_can_swipe_back (HdyDeck *self,
+ gboolean can_swipe_back);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_can_swipe_forward (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_can_swipe_forward (HdyDeck *self,
+ gboolean can_swipe_forward);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_adjacent_child (HdyDeck *self,
+ HdyNavigationDirection direction);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_navigate (HdyDeck *self,
+ HdyNavigationDirection direction);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_child_by_name (HdyDeck *self,
+ const gchar *name);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-deprecation-macros.h b/subprojects/libhandy/src/hdy-deprecation-macros.h
new file mode 100644
index 0000000..e889135
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deprecation-macros.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#if defined(HDY_DISABLE_DEPRECATION_WARNINGS) || defined(HANDY_COMPILATION)
+# define _HDY_DEPRECATED
+# define _HDY_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_MACRO
+# define _HDY_DEPRECATED_MACRO_FOR(f)
+# define _HDY_DEPRECATED_ENUMERATOR
+# define _HDY_DEPRECATED_ENUMERATOR_FOR(f)
+# define _HDY_DEPRECATED_TYPE
+# define _HDY_DEPRECATED_TYPE_FOR(f)
+#else
+# define _HDY_DEPRECATED G_DEPRECATED
+# define _HDY_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_MACRO G_DEPRECATED
+# define _HDY_DEPRECATED_MACRO_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_ENUMERATOR G_DEPRECATED
+# define _HDY_DEPRECATED_ENUMERATOR_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_TYPE G_DEPRECATED
+# define _HDY_DEPRECATED_TYPE_FOR(f) G_DEPRECATED_FOR(f)
+#endif
diff --git a/subprojects/libhandy/src/hdy-enum-value-object.c b/subprojects/libhandy/src/hdy-enum-value-object.c
new file mode 100644
index 0000000..58ca13a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enum-value-object.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-enum-value-object.h"
+
+/**
+ * SECTION:hdy-enum-value-object
+ * @short_description: An object representing a #GEnumValue.
+ * @Title: HdyEnumValueObject
+ *
+ * The #HdyEnumValueObject object represents a #GEnumValue, allowing it to be
+ * used with #GListModel.
+ *
+ * Since: 0.0.6
+ */
+
+struct _HdyEnumValueObject
+{
+ GObject parent_instance;
+
+ GEnumValue enum_value;
+};
+
+G_DEFINE_TYPE (HdyEnumValueObject, hdy_enum_value_object, G_TYPE_OBJECT)
+
+HdyEnumValueObject *
+hdy_enum_value_object_new (GEnumValue *enum_value)
+{
+ HdyEnumValueObject *self = g_object_new (HDY_TYPE_ENUM_VALUE_OBJECT, NULL);
+
+ self->enum_value = *enum_value;
+
+ return self;
+}
+
+static void
+hdy_enum_value_object_class_init (HdyEnumValueObjectClass *klass)
+{
+}
+
+static void
+hdy_enum_value_object_init (HdyEnumValueObject *self)
+{
+}
+
+gint
+hdy_enum_value_object_get_value (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), 0);
+
+ return self->enum_value.value;
+}
+
+const gchar *
+hdy_enum_value_object_get_name (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL);
+
+ return self->enum_value.value_name;
+}
+
+const gchar *
+hdy_enum_value_object_get_nick (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL);
+
+ return self->enum_value.value_nick;
+}
diff --git a/subprojects/libhandy/src/hdy-enum-value-object.h b/subprojects/libhandy/src/hdy-enum-value-object.h
new file mode 100644
index 0000000..b961a62
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enum-value-object.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_ENUM_VALUE_OBJECT (hdy_enum_value_object_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyEnumValueObject, hdy_enum_value_object, HDY, ENUM_VALUE_OBJECT, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdyEnumValueObject *hdy_enum_value_object_new (GEnumValue *enum_value);
+
+HDY_AVAILABLE_IN_ALL
+gint hdy_enum_value_object_get_value (HdyEnumValueObject *self);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_enum_value_object_get_name (HdyEnumValueObject *self);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_enum_value_object_get_nick (HdyEnumValueObject *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-enums-private.c.in b/subprojects/libhandy/src/hdy-enums-private.c.in
new file mode 100644
index 0000000..5a11cda
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums-private.c.in
@@ -0,0 +1,38 @@
+/*** BEGIN file-header ***/
+
+#include "config.h"
+#include "hdy-enums-private.h"
+#include "hdy-stackable-box-private.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType etype = 0;
+ if (G_UNLIKELY(etype == 0)) {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@, "@VALUENAME@", "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+ etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
+ }
+ return etype;
+}
+
+/*** END value-tail ***/
+
+/*** BEGIN file-tail ***/
+
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums-private.h.in b/subprojects/libhandy/src/hdy-enums-private.h.in
new file mode 100644
index 0000000..1955f4e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums-private.h.in
@@ -0,0 +1,27 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+
+/* enumerations from "@basename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType @enum_name@_get_type (void);
+#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ())
+/*** END value-header ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums.c.in b/subprojects/libhandy/src/hdy-enums.c.in
new file mode 100644
index 0000000..a630555
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums.c.in
@@ -0,0 +1,44 @@
+/*** BEGIN file-header ***/
+
+#include "config.h"
+#include "hdy-deck.h"
+#include "hdy-enums.h"
+#include "hdy-header-bar.h"
+#include "hdy-header-group.h"
+#include "hdy-leaflet.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-squeezer.h"
+#include "hdy-view-switcher.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType etype = 0;
+ if (G_UNLIKELY(etype == 0)) {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@, "@VALUENAME@", "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+ etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
+ }
+ return etype;
+}
+
+/*** END value-tail ***/
+
+/*** BEGIN file-tail ***/
+
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums.h.in b/subprojects/libhandy/src/hdy-enums.h.in
new file mode 100644
index 0000000..7b39850
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums.h.in
@@ -0,0 +1,28 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+
+/* enumerations from "@basename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+
+HDY_AVAILABLE_IN_ALL GType @enum_name@_get_type (void);
+#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ())
+/*** END value-header ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-expander-row.c b/subprojects/libhandy/src/hdy-expander-row.c
new file mode 100644
index 0000000..55bf695
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.c
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-expander-row.h"
+
+#include <glib/gi18n-lib.h>
+#include "hdy-action-row.h"
+
+/**
+ * SECTION:hdy-expander-row
+ * @short_description: A #GtkListBox row used to reveal widgets.
+ * @Title: HdyExpanderRow
+ *
+ * The #HdyExpanderRow allows the user to reveal or hide widgets below it. It
+ * also allows the user to enable the expansion of the row, allowing to disable
+ * all that the row contains.
+ *
+ * It also supports adding a child as an action widget by specifying “action” as
+ * the “type” attribute of a &lt;child&gt; element. It also supports setting a
+ * child as a prefix widget by specifying “prefix” as the “type” attribute of a
+ * &lt;child&gt; element.
+ *
+ * # CSS nodes
+ *
+ * #HdyExpanderRow has a main CSS node with name row, and the .expander style
+ * class. It has the .empty style class when it contains no children.
+ *
+ * It contains the subnodes row.header for its main embedded row, list.nested
+ * for the list it can expand, and image.expander-row-arrow for its arrow.
+ *
+ * When expanded, #HdyExpanderRow will add the
+ * .checked-expander-row-previous-sibling style class to its previous sibling,
+ * and remove it when retracted.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkBox *actions;
+ GtkBox *prefixes;
+ GtkListBox *list;
+ HdyActionRow *action_row;
+ GtkSwitch *enable_switch;
+ GtkImage *image;
+
+ gboolean expanded;
+ gboolean enable_expansion;
+ gboolean show_enable_switch;
+} HdyExpanderRowPrivate;
+
+static void hdy_expander_row_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyExpanderRow, hdy_expander_row, HDY_TYPE_PREFERENCES_ROW,
+ G_ADD_PRIVATE (HdyExpanderRow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_expander_row_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_SUBTITLE,
+ PROP_USE_UNDERLINE,
+ PROP_ICON_NAME,
+ PROP_EXPANDED,
+ PROP_ENABLE_EXPANSION,
+ PROP_SHOW_ENABLE_SWITCH,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_arrow (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self));
+ GtkWidget *previous_sibling = NULL;
+
+ if (parent) {
+ g_autoptr (GList) siblings = gtk_container_get_children (GTK_CONTAINER (parent));
+ GList *l;
+
+ for (l = siblings; l != NULL && l->next != NULL && l->next->data != self; l = l->next);
+
+ if (l && l->next && l->next->data == self)
+ previous_sibling = l->data;
+ }
+
+ if (priv->expanded)
+ gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE);
+ else
+ gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED);
+
+ if (previous_sibling) {
+ GtkStyleContext *previous_sibling_context = gtk_widget_get_style_context (previous_sibling);
+
+ if (priv->expanded)
+ gtk_style_context_add_class (previous_sibling_context, "checked-expander-row-previous-sibling");
+ else
+ gtk_style_context_remove_class (previous_sibling_context, "checked-expander-row-previous-sibling");
+ }
+}
+
+static void
+hdy_expander_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_expander_row_get_subtitle (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_expander_row_get_use_underline (self));
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_expander_row_get_icon_name (self));
+ break;
+ case PROP_EXPANDED:
+ g_value_set_boolean (value, hdy_expander_row_get_expanded (self));
+ break;
+ case PROP_ENABLE_EXPANSION:
+ g_value_set_boolean (value, hdy_expander_row_get_enable_expansion (self));
+ break;
+ case PROP_SHOW_ENABLE_SWITCH:
+ g_value_set_boolean (value, hdy_expander_row_get_show_enable_switch (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_expander_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SUBTITLE:
+ hdy_expander_row_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_expander_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ case PROP_ICON_NAME:
+ hdy_expander_row_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_EXPANDED:
+ hdy_expander_row_set_expanded (self, g_value_get_boolean (value));
+ break;
+ case PROP_ENABLE_EXPANSION:
+ hdy_expander_row_set_enable_expansion (self, g_value_get_boolean (value));
+ break;
+ case PROP_SHOW_ENABLE_SWITCH:
+ hdy_expander_row_set_show_enable_switch (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_expander_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else {
+ if (priv->prefixes)
+ gtk_container_foreach (GTK_CONTAINER (priv->prefixes), callback, callback_data);
+ if (priv->actions)
+ gtk_container_foreach (GTK_CONTAINER (priv->actions), callback, callback_data);
+ if (priv->list)
+ gtk_container_foreach (GTK_CONTAINER (priv->list), callback, callback_data);
+ }
+}
+
+static void
+activate_cb (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_expander_row_set_expanded (self, !priv->expanded);
+}
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+list_children_changed_cb (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ gint count = 0;
+
+ gtk_container_foreach (GTK_CONTAINER (priv->list), (GtkCallback) count_children_cb, &count);
+
+ if (count == 0)
+ gtk_style_context_add_class (context, "empty");
+ else
+ gtk_style_context_remove_class (context, "empty");
+}
+
+static void
+hdy_expander_row_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ /* When constructing the widget, we want the box to be added as the child of
+ * the GtkListBoxRow, as an implementation detail.
+ */
+ if (priv->box == NULL)
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->add (container, child);
+ else
+ gtk_container_add (GTK_CONTAINER (priv->list), child);
+}
+
+static void
+hdy_expander_row_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->box))
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->remove (container, child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->actions))
+ gtk_container_remove (GTK_CONTAINER (priv->actions), child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes))
+ gtk_container_remove (GTK_CONTAINER (priv->prefixes), child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->list), child);
+}
+
+static void
+hdy_expander_row_class_init (HdyExpanderRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_expander_row_get_property;
+ object_class->set_property = hdy_expander_row_set_property;
+
+ container_class->add = hdy_expander_row_add;
+ container_class->remove = hdy_expander_row_remove;
+ container_class->forall = hdy_expander_row_forall;
+
+ /**
+ * HdyExpanderRow:subtitle:
+ *
+ * The subtitle for this row.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle for this row"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title and subtitle labels
+ * indicates a mnemonic.
+ *
+ * Since: 1.0
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:icon-name:
+ *
+ * The icon name for this row.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:expanded:
+ *
+ * %TRUE if the row is expanded.
+ */
+ props[PROP_EXPANDED] =
+ g_param_spec_boolean ("expanded",
+ _("Expanded"),
+ _("Whether the row is expanded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:enable-expansion:
+ *
+ * %TRUE if the expansion is enabled.
+ */
+ props[PROP_ENABLE_EXPANSION] =
+ g_param_spec_boolean ("enable-expansion",
+ _("Enable expansion"),
+ _("Whether the expansion is enabled"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:show-enable-switch:
+ *
+ * %TRUE if the switch enabling the expansion is visible.
+ */
+ props[PROP_SHOW_ENABLE_SWITCH] =
+ g_param_spec_boolean ("show-enable-switch",
+ _("Show enable switch"),
+ _("Whether the switch enabling the expansion is visible"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-expander-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, action_row);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, actions);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, list);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, enable_switch);
+ gtk_widget_class_bind_template_callback (widget_class, activate_cb);
+ gtk_widget_class_bind_template_callback (widget_class, list_children_changed_cb);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (gpointer this) { \
+ g_object_notify_by_pspec (G_OBJECT (this), props[prop]); \
+} \
+
+NOTIFY (notify_subtitle_cb, PROP_SUBTITLE);
+NOTIFY (notify_use_underline_cb, PROP_USE_UNDERLINE);
+NOTIFY (notify_icon_name_cb, PROP_ICON_NAME);
+
+static void
+hdy_expander_row_init (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ priv->prefixes = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ hdy_expander_row_set_enable_expansion (self, TRUE);
+ hdy_expander_row_set_expanded (self, FALSE);
+
+ g_signal_connect_object (priv->action_row, "notify::subtitle", G_CALLBACK (notify_subtitle_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->action_row, "notify::use-underline", G_CALLBACK (notify_use_underline_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->action_row, "notify::icon-name", G_CALLBACK (notify_icon_name_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_expander_row_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (buildable);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (priv->box == NULL || !type)
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+ else if (type && strcmp (type, "action") == 0)
+ hdy_expander_row_add_action (self, GTK_WIDGET (child));
+ else if (type && strcmp (type, "prefix") == 0)
+ hdy_expander_row_add_prefix (self, GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type);
+}
+
+static void
+hdy_expander_row_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+ iface->add_child = hdy_expander_row_buildable_add_child;
+}
+
+/**
+ * hdy_expander_row_new:
+ *
+ * Creates a new #HdyExpanderRow.
+ *
+ * Returns: a new #HdyExpanderRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_expander_row_new (void)
+{
+ return g_object_new (HDY_TYPE_EXPANDER_ROW, NULL);
+}
+
+/**
+ * hdy_expander_row_get_subtitle:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets the subtitle for @self.
+ *
+ * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_expander_row_get_subtitle (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_subtitle (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_subtitle:
+ * @self: a #HdyExpanderRow
+ * @subtitle: (nullable): the subtitle
+ *
+ * Sets the subtitle for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_subtitle (HdyExpanderRow *self,
+ const gchar *subtitle)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_subtitle (priv->action_row, subtitle);
+}
+
+/**
+ * hdy_expander_row_get_use_underline:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether an embedded underline in the text of the title and subtitle
+ * labels indicates a mnemonic. See hdy_expander_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title and subtitle labels
+ * indicates the mnemonic accelerator keys.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_expander_row_get_use_underline (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_use_underline (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_use_underline:
+ * @self: a #HdyExpanderRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title and subtitle labels indicates
+ * the next character should be used for the mnemonic accelerator key.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_use_underline (HdyExpanderRow *self,
+ gboolean use_underline)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_use_underline (priv->action_row, use_underline);
+}
+
+/**
+ * hdy_expander_row_get_icon_name:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets the icon name for @self.
+ *
+ * Returns: the icon name for @self.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_expander_row_get_icon_name (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_icon_name (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_icon_name:
+ * @self: a #HdyExpanderRow
+ * @icon_name: the icon name
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_icon_name (HdyExpanderRow *self,
+ const gchar *icon_name)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_icon_name (priv->action_row, icon_name);
+}
+
+gboolean
+hdy_expander_row_get_expanded (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->expanded;
+}
+
+void
+hdy_expander_row_set_expanded (HdyExpanderRow *self,
+ gboolean expanded)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ expanded = !!expanded && priv->enable_expansion;
+
+ if (priv->expanded == expanded)
+ return;
+
+ priv->expanded = expanded;
+
+ update_arrow (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPANDED]);
+}
+
+/**
+ * hdy_expander_row_get_enable_expansion:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether the expansion of @self is enabled.
+ *
+ * Returns: whether the expansion of @self is enabled.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_expander_row_get_enable_expansion (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->enable_expansion;
+}
+
+/**
+ * hdy_expander_row_set_enable_expansion:
+ * @self: a #HdyExpanderRow
+ * @enable_expansion: %TRUE to enable the expansion
+ *
+ * Sets whether the expansion of @self is enabled.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_expander_row_set_enable_expansion (HdyExpanderRow *self,
+ gboolean enable_expansion)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ enable_expansion = !!enable_expansion;
+
+ if (priv->enable_expansion == enable_expansion)
+ return;
+
+ priv->enable_expansion = enable_expansion;
+
+ hdy_expander_row_set_expanded (self, priv->enable_expansion);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLE_EXPANSION]);
+}
+
+/**
+ * hdy_expander_row_get_show_enable_switch:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether the switch enabling the expansion of @self is visible.
+ *
+ * Returns: whether the switch enabling the expansion of @self is visible.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->show_enable_switch;
+}
+
+/**
+ * hdy_expander_row_set_show_enable_switch:
+ * @self: a #HdyExpanderRow
+ * @show_enable_switch: %TRUE to show the switch enabling the expansion
+ *
+ * Sets whether the switch enabling the expansion of @self is visible.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self,
+ gboolean show_enable_switch)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ show_enable_switch = !!show_enable_switch;
+
+ if (priv->show_enable_switch == show_enable_switch)
+ return;
+
+ priv->show_enable_switch = show_enable_switch;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_ENABLE_SWITCH]);
+}
+
+/**
+ * hdy_expander_row_add_action:
+ * @self: a #HdyExpanderRow
+ * @widget: the action widget
+ *
+ * Adds an action widget to @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_add_action (HdyExpanderRow *self,
+ GtkWidget *widget)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ gtk_box_pack_start (priv->actions, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->actions));
+}
+
+/**
+ * hdy_expander_row_add_prefix:
+ * @self: a #HdyExpanderRow
+ * @widget: the prefix widget
+ *
+ * Adds a prefix widget to @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_add_prefix (HdyExpanderRow *self,
+ GtkWidget *widget)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ if (priv->prefixes == NULL) {
+ priv->prefixes = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12));
+ gtk_widget_set_no_show_all (GTK_WIDGET (priv->prefixes), TRUE);
+ gtk_widget_set_can_focus (GTK_WIDGET (priv->prefixes), FALSE);
+ hdy_action_row_add_prefix (HDY_ACTION_ROW (priv->action_row), GTK_WIDGET (priv->prefixes));
+ }
+ gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->prefixes));
+}
diff --git a/subprojects/libhandy/src/hdy-expander-row.h b/subprojects/libhandy/src/hdy-expander-row.h
new file mode 100644
index 0000000..295f1d0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_EXPANDER_ROW (hdy_expander_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyExpanderRow, hdy_expander_row, HDY, EXPANDER_ROW, HdyPreferencesRow)
+
+/**
+ * HdyExpanderRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyExpanderRowClass
+{
+ HdyPreferencesRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_expander_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_expander_row_get_subtitle (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_subtitle (HdyExpanderRow *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_use_underline (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_use_underline (HdyExpanderRow *self,
+ gboolean use_underline);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_expander_row_get_icon_name (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_icon_name (HdyExpanderRow *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_expanded (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_expanded (HdyExpanderRow *self,
+ gboolean expanded);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_enable_expansion (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_enable_expansion (HdyExpanderRow *self,
+ gboolean enable_expansion);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self,
+ gboolean show_enable_switch);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_add_action (HdyExpanderRow *self,
+ GtkWidget *widget);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_add_prefix (HdyExpanderRow *self,
+ GtkWidget *widget);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-expander-row.ui b/subprojects/libhandy/src/hdy-expander-row.ui
new file mode 100644
index 0000000..54d2650
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyExpanderRow" parent="HdyPreferencesRow">
+ <!-- The row must not be activatable, to be sure it doesn't conflict with
+ clicking nested rows. -->
+ <property name="activatable">False</property>
+ <!-- The row must be focusable for keyboard navigation to work as
+ expected. -->
+ <property name="can-focus">True</property>
+ <!-- The row is focusable and can still be activated via keyboard, despite
+ being marked as inactivatable. Activating the row should toggle its
+ expansion. -->
+ <signal name="activate" handler="activate_cb" after="yes"/>
+ <style>
+ <class name="empty"/>
+ <class name="expander"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="no-show-all">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox">
+ <property name="selection-mode">none</property>
+ <property name="visible">True</property>
+ <!-- The header row is focusable, activatable, and can be activated
+ by clicking it or via keyboard. Activating the row should
+ toggle its expansion. -->
+ <signal name="row-activated" handler="activate_cb" after="yes" swapped="yes"/>
+ <child>
+ <object class="HdyActionRow" id="action_row">
+ <!-- The header row must be activatable to toggle expansion by
+ clicking it or via keyboard activation. -->
+ <property name="activatable">True</property>
+ <!-- The header row must be focusable for keyboard navigation to
+ work as expected. -->
+ <property name="can-focus">True</property>
+ <property name="title" bind-source="HdyExpanderRow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="header"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="actions">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="enable_switch">
+ <property name="active" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="bidirectional|sync-create"/>
+ <property name="can-focus">True</property>
+ <property name="valign">center</property>
+ <property name="visible" bind-source="HdyExpanderRow" bind-property="show-enable-switch" bind-flags="bidirectional|sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="can-focus">False</property>
+ <property name="icon-name">hdy-expander-arrow-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="sensitive" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="expander-row-arrow"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer">
+ <property name="reveal-child" bind-source="HdyExpanderRow" bind-property="expanded" bind-flags="sync-create"/>
+ <property name="transition-type">slide-up</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="selection-mode">none</property>
+ <property name="visible">True</property>
+ <signal name="add" handler="list_children_changed_cb" swapped="yes"/>
+ <signal name="remove" handler="list_children_changed_cb" swapped="yes"/>
+ <style>
+ <class name="nested"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-header-bar.c b/subprojects/libhandy/src/hdy-header-bar.c
new file mode 100644
index 0000000..c07a402
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-bar.c
@@ -0,0 +1,2868 @@
+/*
+ * Copyright (c) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * This program 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 program 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-header-bar.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-css-private.h"
+#include "hdy-enums.h"
+#include "hdy-window-handle-controller-private.h"
+#include "gtkprogresstrackerprivate.h"
+#include "gtk-window-private.h"
+
+/**
+ * SECTION:hdy-header-bar
+ * @short_description: A box with a centered child.
+ * @Title: HdyHeaderBar
+ * @See_also: #GtkHeaderBar, #HdyApplicationWindow, #HdyTitleBar, #HdyViewSwitcher, #HdyWindow
+ *
+ * HdyHeaderBar is similar to #GtkHeaderBar but is designed to fix some of its
+ * shortcomings for adaptive applications.
+ *
+ * HdyHeaderBar doesn't force the custom title widget to be vertically centered,
+ * hence allowing it to fill up the whole height, which is e.g. needed for
+ * #HdyViewSwitcher.
+ *
+ * When used in a mobile dialog, HdyHeaderBar will replace its window
+ * decorations by a back button allowing to close it. It doesn't have to be its
+ * direct child and you can use any complex contraption you like as the dialog's
+ * titlebar.
+ *
+ * #HdyHeaderBar can be used in window's content area rather than titlebar, and
+ * will still be draggable and will handle right click, middle click and double
+ * click as expected from a titlebar. This is particularly useful with
+ * #HdyWindow or #HdyApplicationWindow.
+ *
+ * # CSS nodes
+ *
+ * #HdyHeaderBar has a single CSS node with name headerbar.
+ */
+
+/**
+ * HdyCenteringPolicy:
+ * @HDY_CENTERING_POLICY_LOOSE: Keep the title centered when possible
+ * @HDY_CENTERING_POLICY_STRICT: Keep the title centered at all cost
+ */
+
+#define DEFAULT_SPACING 6
+#define MIN_TITLE_CHARS 5
+
+#define MOBILE_WINDOW_WIDTH 480
+#define MOBILE_WINDOW_HEIGHT 800
+
+typedef struct {
+ gchar *title;
+ gchar *subtitle;
+ GtkWidget *title_label;
+ GtkWidget *subtitle_label;
+ GtkWidget *label_box;
+ GtkWidget *label_sizing_box;
+ GtkWidget *subtitle_sizing_label;
+ GtkWidget *custom_title;
+ gint spacing;
+ gboolean has_subtitle;
+
+ GList *children;
+
+ gboolean shows_wm_decorations;
+ gchar *decoration_layout;
+ gboolean decoration_layout_set;
+
+ GtkWidget *titlebar_start_box;
+ GtkWidget *titlebar_end_box;
+
+ GtkWidget *titlebar_start_separator;
+ GtkWidget *titlebar_end_separator;
+
+ GtkWidget *titlebar_icon;
+
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ HdyCenteringPolicy centering_policy;
+ guint transition_duration;
+ gboolean interpolate_size;
+
+ gboolean is_mobile_window;
+
+ gulong window_size_allocated_id;
+
+ HdyWindowHandleController *controller;
+} HdyHeaderBarPrivate;
+
+typedef struct _Child Child;
+struct _Child
+{
+ GtkWidget *widget;
+ GtkPackType pack_type;
+};
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_HAS_SUBTITLE,
+ PROP_CUSTOM_TITLE,
+ PROP_SPACING,
+ PROP_SHOW_CLOSE_BUTTON,
+ PROP_DECORATION_LAYOUT,
+ PROP_DECORATION_LAYOUT_SET,
+ PROP_CENTERING_POLICY,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ LAST_PROP
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_PACK_TYPE,
+ CHILD_PROP_POSITION
+};
+
+static GParamSpec *props[LAST_PROP] = { NULL, };
+
+static void hdy_header_bar_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyHeaderBar, hdy_header_bar, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyHeaderBar)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_header_bar_buildable_init));
+
+static gboolean
+hdy_header_bar_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->first_frame_skipped)
+ gtk_progress_tracker_advance_frame (&priv->tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ else
+ priv->first_frame_skipped = TRUE;
+
+ /* Finish the animation early if the widget isn't mapped anymore. */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&priv->tracker);
+
+ gtk_widget_queue_resize (widget);
+
+ if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ priv->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_header_bar_schedule_ticks (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->tick_id == 0) {
+ priv->tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_header_bar_transition_cb, self, NULL);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_header_bar_unschedule_ticks (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->tick_id);
+ priv->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_header_bar_start_transition (HdyHeaderBar *self,
+ guint transition_duration)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ priv->interpolate_size &&
+ transition_duration != 0) {
+ priv->first_frame_skipped = FALSE;
+ hdy_header_bar_schedule_ticks (self);
+ gtk_progress_tracker_start (&priv->tracker,
+ priv->transition_duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ hdy_header_bar_unschedule_ticks (self);
+ gtk_progress_tracker_finish (&priv->tracker);
+ }
+
+ gtk_widget_queue_resize (widget);
+}
+
+static void
+init_sizing_box (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *w;
+ GtkStyleContext *context;
+
+ /* We use this box to always request size for the two labels (title
+ * and subtitle) as if they were always visible, but then allocate
+ * the real label box with its actual size, to keep it center-aligned
+ * in case we have only the title.
+ */
+ w = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show (w);
+ priv->label_sizing_box = g_object_ref_sink (w);
+
+ w = gtk_label_new (NULL);
+ gtk_widget_show (w);
+ context = gtk_widget_get_style_context (w);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE);
+ gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0);
+ gtk_label_set_line_wrap (GTK_LABEL (w), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END);
+ gtk_label_set_width_chars (GTK_LABEL (w), MIN_TITLE_CHARS);
+
+ w = gtk_label_new (NULL);
+ context = gtk_widget_get_style_context (w);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE);
+ gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0);
+ gtk_label_set_line_wrap (GTK_LABEL (w), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END);
+ gtk_widget_set_visible (w, priv->has_subtitle || (priv->subtitle && priv->subtitle[0]));
+ priv->subtitle_sizing_label = w;
+}
+
+static GtkWidget *
+create_title_box (const char *title,
+ const char *subtitle,
+ GtkWidget **ret_title_label,
+ GtkWidget **ret_subtitle_label)
+{
+ GtkWidget *label_box;
+ GtkWidget *title_label;
+ GtkWidget *subtitle_label;
+ GtkStyleContext *context;
+
+ label_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_valign (label_box, GTK_ALIGN_CENTER);
+ gtk_widget_show (label_box);
+
+ title_label = gtk_label_new (title);
+ context = gtk_widget_get_style_context (title_label);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE);
+ gtk_label_set_line_wrap (GTK_LABEL (title_label), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (title_label), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (title_label), PANGO_ELLIPSIZE_END);
+ gtk_box_pack_start (GTK_BOX (label_box), title_label, FALSE, FALSE, 0);
+ gtk_widget_show (title_label);
+ gtk_label_set_width_chars (GTK_LABEL (title_label), MIN_TITLE_CHARS);
+
+ subtitle_label = gtk_label_new (subtitle);
+ context = gtk_widget_get_style_context (subtitle_label);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE);
+ gtk_label_set_line_wrap (GTK_LABEL (subtitle_label), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (subtitle_label), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (subtitle_label), PANGO_ELLIPSIZE_END);
+ gtk_box_pack_start (GTK_BOX (label_box), subtitle_label, FALSE, FALSE, 0);
+ gtk_widget_set_no_show_all (subtitle_label, TRUE);
+ gtk_widget_set_visible (subtitle_label, subtitle && subtitle[0]);
+
+ if (ret_title_label)
+ *ret_title_label = title_label;
+ if (ret_subtitle_label)
+ *ret_subtitle_label = subtitle_label;
+
+ return label_box;
+}
+
+static gboolean
+hdy_header_bar_update_window_icon (HdyHeaderBar *self,
+ GtkWindow *window)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GdkPixbuf *pixbuf;
+ gint scale;
+
+ if (priv->titlebar_icon == NULL)
+ return FALSE;
+
+ scale = gtk_widget_get_scale_factor (priv->titlebar_icon);
+ if (GTK_IS_BUTTON (gtk_widget_get_parent (priv->titlebar_icon)))
+ pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 16);
+ else
+ pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 20);
+
+ if (pixbuf) {
+ g_autoptr (cairo_surface_t) surface =
+ gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, gtk_widget_get_window (priv->titlebar_icon));
+
+ gtk_image_set_from_surface (GTK_IMAGE (priv->titlebar_icon), surface);
+ g_object_unref (pixbuf);
+ gtk_widget_show (priv->titlebar_icon);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+_hdy_header_bar_update_separator_visibility (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gboolean have_visible_at_start = FALSE;
+ gboolean have_visible_at_end = FALSE;
+ GList *l;
+
+ for (l = priv->children; l != NULL; l = l->next) {
+ Child *child = l->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ if (child->pack_type == GTK_PACK_START)
+ have_visible_at_start = TRUE;
+ else
+ have_visible_at_end = TRUE;
+ }
+ }
+
+ if (priv->titlebar_start_separator != NULL)
+ gtk_widget_set_visible (priv->titlebar_start_separator, have_visible_at_start);
+
+ if (priv->titlebar_end_separator != NULL)
+ gtk_widget_set_visible (priv->titlebar_end_separator, have_visible_at_end);
+}
+
+static void
+hdy_header_bar_update_window_buttons (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *widget = GTK_WIDGET (self), *toplevel;
+ GtkWindow *window;
+ GtkTextDirection direction;
+ gchar *layout_desc;
+ gchar **tokens, **t;
+ gint i, j;
+ GMenuModel *menu;
+ gboolean shown_by_shell;
+ gboolean is_sovereign_window;
+ gboolean is_mobile_dialog;
+
+ toplevel = gtk_widget_get_toplevel (widget);
+ if (!gtk_widget_is_toplevel (toplevel))
+ return;
+
+ if (priv->titlebar_start_box) {
+ gtk_widget_unparent (priv->titlebar_start_box);
+ priv->titlebar_start_box = NULL;
+ priv->titlebar_start_separator = NULL;
+ }
+ if (priv->titlebar_end_box) {
+ gtk_widget_unparent (priv->titlebar_end_box);
+ priv->titlebar_end_box = NULL;
+ priv->titlebar_end_separator = NULL;
+ }
+
+ priv->titlebar_icon = NULL;
+
+ if (!priv->shows_wm_decorations)
+ return;
+
+ direction = gtk_widget_get_direction (widget);
+
+ g_object_get (gtk_widget_get_settings (widget),
+ "gtk-shell-shows-app-menu", &shown_by_shell,
+ "gtk-decoration-layout", &layout_desc,
+ NULL);
+
+ if (priv->decoration_layout_set) {
+ g_free (layout_desc);
+ layout_desc = g_strdup (priv->decoration_layout);
+ }
+
+ window = GTK_WINDOW (toplevel);
+
+ if (!shown_by_shell && gtk_window_get_application (window))
+ menu = gtk_application_get_app_menu (gtk_window_get_application (window));
+ else
+ menu = NULL;
+
+ is_sovereign_window = (!gtk_window_get_modal (window) &&
+ gtk_window_get_transient_for (window) == NULL &&
+ gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL);
+
+ is_mobile_dialog= (priv->is_mobile_window && !is_sovereign_window);
+
+ tokens = g_strsplit (layout_desc, ":", 2);
+ if (tokens) {
+ for (i = 0; i < 2; i++) {
+ GtkWidget *box;
+ GtkWidget *separator;
+ int n_children = 0;
+
+ if (tokens[i] == NULL)
+ break;
+
+ t = g_strsplit (tokens[i], ",", -1);
+
+ separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
+ gtk_widget_set_no_show_all (separator, TRUE);
+ gtk_style_context_add_class (gtk_widget_get_style_context (separator), "titlebutton");
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, priv->spacing);
+
+ for (j = 0; t[j]; j++) {
+ GtkWidget *button = NULL;
+ GtkWidget *image = NULL;
+ AtkObject *accessible;
+
+ if (strcmp (t[j], "icon") == 0 &&
+ is_sovereign_window) {
+ button = gtk_image_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ priv->titlebar_icon = button;
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "icon");
+ gtk_widget_set_size_request (button, 20, 20);
+ gtk_widget_show (button);
+
+ if (!hdy_header_bar_update_window_icon (self, window))
+ {
+ gtk_widget_destroy (button);
+ priv->titlebar_icon = NULL;
+ button = NULL;
+ }
+ } else if (strcmp (t[j], "menu") == 0 &&
+ menu != NULL &&
+ is_sovereign_window) {
+ button = gtk_menu_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (button), menu);
+ gtk_menu_button_set_use_popover (GTK_MENU_BUTTON (button), TRUE);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "appmenu");
+ image = gtk_image_new ();
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Application menu"));
+
+ priv->titlebar_icon = image;
+ if (!hdy_header_bar_update_window_icon (self, window))
+ gtk_image_set_from_icon_name (GTK_IMAGE (priv->titlebar_icon),
+ "application-x-executable-symbolic", GTK_ICON_SIZE_MENU);
+ } else if (strcmp (t[j], "minimize") == 0 &&
+ is_sovereign_window) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "minimize");
+ image = gtk_image_new_from_icon_name ("window-minimize-symbolic", GTK_ICON_SIZE_MENU);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_iconify), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Minimize"));
+ } else if (strcmp (t[j], "maximize") == 0 &&
+ gtk_window_get_resizable (window) &&
+ is_sovereign_window) {
+ const gchar *icon_name;
+ gboolean maximized = gtk_window_is_maximized (window);
+
+ icon_name = maximized ? "window-restore-symbolic" : "window-maximize-symbolic";
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "maximize");
+ image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (hdy_gtk_window_toggle_maximized), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, maximized ? _("Restore") : _("Maximize"));
+ } else if (strcmp (t[j], "close") == 0 &&
+ gtk_window_get_deletable (window) &&
+ !is_mobile_dialog) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ image = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "close");
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_close), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Close"));
+ } else if (i == 0 && /* Only at the start. */
+ gtk_window_get_deletable (window) &&
+ is_mobile_dialog) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ image = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_BUTTON);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, TRUE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_close), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Back"));
+ }
+
+ if (button) {
+ gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
+ n_children ++;
+ }
+ }
+ g_strfreev (t);
+
+ if (n_children == 0) {
+ g_object_ref_sink (box);
+ g_object_unref (box);
+ g_object_ref_sink (separator);
+ g_object_unref (separator);
+ continue;
+ }
+
+ gtk_box_pack_start (GTK_BOX (box), separator, FALSE, FALSE, 0);
+ if (i == 1)
+ gtk_box_reorder_child (GTK_BOX (box), separator, 0);
+
+ if ((direction == GTK_TEXT_DIR_LTR && i == 0) ||
+ (direction == GTK_TEXT_DIR_RTL && i == 1))
+ gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_LEFT);
+ else
+ gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_RIGHT);
+
+ gtk_widget_show (box);
+ gtk_widget_set_parent (box, GTK_WIDGET (self));
+
+ if (i == 0) {
+ priv->titlebar_start_box = box;
+ priv->titlebar_start_separator = separator;
+ } else {
+ priv->titlebar_end_box = box;
+ priv->titlebar_end_separator = separator;
+ }
+ }
+ g_strfreev (tokens);
+ }
+ g_free (layout_desc);
+
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static gboolean
+compute_is_mobile_window (GtkWindow *window)
+{
+ gint window_width, window_height;
+
+ gtk_window_get_size (window, &window_width, &window_height);
+
+ if (window_width <= MOBILE_WINDOW_WIDTH &&
+ gtk_window_is_maximized (window))
+ return TRUE;
+
+ /* Mobile landscape mode. */
+ if (window_width <= MOBILE_WINDOW_HEIGHT &&
+ window_height <= MOBILE_WINDOW_WIDTH &&
+ gtk_window_is_maximized (window))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+update_is_mobile_window (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+ gboolean was_mobile_window = priv->is_mobile_window;
+
+ if (!gtk_widget_is_toplevel (toplevel))
+ return;
+
+ priv->is_mobile_window = compute_is_mobile_window (GTK_WINDOW (toplevel));
+
+ if (priv->is_mobile_window != was_mobile_window)
+ hdy_header_bar_update_window_buttons (self);
+}
+
+static void
+construct_label_box (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_assert (priv->label_box == NULL);
+
+ priv->label_box = create_title_box (priv->title,
+ priv->subtitle,
+ &priv->title_label,
+ &priv->subtitle_label);
+ gtk_widget_set_parent (priv->label_box, GTK_WIDGET (self));
+}
+
+static gint
+count_visible_children (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint n;
+
+ n = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (gtk_widget_get_visible (child->widget))
+ n++;
+ }
+
+ return n;
+}
+
+static gint
+count_visible_children_for_pack_type (HdyHeaderBar *self, GtkPackType pack_type)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint n;
+
+ n = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (gtk_widget_get_visible (child->widget) && child->pack_type == pack_type)
+ n++;
+ }
+
+ return n;
+}
+
+static gboolean
+add_child_size (GtkWidget *child,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ gint child_minimum, child_natural;
+
+ if (!gtk_widget_get_visible (child))
+ return FALSE;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ gtk_widget_get_preferred_width (child, &child_minimum, &child_natural);
+ else
+ gtk_widget_get_preferred_height (child, &child_minimum, &child_natural);
+
+ if (GTK_ORIENTATION_HORIZONTAL == orientation) {
+ *minimum += child_minimum;
+ *natural += child_natural;
+ } else {
+ *minimum = MAX (*minimum, child_minimum);
+ *natural = MAX (*natural, child_natural);
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_header_bar_get_size (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ gint n_start_children = 0, n_end_children = 0;
+ gint start_min = 0, start_nat = 0;
+ gint end_min = 0, end_nat = 0;
+ gint center_min = 0, center_nat = 0;
+
+ for (l = priv->children; l; l = l->next) {
+ Child *child = l->data;
+
+ if (child->pack_type == GTK_PACK_START) {
+ if (add_child_size (child->widget, orientation, &start_min, &start_nat))
+ n_start_children += 1;
+ } else {
+ if (add_child_size (child->widget, orientation, &end_min, &end_nat))
+ n_end_children += 1;
+ }
+ }
+
+ if (priv->label_box != NULL) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ add_child_size (priv->label_box, orientation, &center_min, &center_nat);
+ else
+ add_child_size (priv->label_sizing_box, orientation, &center_min, &center_nat);
+ }
+
+ if (priv->custom_title != NULL)
+ add_child_size (priv->custom_title, orientation, &center_min, &center_nat);
+
+ if (priv->titlebar_start_box != NULL) {
+ if (add_child_size (priv->titlebar_start_box, orientation, &start_min, &start_nat))
+ n_start_children += 1;
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ if (add_child_size (priv->titlebar_end_box, orientation, &end_min, &end_nat))
+ n_end_children += 1;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gdouble strict_centering_t;
+ gint start_min_spaced = start_min + n_start_children * priv->spacing;
+ gint end_min_spaced = end_min + n_end_children * priv->spacing;
+ gint start_nat_spaced = start_nat + n_start_children * priv->spacing;
+ gint end_nat_spaced = end_nat + n_end_children * priv->spacing;
+
+ if (gtk_progress_tracker_get_state (&priv->tracker) != GTK_PROGRESS_STATE_AFTER) {
+ strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE);
+ if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT)
+ strict_centering_t = 1.0 - strict_centering_t;
+ } else
+ strict_centering_t = priv->centering_policy == HDY_CENTERING_POLICY_STRICT ? 1.0 : 0.0;
+
+ *minimum = center_min + n_start_children * priv->spacing +
+ hdy_lerp (start_min_spaced + end_min_spaced,
+ 2 * MAX (start_min_spaced, end_min_spaced),
+ strict_centering_t);
+ *natural = center_nat + n_start_children * priv->spacing +
+ hdy_lerp (start_nat_spaced + end_nat_spaced,
+ 2 * MAX (start_nat_spaced, end_nat_spaced),
+ strict_centering_t);
+ } else {
+ *minimum = MAX (MAX (start_min, end_min), center_min);
+ *natural = MAX (MAX (start_nat, end_nat), center_nat);
+ }
+}
+
+static void
+hdy_header_bar_compute_size_for_orientation (GtkWidget *widget,
+ gint avail_size,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *children;
+ gint required_size = 0;
+ gint required_natural = 0;
+ gint child_size;
+ gint child_natural;
+ gint nvis_children = 0;
+
+ for (children = priv->children; children != NULL; children = children->next) {
+ Child *child = children->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ avail_size, &child_size, &child_natural);
+
+ required_size += child_size;
+ required_natural += child_natural;
+
+ nvis_children += 1;
+ }
+ }
+
+ if (priv->label_box != NULL) {
+ gtk_widget_get_preferred_width (priv->label_sizing_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ }
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title)) {
+ gtk_widget_get_preferred_width (priv->custom_title,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ }
+
+ if (priv->titlebar_start_box != NULL) {
+ gtk_widget_get_preferred_width (priv->titlebar_start_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ nvis_children += 1;
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ gtk_widget_get_preferred_width (priv->titlebar_end_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ nvis_children += 1;
+ }
+
+ required_size += nvis_children * priv->spacing;
+ required_natural += nvis_children * priv->spacing;
+
+ *minimum_size = required_size;
+ *natural_size = required_natural;
+}
+
+static void
+hdy_header_bar_compute_size_for_opposing_orientation (GtkWidget *widget,
+ gint avail_size,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+ GList *children;
+ gint nvis_children;
+ gint computed_minimum = 0;
+ gint computed_natural = 0;
+ GtkRequestedSize *sizes;
+ GtkPackType packing;
+ gint i;
+ gint child_size;
+ gint child_minimum;
+ gint child_natural;
+ gint center_min, center_nat;
+
+ nvis_children = count_visible_children (self);
+
+ if (nvis_children <= 0)
+ return;
+
+ sizes = g_newa (GtkRequestedSize, nvis_children);
+
+ /* Retrieve desired size for visible children */
+ for (i = 0, children = priv->children; children; children = children->next) {
+ child = children->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ gtk_widget_get_preferred_width (child->widget,
+ &sizes[i].minimum_size,
+ &sizes[i].natural_size);
+
+ sizes[i].data = child;
+ i += 1;
+ }
+ }
+
+ /* Bring children up to size first */
+ gtk_distribute_natural_allocation (MAX (0, avail_size), nvis_children, sizes);
+
+ /* Allocate child positions. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; ++packing) {
+ for (i = 0, children = priv->children; children; children = children->next) {
+ child = children->data;
+
+ /* If widget is not visible, skip it. */
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ /* If widget is packed differently skip it, but still increment i,
+ * since widget is visible and will be handled in next loop
+ * iteration.
+ */
+ if (child->pack_type != packing) {
+ i++;
+ continue;
+ }
+
+ child_size = sizes[i].minimum_size;
+
+ gtk_widget_get_preferred_height_for_width (child->widget,
+ child_size, &child_minimum, &child_natural);
+
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+ }
+
+ center_min = center_nat = 0;
+ if (priv->label_box != NULL) {
+ gtk_widget_get_preferred_height (priv->label_sizing_box,
+ &center_min, &center_nat);
+ }
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title)) {
+ gtk_widget_get_preferred_height (priv->custom_title,
+ &center_min, &center_nat);
+ }
+
+ if (priv->titlebar_start_box != NULL) {
+ gtk_widget_get_preferred_height (priv->titlebar_start_box,
+ &child_minimum, &child_natural);
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ gtk_widget_get_preferred_height (priv->titlebar_end_box,
+ &child_minimum, &child_natural);
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+
+ *minimum_size = computed_minimum;
+ *natural_size = computed_natural;
+}
+
+static void
+hdy_header_bar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ gint css_width, css_height;
+
+ gtk_style_context_get (gtk_widget_get_style_context (widget),
+ gtk_widget_get_state_flags (widget),
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+
+ if (for_size < 0)
+ hdy_header_bar_get_size (widget, orientation, minimum, natural);
+ else if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ hdy_header_bar_compute_size_for_orientation (widget, MAX (for_size, css_height), minimum, natural);
+ else
+ hdy_header_bar_compute_size_for_opposing_orientation (widget, MAX (for_size, css_width), minimum, natural);
+
+ hdy_css_measure (widget, orientation, minimum, natural);
+}
+
+static void
+hdy_header_bar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static GtkWidget *
+get_title_size (HdyHeaderBar *self,
+ gint for_height,
+ GtkRequestedSize *size,
+ gint *expanded)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *title_widget;
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title))
+ title_widget = priv->custom_title;
+ else if (priv->label_box != NULL)
+ title_widget = priv->label_box;
+ else
+ return NULL;
+
+ gtk_widget_get_preferred_width_for_height (title_widget,
+ for_height,
+ &(size->minimum_size),
+ &(size->natural_size));
+
+ *expanded = gtk_widget_compute_expand (title_widget, GTK_ORIENTATION_HORIZONTAL);
+
+ return title_widget;
+}
+
+static void
+children_allocate (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkRequestedSize *sizes,
+ gint decoration_width[2],
+ gint uniform_expand_bonus[2],
+ gint leftover_expand_bonus[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkPackType packing;
+ GtkAllocation child_allocation;
+ gint x;
+ gint i;
+ GList *l;
+ Child *child;
+ gint child_size;
+ /* GtkTextDirection direction; */
+
+ /* Allocate the children on both sides of the title. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ child_allocation.y = allocation->y;
+ child_allocation.height = allocation->height;
+ if (packing == GTK_PACK_START)
+ x = allocation->x + decoration_width[0];
+ else
+ x = allocation->x + allocation->width - decoration_width[1];
+
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type != packing)
+ goto next;
+
+ child_size = sizes[i].minimum_size;
+
+ /* If this child is expanded, give it extra space from the reserves. */
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL)) {
+ gint expand_bonus;
+
+ expand_bonus = uniform_expand_bonus[packing];
+
+ if (leftover_expand_bonus[packing] > 0) {
+ expand_bonus++;
+ leftover_expand_bonus[packing]--;
+ }
+
+ child_size += expand_bonus;
+ }
+
+ child_allocation.width = child_size;
+
+ if (packing == GTK_PACK_START) {
+ child_allocation.x = x;
+ x += child_size;
+ x += priv->spacing;
+ } else {
+ x -= child_size;
+ child_allocation.x = x;
+ x -= priv->spacing;
+ }
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width;
+
+ (*allocations)[i] = child_allocation;
+
+ next:
+ i++;
+ }
+ }
+}
+
+static void
+get_loose_centering_allocations (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkAllocation *title_allocation,
+ gint decoration_width[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkRequestedSize *sizes;
+ gint width;
+ gint nvis_children;
+ GtkRequestedSize title_size = { 0 };
+ gboolean title_expands = FALSE;
+ gint side[2] = { 0 };
+ gint uniform_expand_bonus[2] = { 0 };
+ gint leftover_expand_bonus[2] = { 0 };
+ gint side_free_space[2] = { 0 };
+ gint center_free_space[2] = { 0 };
+ gint nexpand_children[2] = { 0 };
+ gint center_free_space_min;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkPackType packing;
+
+ nvis_children = count_visible_children (self);
+ sizes = g_newa (GtkRequestedSize, nvis_children);
+
+ width = allocation->width - nvis_children * priv->spacing;
+
+ i = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL))
+ nexpand_children[child->pack_type]++;
+
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ allocation->height,
+ &sizes[i].minimum_size,
+ &sizes[i].natural_size);
+ width -= sizes[i].minimum_size;
+ i++;
+ }
+
+ get_title_size (self, allocation->height, &title_size, &title_expands);
+ width -= title_size.minimum_size;
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type == packing)
+ side[packing] += sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+ }
+
+ /* Distribute the available space for natural expansion of the children. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++)
+ width -= decoration_width[packing];
+ width = gtk_distribute_natural_allocation (MAX (0, width), 1, &title_size);
+ width = gtk_distribute_natural_allocation (MAX (0, width), nvis_children, sizes);
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ i = 0;
+ side[packing] = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type == packing)
+ side[packing] += sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+ }
+
+ /* Figure out how much space is left on each side of the title, and earkmark
+ * that space for the expanded children.
+ *
+ * If the title itself is expanded, then it gets half the spoils from each
+ * side.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ side_free_space[packing] = MIN (MAX (allocation->width / 2 - title_size.natural_size / 2 - decoration_width[packing] - side[packing], 0), width);
+ if (title_expands)
+ center_free_space[packing] = nexpand_children[packing] > 0 ?
+ side_free_space[packing] / 2 :
+ side_free_space[packing];
+ }
+ center_free_space_min = MIN (center_free_space[0], center_free_space[1]);
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ center_free_space[packing] = center_free_space_min;
+ side_free_space[packing] -= center_free_space[packing];
+ width -= side_free_space[packing];
+
+ if (nexpand_children[packing] == 0)
+ continue;
+
+ uniform_expand_bonus[packing] = (side_free_space[packing]) / nexpand_children[packing];
+ leftover_expand_bonus[packing] = (side_free_space[packing]) % nexpand_children[packing];
+ }
+
+ children_allocate (self, allocation, allocations, sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus);
+
+ /* We don't enforce css borders on the center widget, to make title/subtitle
+ * combinations fit without growing the header.
+ */
+ title_allocation->y = allocation->y;
+ title_allocation->height = allocation->height;
+
+ title_allocation->width = MIN (allocation->width - decoration_width[0] - side[0] - decoration_width[1] - side[1],
+ title_size.natural_size);
+ title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2;
+
+ /* If the title widget is expanded, then grow it by all the available free
+ * space, and recenter it.
+ */
+ if (title_expands && width > 0) {
+ title_allocation->width += width;
+ title_allocation->x -= width / 2;
+ }
+
+ if (allocation->x + decoration_width[0] + side[0] > title_allocation->x)
+ title_allocation->x = allocation->x + decoration_width[0] + side[0];
+ else if (allocation->x + allocation->width - decoration_width[1] - side[1] < title_allocation->x + title_allocation->width)
+ title_allocation->x = allocation->x + allocation->width - decoration_width[1] - side[1] - title_allocation->width;
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width;
+}
+
+static void
+get_strict_centering_allocations (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkAllocation *title_allocation,
+ gint decoration_width[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ GtkRequestedSize *children_sizes = { 0 };
+ GtkRequestedSize *children_sizes_for_side[2] = { 0 };
+ GtkRequestedSize side_size[2] = { 0 }; /* The size requested by each side. */
+ GtkRequestedSize title_size = { 0 }; /* The size requested by the title. */
+ GtkRequestedSize side_request = { 0 }; /* The maximum size requested by each side, decoration included. */
+ gint side_max; /* The maximum space allocatable to each side, decoration included. */
+ gint title_leftover; /* The or 0px or 1px leftover from ensuring each side is allocated the same size. */
+ /* The space available for expansion on each side, including for the title. */
+ gint free_space[2] = { 0 };
+ /* The space the title will take from the free space of each side for its expansion. */
+ gint title_expand_bonus = 0;
+ gint uniform_expand_bonus[2] = { 0 };
+ gint leftover_expand_bonus[2] = { 0 };
+
+ gint nvis_children, n_side_vis_children[2] = { 0 };
+ gint nexpand_children[2] = { 0 };
+ gboolean title_expands = FALSE;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkPackType packing;
+
+ get_title_size (self, allocation->height, &title_size, &title_expands);
+
+ nvis_children = count_visible_children (self);
+ children_sizes = g_newa (GtkRequestedSize, nvis_children);
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ n_side_vis_children[packing] = count_visible_children_for_pack_type (self, packing);
+ children_sizes_for_side[packing] = packing == 0 ? children_sizes : children_sizes + n_side_vis_children[packing - 1];
+ free_space[packing] = (allocation->width - title_size.minimum_size) / 2 - decoration_width[packing];
+ }
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ i = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL))
+ nexpand_children[child->pack_type]++;
+
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ allocation->height,
+ &children_sizes[i].minimum_size,
+ &children_sizes[i].natural_size);
+ side_size[child->pack_type].minimum_size += children_sizes[i].minimum_size + priv->spacing;
+ side_size[child->pack_type].natural_size += children_sizes[i].natural_size + priv->spacing;
+ free_space[child->pack_type] -= children_sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+
+ /* Figure out the space maximum size requests from each side to help centering
+ * the title.
+ */
+ side_request.minimum_size = MAX (side_size[GTK_PACK_START].minimum_size + decoration_width[GTK_PACK_START],
+ side_size[GTK_PACK_END].minimum_size + decoration_width[GTK_PACK_END]);
+ side_request.natural_size = MAX (side_size[GTK_PACK_START].natural_size + decoration_width[GTK_PACK_START],
+ side_size[GTK_PACK_END].natural_size + decoration_width[GTK_PACK_END]);
+ title_leftover = (allocation->width - title_size.natural_size) % 2;
+ side_max = MAX ((allocation->width - title_size.natural_size) / 2, side_request.minimum_size);
+
+ /* Distribute the available space for natural expansion of the children and
+ * figure out how much space is left on each side of the title, free to be
+ * used for expansion.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ gint leftovers = side_max - side_size[packing].minimum_size - decoration_width[packing];
+ free_space[packing] = gtk_distribute_natural_allocation (leftovers, n_side_vis_children[packing], children_sizes_for_side[packing]);
+ }
+
+ /* Compute how much of each side's free space should be distributed to the
+ * title for its expansion.
+ */
+ title_expand_bonus = !title_expands ? 0 :
+ MIN (nexpand_children[GTK_PACK_START] > 0 ? free_space[GTK_PACK_START] / 2 :
+ free_space[GTK_PACK_START],
+ nexpand_children[GTK_PACK_END] > 0 ? free_space[GTK_PACK_END] / 2 :
+ free_space[GTK_PACK_END]);
+
+ /* Remove the space the title takes from each side for its expansion. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++)
+ free_space[packing] -= title_expand_bonus;
+
+ /* Distribute the free space for expansion of the children. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ if (nexpand_children[packing] == 0)
+ continue;
+
+ uniform_expand_bonus[packing] = free_space[packing] / nexpand_children[packing];
+ leftover_expand_bonus[packing] = free_space[packing] % nexpand_children[packing];
+ }
+
+ children_allocate (self, allocation, allocations, children_sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus);
+
+ /* We don't enforce css borders on the center widget, to make title/subtitle
+ * combinations fit without growing the header.
+ */
+ title_allocation->y = allocation->y;
+ title_allocation->height = allocation->height;
+
+ title_allocation->width = MIN (allocation->width - 2 * side_max + title_leftover,
+ title_size.natural_size);
+ title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2;
+
+ /* If the title widget is expanded, then grow it by the free space available
+ * for it.
+ */
+ if (title_expands) {
+ title_allocation->width += 2 * title_expand_bonus;
+ title_allocation->x -= title_expand_bonus;
+ }
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width;
+}
+
+static void
+hdy_header_bar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkAllocation *allocations;
+ GtkAllocation title_allocation;
+ GtkAllocation clip;
+ gint nvis_children;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkAllocation child_allocation;
+ GtkTextDirection direction;
+ GtkWidget *decoration_box[2] = { priv->titlebar_start_box, priv->titlebar_end_box };
+ gint decoration_width[2] = { 0 };
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (gtk_widget_get_realized (widget))
+ gdk_window_move_resize (gtk_widget_get_window (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height);
+
+ hdy_css_size_allocate (widget, allocation);
+
+ direction = gtk_widget_get_direction (widget);
+ nvis_children = count_visible_children (self);
+ allocations = g_newa (GtkAllocation, nvis_children);
+
+ /* Get the decoration width. */
+ for (GtkPackType packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ gint min, nat;
+
+ if (decoration_box[packing] == NULL)
+ continue;
+
+ gtk_widget_get_preferred_width_for_height (decoration_box[packing],
+ allocation->height,
+ &min, &nat);
+ decoration_width[packing] = nat + priv->spacing;
+ }
+
+ /* Allocate the decoration widgets. */
+ child_allocation.y = allocation->y;
+ child_allocation.height = allocation->height;
+
+ if (priv->titlebar_start_box) {
+ if (direction == GTK_TEXT_DIR_LTR)
+ child_allocation.x = allocation->x;
+ else
+ child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_START] + priv->spacing;
+ child_allocation.width = decoration_width[GTK_PACK_START] - priv->spacing;
+ gtk_widget_size_allocate (priv->titlebar_start_box, &child_allocation);
+ }
+
+ if (priv->titlebar_end_box) {
+ if (direction != GTK_TEXT_DIR_LTR)
+ child_allocation.x = allocation->x;
+ else
+ child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_END] + priv->spacing;
+ child_allocation.width = decoration_width[GTK_PACK_END] - priv->spacing;
+ gtk_widget_size_allocate (priv->titlebar_end_box, &child_allocation);
+ }
+
+ /* Get the allocation for widgets on both side of the title. */
+ if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (priv->centering_policy == HDY_CENTERING_POLICY_STRICT)
+ get_strict_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ else
+ get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ } else {
+ /* For memory usage optimisation's sake, we will use the allocations
+ * variable to store the loose centering allocations and the
+ * title_allocation variable to store the loose title allocation.
+ */
+ GtkAllocation *strict_allocations = g_newa (GtkAllocation, nvis_children);
+ GtkAllocation strict_title_allocation;
+ gdouble strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE);
+
+ if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT)
+ strict_centering_t = 1.0 - strict_centering_t;
+
+ get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ get_strict_centering_allocations (self, allocation, &strict_allocations, &strict_title_allocation, decoration_width);
+
+ for (i = 0; i < nvis_children; i++) {
+ allocations[i].x = hdy_lerp (allocations[i].x, strict_allocations[i].x, strict_centering_t);
+ allocations[i].y = hdy_lerp (allocations[i].y, strict_allocations[i].y, strict_centering_t);
+ allocations[i].width = hdy_lerp (allocations[i].width, strict_allocations[i].width, strict_centering_t);
+ allocations[i].height = hdy_lerp (allocations[i].height, strict_allocations[i].height, strict_centering_t);
+ }
+ title_allocation.x = hdy_lerp (title_allocation.x, strict_title_allocation.x, strict_centering_t);
+ title_allocation.y = hdy_lerp (title_allocation.y, strict_title_allocation.y, strict_centering_t);
+ title_allocation.width = hdy_lerp (title_allocation.width, strict_title_allocation.width, strict_centering_t);
+ title_allocation.height = hdy_lerp (title_allocation.height, strict_title_allocation.height, strict_centering_t);
+ }
+
+ /* Allocate the children on both sides of the title. */
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ gtk_widget_size_allocate (child->widget, &allocations[i]);
+ i++;
+ }
+
+ /* Allocate the title widget. */
+ if (priv->custom_title != NULL && gtk_widget_get_visible (priv->custom_title))
+ gtk_widget_size_allocate (priv->custom_title, &title_allocation);
+ else if (priv->label_box != NULL)
+ gtk_widget_size_allocate (priv->label_box, &title_allocation);
+
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_header_bar_destroy (GtkWidget *widget)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (widget));
+
+ if (priv->label_sizing_box) {
+ gtk_widget_destroy (priv->label_sizing_box);
+ g_clear_object (&priv->label_sizing_box);
+ }
+
+ if (priv->custom_title) {
+ gtk_widget_unparent (priv->custom_title);
+ priv->custom_title = NULL;
+ }
+
+ if (priv->label_box) {
+ gtk_widget_unparent (priv->label_box);
+ priv->label_box = NULL;
+ }
+
+ if (priv->titlebar_start_box) {
+ gtk_widget_unparent (priv->titlebar_start_box);
+ priv->titlebar_start_box = NULL;
+ priv->titlebar_start_separator = NULL;
+ }
+
+ if (priv->titlebar_end_box) {
+ gtk_widget_unparent (priv->titlebar_end_box);
+ priv->titlebar_end_box = NULL;
+ priv->titlebar_end_separator = NULL;
+ }
+
+ GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->destroy (widget);
+}
+
+static void
+hdy_header_bar_finalize (GObject *object)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (object));
+
+ g_clear_pointer (&priv->title, g_free);
+ g_clear_pointer (&priv->subtitle, g_free);
+ g_clear_pointer (&priv->decoration_layout, g_free);
+ g_clear_object (&priv->controller);
+
+ G_OBJECT_CLASS (hdy_header_bar_parent_class)->finalize (object);
+}
+
+static void
+hdy_header_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (object);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, priv->title);
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, priv->subtitle);
+ break;
+ case PROP_CUSTOM_TITLE:
+ g_value_set_object (value, priv->custom_title);
+ break;
+ case PROP_SPACING:
+ g_value_set_int (value, priv->spacing);
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ g_value_set_boolean (value, hdy_header_bar_get_show_close_button (self));
+ break;
+ case PROP_HAS_SUBTITLE:
+ g_value_set_boolean (value, hdy_header_bar_get_has_subtitle (self));
+ break;
+ case PROP_DECORATION_LAYOUT:
+ g_value_set_string (value, hdy_header_bar_get_decoration_layout (self));
+ break;
+ case PROP_DECORATION_LAYOUT_SET:
+ g_value_set_boolean (value, priv->decoration_layout_set);
+ break;
+ case PROP_CENTERING_POLICY:
+ g_value_set_enum (value, hdy_header_bar_get_centering_policy (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_header_bar_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_header_bar_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_header_bar_get_interpolate_size (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_header_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (object);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ hdy_header_bar_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_header_bar_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_CUSTOM_TITLE:
+ hdy_header_bar_set_custom_title (self, g_value_get_object (value));
+ break;
+ case PROP_SPACING:
+ if (priv->spacing != g_value_get_int (value)) {
+ priv->spacing = g_value_get_int (value);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (object, pspec);
+ }
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ hdy_header_bar_set_show_close_button (self, g_value_get_boolean (value));
+ break;
+ case PROP_HAS_SUBTITLE:
+ hdy_header_bar_set_has_subtitle (self, g_value_get_boolean (value));
+ break;
+ case PROP_DECORATION_LAYOUT:
+ hdy_header_bar_set_decoration_layout (self, g_value_get_string (value));
+ break;
+ case PROP_DECORATION_LAYOUT_SET:
+ priv->decoration_layout_set = g_value_get_boolean (value);
+ break;
+ case PROP_CENTERING_POLICY:
+ hdy_header_bar_set_centering_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_header_bar_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_header_bar_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+notify_child_cb (GObject *child,
+ GParamSpec *pspec,
+ HdyHeaderBar *self)
+{
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static void
+hdy_header_bar_pack (HdyHeaderBar *self,
+ GtkWidget *widget,
+ GtkPackType pack_type)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+
+ g_return_if_fail (gtk_widget_get_parent (widget) == NULL);
+
+ child = g_new (Child, 1);
+ child->widget = widget;
+ child->pack_type = pack_type;
+
+ priv->children = g_list_append (priv->children, child);
+
+ gtk_widget_freeze_child_notify (widget);
+ gtk_widget_set_parent (widget, GTK_WIDGET (self));
+ g_signal_connect (widget, "notify::visible", G_CALLBACK (notify_child_cb), self);
+ gtk_widget_child_notify (widget, "pack-type");
+ gtk_widget_child_notify (widget, "position");
+ gtk_widget_thaw_child_notify (widget);
+
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static void
+hdy_header_bar_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (HDY_HEADER_BAR (container), child, GTK_PACK_START);
+}
+
+static GList *
+find_child_link (HdyHeaderBar *self,
+ GtkWidget *widget,
+ gint *position)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint i;
+
+ for (l = priv->children, i = 0; l; l = l->next, i++) {
+ child = l->data;
+ if (child->widget == widget) {
+ if (position)
+ *position = i;
+
+ return l;
+ }
+ }
+
+ return NULL;
+}
+
+static void
+hdy_header_bar_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l) {
+ child = l->data;
+ g_signal_handlers_disconnect_by_func (widget, notify_child_cb, self);
+ gtk_widget_unparent (child->widget);
+ priv->children = g_list_delete_link (priv->children, l);
+ g_free (child);
+ gtk_widget_queue_resize (GTK_WIDGET (container));
+ _hdy_header_bar_update_separator_visibility (self);
+ }
+}
+
+static void
+hdy_header_bar_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+ GList *children;
+
+ if (include_internals && priv->titlebar_start_box != NULL)
+ (* callback) (priv->titlebar_start_box, callback_data);
+
+ children = priv->children;
+ while (children) {
+ child = children->data;
+ children = children->next;
+ if (child->pack_type == GTK_PACK_START)
+ (* callback) (child->widget, callback_data);
+ }
+
+ if (priv->custom_title != NULL)
+ (* callback) (priv->custom_title, callback_data);
+
+ if (include_internals && priv->label_box != NULL)
+ (* callback) (priv->label_box, callback_data);
+
+ children = priv->children;
+ while (children) {
+ child = children->data;
+ children = children->next;
+ if (child->pack_type == GTK_PACK_END)
+ (* callback) (child->widget, callback_data);
+ }
+
+ if (include_internals && priv->titlebar_end_box != NULL)
+ (* callback) (priv->titlebar_end_box, callback_data);
+}
+
+static void
+hdy_header_bar_reorder_child (HdyHeaderBar *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ gint old_position;
+ Child *child;
+
+ l = find_child_link (self, widget, &old_position);
+
+ if (l == NULL)
+ return;
+
+ if (old_position == position)
+ return;
+
+ child = l->data;
+ priv->children = g_list_delete_link (priv->children, l);
+
+ if (position < 0)
+ l = NULL;
+ else
+ l = g_list_nth (priv->children, position);
+
+ priv->children = g_list_insert_before (priv->children, l, child);
+ gtk_widget_child_notify (widget, "position");
+ gtk_widget_queue_resize (widget);
+}
+
+static GType
+hdy_header_bar_child_type (GtkContainer *container)
+{
+ return GTK_TYPE_WIDGET;
+}
+
+static void
+hdy_header_bar_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l == NULL) {
+ g_param_value_set_default (pspec, value);
+
+ return;
+ }
+
+ child = l->data;
+
+ switch (property_id) {
+ case CHILD_PROP_PACK_TYPE:
+ g_value_set_enum (value, child->pack_type);
+ break;
+
+ case CHILD_PROP_POSITION:
+ g_value_set_int (value, g_list_position (priv->children, l));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_header_bar_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l == NULL)
+ return;
+
+ child = l->data;
+
+ switch (property_id) {
+ case CHILD_PROP_PACK_TYPE:
+ child->pack_type = g_value_get_enum (value);
+ _hdy_header_bar_update_separator_visibility (self);
+ gtk_widget_queue_resize (widget);
+ break;
+
+ case CHILD_PROP_POSITION:
+ hdy_header_bar_reorder_child (self, widget, g_value_get_int (value));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static gboolean
+hdy_header_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ /* GtkWidget draws nothing by default so we have to render the background
+ * explicitly for HdyHederBar to render the typical titlebar background.
+ */
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ /* Ditto for the borders. */
+ gtk_render_frame (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+
+ return GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->draw (widget, cr);
+}
+
+static void
+hdy_header_bar_realize (GtkWidget *widget)
+{
+ GtkSettings *settings;
+ GtkAllocation allocation;
+ GdkWindowAttr attributes;
+ gint attributes_mask;
+ GdkWindow *window;
+
+ settings = gtk_widget_get_settings (widget);
+ g_signal_connect_swapped (settings, "notify::gtk-shell-shows-app-menu",
+ G_CALLBACK (hdy_header_bar_update_window_buttons), widget);
+ g_signal_connect_swapped (settings, "notify::gtk-decoration-layout",
+ G_CALLBACK (hdy_header_bar_update_window_buttons), widget);
+ update_is_mobile_window (HDY_HEADER_BAR (widget));
+ hdy_header_bar_update_window_buttons (HDY_HEADER_BAR (widget));
+
+ gtk_widget_get_allocation (widget, &allocation);
+ gtk_widget_set_realized (widget, TRUE);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes,
+ attributes_mask);
+ gtk_widget_set_window (widget, window);
+ gtk_widget_register_window (widget, window);
+}
+
+static void
+hdy_header_bar_unrealize (GtkWidget *widget)
+{
+ GtkSettings *settings;
+
+ settings = gtk_widget_get_settings (widget);
+
+ g_signal_handlers_disconnect_by_func (settings, hdy_header_bar_update_window_buttons, widget);
+
+ GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->unrealize (widget);
+}
+
+static gboolean
+window_state_changed (GtkWidget *window,
+ GdkEventWindowState *event,
+ gpointer data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (data);
+
+ if (event->changed_mask & (GDK_WINDOW_STATE_FULLSCREEN |
+ GDK_WINDOW_STATE_MAXIMIZED |
+ GDK_WINDOW_STATE_TILED |
+ GDK_WINDOW_STATE_TOP_TILED |
+ GDK_WINDOW_STATE_RIGHT_TILED |
+ GDK_WINDOW_STATE_BOTTOM_TILED |
+ GDK_WINDOW_STATE_LEFT_TILED))
+ hdy_header_bar_update_window_buttons (self);
+
+ return FALSE;
+}
+
+static void
+hdy_header_bar_hierarchy_changed (GtkWidget *widget,
+ GtkWidget *previous_toplevel)
+{
+ GtkWidget *toplevel;
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ toplevel = gtk_widget_get_toplevel (widget);
+
+ if (previous_toplevel)
+ g_signal_handlers_disconnect_by_func (previous_toplevel,
+ window_state_changed, widget);
+
+ if (toplevel)
+ g_signal_connect_after (toplevel, "window-state-event",
+ G_CALLBACK (window_state_changed), widget);
+
+ if (priv->window_size_allocated_id > 0) {
+ g_signal_handler_disconnect (previous_toplevel, priv->window_size_allocated_id);
+ priv->window_size_allocated_id = 0;
+ }
+
+ if (GTK_IS_WINDOW (toplevel))
+ priv->window_size_allocated_id =
+ g_signal_connect_swapped (toplevel, "size-allocate",
+ G_CALLBACK (update_is_mobile_window), self);
+
+ update_is_mobile_window (self);
+ hdy_header_bar_update_window_buttons (self);
+}
+
+static void
+hdy_header_bar_class_init (HdyHeaderBarClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class);
+
+ object_class->finalize = hdy_header_bar_finalize;
+ object_class->get_property = hdy_header_bar_get_property;
+ object_class->set_property = hdy_header_bar_set_property;
+
+ widget_class->destroy = hdy_header_bar_destroy;
+ widget_class->size_allocate = hdy_header_bar_size_allocate;
+ widget_class->get_preferred_width = hdy_header_bar_get_preferred_width;
+ widget_class->get_preferred_height = hdy_header_bar_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_header_bar_get_preferred_height_for_width;
+ widget_class->get_preferred_width_for_height = hdy_header_bar_get_preferred_width_for_height;
+ widget_class->draw = hdy_header_bar_draw;
+ widget_class->realize = hdy_header_bar_realize;
+ widget_class->unrealize = hdy_header_bar_unrealize;
+ widget_class->hierarchy_changed = hdy_header_bar_hierarchy_changed;
+
+ container_class->add = hdy_header_bar_add;
+ container_class->remove = hdy_header_bar_remove;
+ container_class->forall = hdy_header_bar_forall;
+ container_class->child_type = hdy_header_bar_child_type;
+ container_class->set_child_property = hdy_header_bar_set_child_property;
+ container_class->get_child_property = hdy_header_bar_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ gtk_container_class_install_child_property (container_class,
+ CHILD_PROP_PACK_TYPE,
+ g_param_spec_enum ("pack-type",
+ _("Pack type"),
+ _("A GtkPackType indicating whether the child is packed with reference to the start or end of the parent"),
+ GTK_TYPE_PACK_TYPE, GTK_PACK_START,
+ G_PARAM_READWRITE));
+ gtk_container_class_install_child_property (container_class,
+ CHILD_PROP_POSITION,
+ g_param_spec_int ("position",
+ _("Position"),
+ _("The index of the child in the parent"),
+ -1, G_MAXINT, 0,
+ G_PARAM_READWRITE));
+
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title to display"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle to display"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ props[PROP_CUSTOM_TITLE] =
+ g_param_spec_object ("custom-title",
+ _("Custom Title"),
+ _("Custom title widget to display"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+ props[PROP_SPACING] =
+ g_param_spec_int ("spacing",
+ _("Spacing"),
+ _("The amount of space between children"),
+ 0, G_MAXINT,
+ DEFAULT_SPACING,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyHeaderBar:show-close-button:
+ *
+ * Whether to show window decorations.
+ *
+ * Which buttons are actually shown and where is determined
+ * by the #HdyHeaderBar:decoration-layout property, and by
+ * the state of the window (e.g. a close button will not be
+ * shown if the window can't be closed).
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_SHOW_CLOSE_BUTTON] =
+ g_param_spec_boolean ("show-close-button",
+ _("Show decorations"),
+ _("Whether to show window decorations"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyHeaderBar:decoration-layout:
+ *
+ * The decoration layout for buttons. If this property is
+ * not set, the #GtkSettings:gtk-decoration-layout setting
+ * is used.
+ *
+ * See hdy_header_bar_set_decoration_layout() for information
+ * about the format of this string.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DECORATION_LAYOUT] =
+ g_param_spec_string ("decoration-layout",
+ _("Decoration Layout"),
+ _("The layout for window decorations"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyHeaderBar:decoration-layout-set:
+ *
+ * Set to %TRUE if #HdyHeaderBar:decoration-layout is set.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DECORATION_LAYOUT_SET] =
+ g_param_spec_boolean ("decoration-layout-set",
+ _("Decoration Layout Set"),
+ _("Whether the decoration-layout property has been set"),
+ FALSE,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyHeaderBar:has-subtitle:
+ *
+ * If %TRUE, reserve space for a subtitle, even if none
+ * is currently set.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_HAS_SUBTITLE] =
+ g_param_spec_boolean ("has-subtitle",
+ _("Has Subtitle"),
+ _("Whether to reserve space for a subtitle"),
+ TRUE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CENTERING_POLICY] =
+ g_param_spec_enum ("centering-policy",
+ _("Centering policy"),
+ _("The policy to horizontally align the center widget"),
+ HDY_TYPE_CENTERING_POLICY, HDY_CENTERING_POLICY_LOOSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "headerbar");
+}
+
+static void
+hdy_header_bar_init (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+ GtkStyleContext *context;
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ priv->title = NULL;
+ priv->subtitle = NULL;
+ priv->custom_title = NULL;
+ priv->children = NULL;
+ priv->spacing = DEFAULT_SPACING;
+ priv->has_subtitle = TRUE;
+ priv->decoration_layout = NULL;
+ priv->decoration_layout_set = FALSE;
+ priv->transition_duration = 200;
+
+ init_sizing_box (self);
+ construct_label_box (self);
+
+ priv->controller = hdy_window_handle_controller_new (GTK_WIDGET (self));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ /* Ensure the widget has the titlebar style class. */
+ gtk_style_context_add_class (context, "titlebar");
+}
+
+static void
+hdy_header_bar_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ if (type && strcmp (type, "title") == 0)
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (buildable), GTK_WIDGET (child));
+ else if (!type)
+ gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (HDY_HEADER_BAR (buildable), type);
+}
+
+static void
+hdy_header_bar_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_header_bar_buildable_add_child;
+}
+
+/**
+ * hdy_header_bar_new:
+ *
+ * Creates a new #HdyHeaderBar widget.
+ *
+ * Returns: a new #HdyHeaderBar
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_header_bar_new (void)
+{
+ return GTK_WIDGET (g_object_new (HDY_TYPE_HEADER_BAR, NULL));
+}
+
+/**
+ * hdy_header_bar_pack_start:
+ * @self: A #HdyHeaderBar
+ * @child: the #GtkWidget to be added to @self:
+ *
+ * Adds @child to @self:, packed with reference to the
+ * start of the @self:.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_pack_start (HdyHeaderBar *self,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (self, child, GTK_PACK_START);
+}
+
+/**
+ * hdy_header_bar_pack_end:
+ * @self: A #HdyHeaderBar
+ * @child: the #GtkWidget to be added to @self:
+ *
+ * Adds @child to @self:, packed with reference to the
+ * end of the @self:.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_pack_end (HdyHeaderBar *self,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (self, child, GTK_PACK_END);
+}
+
+/**
+ * hdy_header_bar_set_title:
+ * @self: a #HdyHeaderBar
+ * @title: (nullable): a title, or %NULL
+ *
+ * Sets the title of the #HdyHeaderBar. The title should help a user
+ * identify the current view. A good title should not include the
+ * application name.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_title (HdyHeaderBar *self,
+ const gchar *title)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gchar *new_title;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ new_title = g_strdup (title);
+ g_free (priv->title);
+ priv->title = new_title;
+
+ if (priv->title_label != NULL) {
+ gtk_label_set_label (GTK_LABEL (priv->title_label), priv->title);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_header_bar_get_title:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the title of the header. See hdy_header_bar_set_title().
+ *
+ * Returns: (nullable): the title of the header, or %NULL if none has
+ * been set explicitly. The returned string is owned by the widget
+ * and must not be modified or freed.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_title (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->title;
+}
+
+/**
+ * hdy_header_bar_set_subtitle:
+ * @self: a #HdyHeaderBar
+ * @subtitle: (nullable): a subtitle, or %NULL
+ *
+ * Sets the subtitle of the #HdyHeaderBar. The title should give a user
+ * an additional detail to help them identify the current view.
+ *
+ * Note that HdyHeaderBar by default reserves room for the subtitle,
+ * even if none is currently set. If this is not desired, set the
+ * #HdyHeaderBar:has-subtitle property to %FALSE.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_subtitle (HdyHeaderBar *self,
+ const gchar *subtitle)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gchar *new_subtitle;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ new_subtitle = g_strdup (subtitle);
+ g_free (priv->subtitle);
+ priv->subtitle = new_subtitle;
+
+ if (priv->subtitle_label != NULL) {
+ gtk_label_set_label (GTK_LABEL (priv->subtitle_label), priv->subtitle);
+ gtk_widget_set_visible (priv->subtitle_label, priv->subtitle && priv->subtitle[0]);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ }
+
+ gtk_widget_set_visible (priv->subtitle_sizing_label, priv->has_subtitle || (priv->subtitle && priv->subtitle[0]));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_header_bar_get_subtitle:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the subtitle of the header. See hdy_header_bar_set_subtitle().
+ *
+ * Returns: (nullable): the subtitle of the header, or %NULL if none has
+ * been set explicitly. The returned string is owned by the widget
+ * and must not be modified or freed.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_subtitle (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->subtitle;
+}
+
+/**
+ * hdy_header_bar_set_custom_title:
+ * @self: a #HdyHeaderBar
+ * @title_widget: (nullable): a custom widget to use for a title
+ *
+ * Sets a custom title for the #HdyHeaderBar.
+ *
+ * The title should help a user identify the current view. This
+ * supersedes any title set by hdy_header_bar_set_title() or
+ * hdy_header_bar_set_subtitle(). To achieve the same style as
+ * the builtin title and subtitle, use the “title” and “subtitle”
+ * style classes.
+ *
+ * You should set the custom title to %NULL, for the header title
+ * label to be visible again.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_custom_title (HdyHeaderBar *self,
+ GtkWidget *title_widget)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+ if (title_widget)
+ g_return_if_fail (GTK_IS_WIDGET (title_widget));
+
+ /* No need to do anything if the custom widget stays the same */
+ if (priv->custom_title == title_widget)
+ return;
+
+ if (priv->custom_title) {
+ GtkWidget *custom = priv->custom_title;
+
+ priv->custom_title = NULL;
+ gtk_widget_unparent (custom);
+ }
+
+ if (title_widget != NULL) {
+ priv->custom_title = title_widget;
+
+ gtk_widget_set_parent (priv->custom_title, GTK_WIDGET (self));
+
+ if (priv->label_box != NULL) {
+ GtkWidget *label_box = priv->label_box;
+
+ priv->label_box = NULL;
+ priv->title_label = NULL;
+ priv->subtitle_label = NULL;
+ gtk_widget_unparent (label_box);
+ }
+ } else {
+ if (priv->label_box == NULL)
+ construct_label_box (self);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CUSTOM_TITLE]);
+}
+
+/**
+ * hdy_header_bar_get_custom_title:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the custom title widget of the header. See
+ * hdy_header_bar_set_custom_title().
+ *
+ * Returns: (nullable) (transfer none): the custom title widget
+ * of the header, or %NULL if none has been set explicitly.
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_header_bar_get_custom_title (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->custom_title;
+}
+
+/**
+ * hdy_header_bar_get_show_close_button:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns whether this header bar shows the standard window
+ * decorations.
+ *
+ * Returns: %TRUE if the decorations are shown
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_show_close_button (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->shows_wm_decorations;
+}
+
+/**
+ * hdy_header_bar_set_show_close_button:
+ * @self: a #HdyHeaderBar
+ * @setting: %TRUE to show standard window decorations
+ *
+ * Sets whether this header bar shows the standard window decorations,
+ * including close, maximize, and minimize.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_show_close_button (HdyHeaderBar *self,
+ gboolean setting)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ setting = setting != FALSE;
+
+ if (priv->shows_wm_decorations == setting)
+ return;
+
+ priv->shows_wm_decorations = setting;
+ hdy_header_bar_update_window_buttons (self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_CLOSE_BUTTON]);
+}
+
+/**
+ * hdy_header_bar_set_has_subtitle:
+ * @self: a #HdyHeaderBar
+ * @setting: %TRUE to reserve space for a subtitle
+ *
+ * Sets whether the header bar should reserve space
+ * for a subtitle, even if none is currently set.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_has_subtitle (HdyHeaderBar *self,
+ gboolean setting)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ setting = setting != FALSE;
+
+ if (priv->has_subtitle == setting)
+ return;
+
+ priv->has_subtitle = setting;
+ gtk_widget_set_visible (priv->subtitle_sizing_label, setting || (priv->subtitle && priv->subtitle[0]));
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_SUBTITLE]);
+}
+
+/**
+ * hdy_header_bar_get_has_subtitle:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves whether the header bar reserves space for
+ * a subtitle, regardless if one is currently set or not.
+ *
+ * Returns: %TRUE if the header bar reserves space
+ * for a subtitle
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_has_subtitle (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->has_subtitle;
+}
+
+/**
+ * hdy_header_bar_set_decoration_layout:
+ * @self: a #HdyHeaderBar
+ * @layout: (nullable): a decoration layout, or %NULL to unset the layout
+ *
+ * Sets the decoration layout for this header bar, overriding
+ * the #GtkSettings:gtk-decoration-layout setting.
+ *
+ * There can be valid reasons for overriding the setting, such
+ * as a header bar design that does not allow for buttons to take
+ * room on the right, or only offers room for a single close button.
+ * Split header bars are another example for overriding the
+ * setting.
+ *
+ * The format of the string is button names, separated by commas.
+ * A colon separates the buttons that should appear on the left
+ * from those on the right. Recognized button names are minimize,
+ * maximize, close, icon (the window icon) and menu (a menu button
+ * for the fallback app menu).
+ *
+ * For example, “menu:minimize,maximize,close” specifies a menu
+ * on the left, and minimize, maximize and close buttons on the right.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_decoration_layout (HdyHeaderBar *self,
+ const gchar *layout)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ g_clear_pointer (&priv->decoration_layout, g_free);
+ priv->decoration_layout = g_strdup (layout);
+ priv->decoration_layout_set = (layout != NULL);
+
+ hdy_header_bar_update_window_buttons (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT_SET]);
+}
+
+/**
+ * hdy_header_bar_get_decoration_layout:
+ * @self: a #HdyHeaderBar
+ *
+ * Gets the decoration layout set with
+ * hdy_header_bar_set_decoration_layout().
+ *
+ * Returns: the decoration layout
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_decoration_layout (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->decoration_layout;
+}
+
+/**
+ * hdy_header_bar_get_centering_policy:
+ * @self: a #HdyHeaderBar
+ *
+ * Gets the policy @self follows to horizontally align its center widget.
+ *
+ * Returns: the centering policy
+ *
+ * Since: 0.0.10
+ */
+HdyCenteringPolicy
+hdy_header_bar_get_centering_policy (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), HDY_CENTERING_POLICY_LOOSE);
+
+ return priv->centering_policy;
+}
+
+/**
+ * hdy_header_bar_set_centering_policy:
+ * @self: a #HdyHeaderBar
+ * @centering_policy: the centering policy
+ *
+ * Sets the policy @self must follow to horizontally align its center widget.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_centering_policy (HdyHeaderBar *self,
+ HdyCenteringPolicy centering_policy)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ if (priv->centering_policy == centering_policy)
+ return;
+
+ priv->centering_policy = centering_policy;
+ if (priv->interpolate_size)
+ hdy_header_bar_start_transition (self, priv->transition_duration);
+ else
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CENTERING_POLICY]);
+}
+
+/**
+ * hdy_header_bar_get_transition_duration:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between pages in @self will take.
+ *
+ * Returns: the transition duration
+ *
+ * Since: 0.0.10
+ */
+guint
+hdy_header_bar_get_transition_duration (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), 0);
+
+ return priv->transition_duration;
+}
+
+/**
+ * hdy_header_bar_set_transition_duration:
+ * @self: a #HdyHeaderBar
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between pages in @self
+ * will take.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_transition_duration (HdyHeaderBar *self,
+ guint duration)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ if (priv->transition_duration == duration)
+ return;
+
+ priv->transition_duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_header_bar_get_transition_running:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns whether the @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_transition_running (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ return (priv->tick_id != 0);
+}
+
+/**
+ * hdy_header_bar_get_interpolate_size:
+ * @self: A #HdyHeaderBar
+ *
+ * Gets whether @self should interpolate its size on visible child change.
+ *
+ * See hdy_header_bar_set_interpolate_size().
+ *
+ * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_interpolate_size (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->interpolate_size;
+}
+
+/**
+ * hdy_header_bar_set_interpolate_size:
+ * @self: A #HdyHeaderBar
+ * @interpolate_size: %TRUE to interpolate the size
+ *
+ * Sets whether or not @self will interpolate the size of its opposing
+ * orientation when changing the visible child. If %TRUE, @self will interpolate
+ * its size between the one of the previous visible child and the one of the new
+ * visible child, according to the set transition duration and the orientation,
+ * e.g. if @self is horizontal, it will interpolate the its height.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_interpolate_size (HdyHeaderBar *self,
+ gboolean interpolate_size)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ interpolate_size = !!interpolate_size;
+
+ if (priv->interpolate_size == interpolate_size)
+ return;
+
+ priv->interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
diff --git a/subprojects/libhandy/src/hdy-header-bar.h b/subprojects/libhandy/src/hdy-header-bar.h
new file mode 100644
index 0000000..066d847
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-bar.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * This program 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 program 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 program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_HEADER_BAR (hdy_header_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyHeaderBar, hdy_header_bar, HDY, HEADER_BAR, GtkContainer)
+
+typedef enum {
+ HDY_CENTERING_POLICY_LOOSE,
+ HDY_CENTERING_POLICY_STRICT,
+} HdyCenteringPolicy;
+
+/**
+ * HdyHeaderBarClass
+ * @parent_class: The parent class
+ */
+struct _HdyHeaderBarClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_header_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_title (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_title (HdyHeaderBar *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_subtitle (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_subtitle (HdyHeaderBar *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_header_bar_get_custom_title (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_custom_title (HdyHeaderBar *self,
+ GtkWidget *title_widget);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_pack_start (HdyHeaderBar *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_pack_end (HdyHeaderBar *self,
+ GtkWidget *child);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_show_close_button (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_show_close_button (HdyHeaderBar *self,
+ gboolean setting);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_has_subtitle (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_has_subtitle (HdyHeaderBar *self,
+ gboolean setting);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_decoration_layout (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_decoration_layout (HdyHeaderBar *self,
+ const gchar *layout);
+
+HDY_AVAILABLE_IN_ALL
+HdyCenteringPolicy hdy_header_bar_get_centering_policy (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_centering_policy (HdyHeaderBar *self,
+ HdyCenteringPolicy centering_policy);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_header_bar_get_transition_duration (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_transition_duration (HdyHeaderBar *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_transition_running (HdyHeaderBar *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_interpolate_size (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_interpolate_size (HdyHeaderBar *self,
+ gboolean interpolate_size);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-header-group.c b/subprojects/libhandy/src/hdy-header-group.c
new file mode 100644
index 0000000..e8287fa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-group.c
@@ -0,0 +1,1115 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-header-group.h"
+
+/**
+ * SECTION:hdy-header-group
+ * @short_description: An object handling composite title bars.
+ * @Title: HdyHeaderGroup
+ * @See_also: #GtkHeaderBar, #HdyHeaderBar, #HdyLeaflet
+ *
+ * The #HdyHeaderGroup object handles the header bars of a composite title bar.
+ * It splits the window decoration across the header bars, giving the left side
+ * of the decorations to the leftmost header bar, and the right side of the
+ * decorations to the rightmost header bar.
+ * See hdy_header_bar_set_decoration_layout().
+ *
+ * The #HdyHeaderGroup:decorate-all property can be used in conjunction with
+ * #HdyLeaflet:folded when the title bar is split across the pages of a
+ * #HdyLeaflet to automatically display the decorations on all the pages when
+ * the leaflet is folded.
+ *
+ * You can nest header groups, which is convenient when you nest leaflets too:
+ * |[
+ * <object class="HdyHeaderGroup" id="inner_header_group">
+ * <property name="decorate-all" bind-source="inner_leaflet" bind-property="folded" bind-flags="sync-create"/>
+ * <headerbars>
+ * <headerbar name="inner_header_bar_1"/>
+ * <headerbar name="inner_header_bar_2"/>
+ * </headerbars>
+ * </object>
+ * <object class="HdyHeaderGroup" id="outer_header_group">
+ * <property name="decorate-all" bind-source="outer_leaflet" bind-property="folded" bind-flags="sync-create"/>
+ * <headerbars>
+ * <headerbar name="inner_header_group"/>
+ * <headerbar name="outer_header_bar"/>
+ * </headerbars>
+ * </object>
+ * ]|
+ *
+ * Since: 0.0.4
+ */
+
+/**
+ * HdyHeaderGroupChildType:
+ * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: The child is a #HdyHeaderBar
+ * @HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: The child is a #GtkHeaderBar
+ * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: The child is a #HdyHeaderGroup
+ *
+ * This enumeration value describes the child types handled by #HdyHeaderGroup.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyHeaderGroupChild
+{
+ GObject parent_instance;
+
+ HdyHeaderGroupChildType type;
+ GObject *object;
+};
+
+enum {
+ SIGNAL_UPDATE_DECORATION_LAYOUTS,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+G_DEFINE_TYPE (HdyHeaderGroupChild, hdy_header_group_child, G_TYPE_OBJECT)
+
+struct _HdyHeaderGroup
+{
+ GObject parent_instance;
+
+ GSList *children;
+ gboolean decorate_all;
+ gchar *layout;
+};
+
+static void hdy_header_group_buildable_init (GtkBuildableIface *iface);
+static gboolean hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *data);
+static void hdy_header_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data);
+
+G_DEFINE_TYPE_WITH_CODE (HdyHeaderGroup, hdy_header_group, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_header_group_buildable_init))
+
+enum {
+ PROP_0,
+ PROP_DECORATE_ALL,
+ N_PROPS
+};
+
+static GParamSpec *props [N_PROPS];
+
+static void update_decoration_layouts (HdyHeaderGroup *self);
+
+static void
+object_destroyed_cb (HdyHeaderGroupChild *self,
+ GObject *object)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ self->object = NULL;
+
+ g_object_unref (self);
+}
+
+static void
+forward_update_decoration_layouts (HdyHeaderGroupChild *self)
+{
+ HdyHeaderGroup *header_group;
+
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ header_group = HDY_HEADER_GROUP (g_object_get_data (G_OBJECT (self), "header-group"));
+
+ g_assert (HDY_IS_HEADER_GROUP (header_group));
+
+ g_signal_emit (header_group, signals[SIGNAL_UPDATE_DECORATION_LAYOUTS], 0);
+
+ update_decoration_layouts (header_group);
+}
+
+static void
+hdy_header_group_child_dispose (GObject *object)
+{
+ HdyHeaderGroupChild *self = (HdyHeaderGroupChild *)object;
+
+ if (self->object) {
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (object_destroyed_cb), self);
+ g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (forward_update_decoration_layouts), self);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ g_object_weak_unref (self->object, (GWeakNotify) object_destroyed_cb, self);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ self->object = NULL;
+ }
+
+ G_OBJECT_CLASS (hdy_header_group_child_parent_class)->dispose (object);
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_header_bar (HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *self;
+ gpointer header_group;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (header_bar), NULL);
+
+ header_group = g_object_get_data (G_OBJECT (header_bar), "header-group");
+
+ g_return_val_if_fail (header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR;
+ self->object = G_OBJECT (header_bar);
+
+ g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self);
+
+ g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self);
+ g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_gtk_header_bar (GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *self;
+ gpointer header_group;
+
+ g_return_val_if_fail (GTK_IS_HEADER_BAR (header_bar), NULL);
+
+ header_group = g_object_get_data (G_OBJECT (header_bar), "header-group");
+
+ g_return_val_if_fail (header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR;
+ self->object = G_OBJECT (header_bar);
+
+ g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self);
+
+ g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self);
+ g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_header_group (HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *self;
+ gpointer parent_header_group;
+
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (header_group), NULL);
+
+ parent_header_group = g_object_get_data (G_OBJECT (header_group), "header-group");
+
+ g_return_val_if_fail (parent_header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP;
+ self->object = G_OBJECT (header_group);
+
+ g_object_weak_unref (G_OBJECT (header_group), (GWeakNotify) object_destroyed_cb, self);
+
+ g_signal_connect_swapped (header_group, "update-decoration-layouts", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static void
+hdy_header_group_child_class_init (HdyHeaderGroupChildClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_header_group_child_dispose;
+}
+
+static void
+hdy_header_group_child_init (HdyHeaderGroupChild *self)
+{
+}
+
+static void
+hdy_header_group_child_set_decoration_layout (HdyHeaderGroupChild *self,
+ const gchar *layout)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ hdy_header_bar_set_decoration_layout (HDY_HEADER_BAR (self->object), layout);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (self->object), layout);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ {
+ HdyHeaderGroup *group = HDY_HEADER_GROUP (self->object);
+
+ g_free (group->layout);
+ group->layout = g_strdup (layout);
+
+ update_decoration_layouts (group);
+ }
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static gboolean
+hdy_header_group_child_get_mapped (HdyHeaderGroupChild *self)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ return gtk_widget_get_mapped (GTK_WIDGET (self->object));
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ for (GSList *children = HDY_HEADER_GROUP (self->object)->children;
+ children != NULL;
+ children = children->next)
+ if (hdy_header_group_child_get_mapped (HDY_HEADER_GROUP_CHILD (children->data)))
+ return TRUE;
+
+ return FALSE;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static HdyHeaderGroupChild *
+get_child_for_object (HdyHeaderGroup *self,
+ gpointer object)
+{
+ GSList *children;
+
+ for (children = self->children; children != NULL; children = children->next) {
+ HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data);
+
+ g_assert (child);
+
+ if (child->object == object)
+ return child;
+ }
+
+ return NULL;
+}
+
+static void
+update_decoration_layouts (HdyHeaderGroup *self)
+{
+ GSList *children;
+ GtkSettings *settings;
+ HdyHeaderGroupChild *start_child = NULL, *end_child = NULL;
+ g_autofree gchar *layout = NULL;
+ g_autofree gchar *start_layout = NULL;
+ g_autofree gchar *end_layout = NULL;
+ g_auto(GStrv) ends = NULL;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+
+ children = self->children;
+
+ if (children == NULL)
+ return;
+
+ settings = gtk_settings_get_default ();
+ if (self->layout)
+ layout = g_strdup (self->layout);
+ else
+ g_object_get (G_OBJECT (settings), "gtk-decoration-layout", &layout, NULL);
+ if (layout == NULL)
+ layout = g_strdup (":");
+
+ if (self->decorate_all) {
+ for (; children != NULL; children = children->next)
+ hdy_header_group_child_set_decoration_layout (HDY_HEADER_GROUP_CHILD (children->data), layout);
+
+ return;
+ }
+
+ for (; children != NULL; children = children->next) {
+ HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data);
+
+ hdy_header_group_child_set_decoration_layout (child, ":");
+
+ if (!hdy_header_group_child_get_mapped (child))
+ continue;
+
+ /* The headerbars are in reverse order in the list. */
+ start_child = child;
+ if (end_child == NULL)
+ end_child = child;
+ }
+
+ if (start_child == NULL || end_child == NULL)
+ return;
+
+ if (start_child == end_child) {
+ hdy_header_group_child_set_decoration_layout (start_child, layout);
+
+ return;
+ }
+
+ ends = g_strsplit (layout, ":", 2);
+ if (g_strv_length (ends) >= 2) {
+ start_layout = g_strdup_printf ("%s:", ends[0]);
+ end_layout = g_strdup_printf (":%s", ends[1]);
+ } else {
+ start_layout = g_strdup (":");
+ end_layout = g_strdup (":");
+ }
+ hdy_header_group_child_set_decoration_layout (start_child, start_layout);
+ hdy_header_group_child_set_decoration_layout (end_child, end_layout);
+}
+
+static void
+child_destroyed_cb (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_assert (HDY_IS_HEADER_GROUP (self));
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_assert (g_slist_find (self->children, child) != NULL);
+
+ self->children = g_slist_remove (self->children, child);
+
+ g_object_unref (self);
+}
+
+HdyHeaderGroup *
+hdy_header_group_new (void)
+{
+ return g_object_new (HDY_TYPE_HEADER_GROUP, NULL);
+}
+
+static void
+hdy_header_group_add_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_assert (HDY_IS_HEADER_GROUP (self));
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_assert (g_slist_find (self->children, child) == NULL);
+
+ self->children = g_slist_prepend (self->children, child);
+ g_object_weak_ref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self);
+ g_object_ref (self);
+
+ update_decoration_layouts (self);
+
+ g_object_set_data (G_OBJECT (child), "header-group", self);
+}
+
+/**
+ * hdy_header_group_add_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #HdyHeaderBar to add
+ *
+ * Adds @header_bar to @self.
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_BAR (header_bar));
+ g_return_if_fail (get_child_for_object (self, header_bar) == NULL);
+
+ child = hdy_header_group_child_new_for_header_bar (header_bar);
+ hdy_header_group_add_child (self, child);
+}
+
+/**
+ * hdy_header_group_add_gtk_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #GtkHeaderBar to add
+ *
+ * Adds @header_bar to @self.
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (GTK_IS_HEADER_BAR (header_bar));
+ g_return_if_fail (get_child_for_object (self, header_bar) == NULL);
+
+ child = hdy_header_group_child_new_for_gtk_header_bar (header_bar);
+ hdy_header_group_add_child (self, child);
+}
+
+/**
+ * hdy_header_group_add_header_group:
+ * @self: a #HdyHeaderGroup
+ * @header_group: the #HdyHeaderGroup to add
+ *
+ * Adds @header_group to @self.
+ * When the nested group is no longer referenced elsewhere, it will be removed
+ * from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP (header_group));
+ g_return_if_fail (get_child_for_object (self, header_group) == NULL);
+
+ child = hdy_header_group_child_new_for_header_group (header_group);
+ hdy_header_group_add_child (self, child);
+}
+
+typedef struct {
+ gchar *name;
+ gint line;
+ gint col;
+} ItemData;
+
+static void
+item_data_free (gpointer data)
+{
+ ItemData *item_data = data;
+
+ g_free (item_data->name);
+ g_free (item_data);
+}
+
+typedef struct {
+ GObject *object;
+ GtkBuilder *builder;
+ GSList *items;
+} GSListSubParserData;
+
+static void
+hdy_header_group_dispose (GObject *object)
+{
+ HdyHeaderGroup *self = (HdyHeaderGroup *)object;
+
+ g_slist_free_full (self->children, (GDestroyNotify) g_object_unref);
+ self->children = NULL;
+
+ G_OBJECT_CLASS (hdy_header_group_parent_class)->dispose (object);
+}
+
+static void
+hdy_header_group_finalize (GObject *object)
+{
+ HdyHeaderGroup *self = (HdyHeaderGroup *) object;
+
+ g_free (self->layout);
+
+ G_OBJECT_CLASS (hdy_header_group_parent_class)->finalize (object);
+}
+
+static void
+hdy_header_group_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderGroup *self = HDY_HEADER_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DECORATE_ALL:
+ g_value_set_boolean (value, hdy_header_group_get_decorate_all (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_header_group_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderGroup *self = HDY_HEADER_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DECORATE_ALL:
+ hdy_header_group_set_decorate_all (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+/*< private >
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @parent_name: the name of the expected parent element
+ * @error: return location for an error
+ *
+ * Checks that the parent element of the currently handled
+ * start tag is @parent_name and set @error if it isn't.
+ *
+ * This is intended to be called in start_element vfuncs to
+ * ensure that element nesting is as intended.
+ *
+ * Returns: %TRUE if @parent_name is the parent element
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static gboolean
+_gtk_builder_check_parent (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *parent_name,
+ GError **error)
+{
+ const GSList *stack;
+ gint line, col;
+ const gchar *parent;
+ const gchar *element;
+
+ stack = g_markup_parse_context_get_element_stack (context);
+
+ element = (const gchar *)stack->data;
+ parent = stack->next ? (const gchar *)stack->next->data : "";
+
+ if (g_str_equal (parent_name, parent) ||
+ (g_str_equal (parent_name, "object") && g_str_equal (parent, "template")))
+ return TRUE;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_INVALID_TAG,
+ ".:%d:%d Can't use <%s> here",
+ line, col, element);
+
+ return FALSE;
+}
+
+/*< private >
+ * _gtk_builder_prefix_error:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @error: an error
+ *
+ * Calls g_prefix_error() to prepend a filename:line:column marker
+ * to the given error. The filename is taken from @builder, and
+ * the line and column are obtained by calling
+ * g_markup_parse_context_get_position().
+ *
+ * This is intended to be called on errors returned by
+ * g_markup_collect_attributes() in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_prefix_error (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_prefix_error (error, ".:%d:%d ", line, col);
+}
+
+/*< private >
+ * _gtk_builder_error_unhandled_tag:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @object: name of the object that is being handled
+ * @element_name: name of the element whose start tag is being handled
+ * @error: return location for the error
+ *
+ * Sets @error to a suitable error indicating that an @element_name
+ * tag is not expected in the custom markup for @object.
+ *
+ * This is intended to be called in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *object,
+ const gchar *element_name,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_UNHANDLED_TAG,
+ ".:%d:%d Unsupported tag for %s: <%s>",
+ line, col,
+ object, element_name);
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+header_group_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **names,
+ const gchar **values,
+ gpointer user_data,
+ GError **error)
+{
+ GSListSubParserData *data = (GSListSubParserData*)user_data;
+
+ if (strcmp (element_name, "headerbar") == 0)
+ {
+ const gchar *name;
+ ItemData *item_data;
+
+ if (!_gtk_builder_check_parent (data->builder, context, "headerbars", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_STRING, "name", &name,
+ G_MARKUP_COLLECT_INVALID))
+ {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ item_data = g_new (ItemData, 1);
+ item_data->name = g_strdup (name);
+ g_markup_parse_context_get_position (context, &item_data->line, &item_data->col);
+ data->items = g_slist_prepend (data->items, item_data);
+ }
+ else if (strcmp (element_name, "headerbars") == 0)
+ {
+ if (!_gtk_builder_check_parent (data->builder, context, "object", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_INVALID, NULL, NULL,
+ G_MARKUP_COLLECT_INVALID))
+ _gtk_builder_prefix_error (data->builder, context, error);
+ }
+ else
+ {
+ _gtk_builder_error_unhandled_tag (data->builder, context,
+ "HdyHeaderGroup", element_name,
+ error);
+ }
+}
+
+
+/* This has been copied and modified from gtksizegroup.c. */
+static const GMarkupParser header_group_parser =
+ {
+ header_group_start_element
+ };
+
+/* This has been copied and modified from gtksizegroup.c. */
+static gboolean
+hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *parser_data)
+{
+ GSListSubParserData *data;
+
+ if (child)
+ return FALSE;
+
+ if (strcmp (tagname, "headerbars") == 0)
+ {
+ data = g_slice_new0 (GSListSubParserData);
+ data->items = NULL;
+ data->object = G_OBJECT (buildable);
+ data->builder = builder;
+
+ *parser = header_group_parser;
+ *parser_data = data;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+hdy_header_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data)
+{
+ GSList *l;
+ GSListSubParserData *data;
+
+ if (strcmp (tagname, "headerbars") != 0)
+ return;
+
+ data = (GSListSubParserData*)user_data;
+ data->items = g_slist_reverse (data->items);
+
+ for (l = data->items; l; l = l->next) {
+ ItemData *item_data = l->data;
+ GObject *object = gtk_builder_get_object (builder, item_data->name);
+
+ if (!object)
+ continue;
+
+ if (GTK_IS_HEADER_BAR (object))
+ hdy_header_group_add_gtk_header_bar (HDY_HEADER_GROUP (data->object),
+ GTK_HEADER_BAR (object));
+ else if (HDY_IS_HEADER_BAR (object))
+ hdy_header_group_add_header_bar (HDY_HEADER_GROUP (data->object),
+ HDY_HEADER_BAR (object));
+ else if (HDY_IS_HEADER_GROUP (object))
+ hdy_header_group_add_header_group (HDY_HEADER_GROUP (data->object),
+ HDY_HEADER_GROUP (object));
+ }
+
+ g_slist_free_full (data->items, item_data_free);
+ g_slice_free (GSListSubParserData, data);
+}
+
+static void
+hdy_header_group_class_init (HdyHeaderGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_header_group_dispose;
+ object_class->finalize = hdy_header_group_finalize;
+ object_class->get_property = hdy_header_group_get_property;
+ object_class->set_property = hdy_header_group_set_property;
+
+ /**
+ * HdyHeaderGroup:decorate-all:
+ *
+ * Whether the elements of the group should all receive the full decoration.
+ * This is useful in conjunction with #HdyLeaflet:folded when the leaflet
+ * contains the header bars of the group, as you want them all to display the
+ * complete decoration when the leaflet is folded.
+ *
+ * Since: 1.0
+ */
+ props[PROP_DECORATE_ALL] =
+ g_param_spec_boolean ("decorate-all",
+ _("Decorate all"),
+ _("Whether the elements of the group should all receive the full decoration"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, N_PROPS, props);
+
+ /**
+ * HdyHeaderGroup::update-decoration-layouts:
+ * @self: The #HdyHeaderGroup instance
+ *
+ * This signal is emitted before updating the decoration layouts.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_UPDATE_DECORATION_LAYOUTS] =
+ g_signal_new ("update-decoration-layouts",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+hdy_header_group_init (HdyHeaderGroup *self)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+
+ g_signal_connect_swapped (settings, "notify::gtk-decoration-layout", G_CALLBACK (update_decoration_layouts), self);
+}
+
+static void
+hdy_header_group_buildable_init (GtkBuildableIface *iface)
+{
+ iface->custom_tag_start = hdy_header_group_buildable_custom_tag_start;
+ iface->custom_finished = hdy_header_group_buildable_custom_finished;
+}
+
+/**
+ * hdy_header_group_child_get_header_bar:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #HdyHeaderBar.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #HdyHeaderBar, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+HdyHeaderBar *
+hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR, NULL);
+
+ return HDY_HEADER_BAR (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_gtk_header_bar:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #GtkHeaderBar.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #GtkHeaderBar, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+GtkHeaderBar *
+hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR, NULL);
+
+ return GTK_HEADER_BAR (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_header_group:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #HdyHeaderGroup.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #HdyHeaderGroup, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+HdyHeaderGroup *
+hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP, NULL);
+
+ return HDY_HEADER_GROUP (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_child_type:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child type.
+ *
+ * Returns: the child type.
+ *
+ * Since: 1.0
+ */
+HdyHeaderGroupChildType
+hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR);
+
+ return self->type;
+}
+
+/**
+ * hdy_header_group_get_children:
+ * @self: a #HdyHeaderGroup
+ *
+ * Returns the list of children associated with @self.
+ *
+ * Returns: (element-type HdyHeaderGroupChild) (transfer none): the #GSList of
+ * children. The list is owned by libhandy and should not be modified.
+ *
+ * Since: 1.0
+ */
+GSList *
+hdy_header_group_get_children (HdyHeaderGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), NULL);
+
+ return self->children;
+}
+
+static void
+remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ self->children = g_slist_remove (self->children, child);
+
+ g_object_weak_unref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self);
+
+ g_object_unref (self);
+ g_object_unref (child);
+}
+
+/**
+ * hdy_header_group_remove_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #HdyHeaderBar to remove
+ *
+ * Removes @header_bar from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_BAR (header_bar));
+
+ child = get_child_for_object (self, header_bar);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_gtk_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #GtkHeaderBar to remove
+ *
+ * Removes @header_bar from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (GTK_IS_HEADER_BAR (header_bar));
+
+ child = get_child_for_object (self, header_bar);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_header_group:
+ * @self: a #HdyHeaderGroup
+ * @header_group: the #HdyHeaderGroup to remove
+ *
+ * Removes a nested #HdyHeaderGroup from a #HdyHeaderGroup
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP (header_group));
+
+ child = get_child_for_object (self, header_group);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_child:
+ * @self: a #HdyHeaderGroup
+ * @child: the #HdyHeaderGroupChild to remove
+ *
+ * Removes @child from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_return_if_fail (g_slist_find (self->children, child) != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_set_decorate_all:
+ * @self: a #HdyHeaderGroup
+ * @decorate_all: whether the elements of the group should all receive the full decoration
+ *
+ * Sets whether the elements of the group should all receive the full decoration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_set_decorate_all (HdyHeaderGroup *self,
+ gboolean decorate_all)
+{
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+
+ decorate_all = !!decorate_all;
+
+ if (self->decorate_all == decorate_all)
+ return;
+
+ self->decorate_all = decorate_all;
+
+ update_decoration_layouts (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATE_ALL]);
+}
+
+/**
+ * hdy_header_group_get_decorate_all:
+ * @self: a #HdyHeaderGroup
+ *
+ * Gets whether the elements of the group should all receive the full decoration.
+ *
+ * Returns: %TRUE if the elements of the group should all receive the full
+ * decoration, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_header_group_get_decorate_all (HdyHeaderGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), FALSE);
+
+ return self->decorate_all;
+}
diff --git a/subprojects/libhandy/src/hdy-header-group.h b/subprojects/libhandy/src/hdy-header-group.h
new file mode 100644
index 0000000..dc20a76
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-group.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-header-bar.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_HEADER_GROUP_CHILD (hdy_header_group_child_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyHeaderGroupChild, hdy_header_group_child, HDY, HEADER_GROUP_CHILD, GObject)
+
+#define HDY_TYPE_HEADER_GROUP (hdy_header_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyHeaderGroup, hdy_header_group, HDY, HEADER_GROUP, GObject)
+
+typedef enum {
+ HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR,
+ HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR,
+ HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP,
+} HdyHeaderGroupChildType;
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderBar *hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self);
+HDY_AVAILABLE_IN_ALL
+GtkHeaderBar *hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self);
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroup *hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroupChildType hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroup *hdy_header_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group);
+
+HDY_AVAILABLE_IN_ALL
+GSList *hdy_header_group_get_children (HdyHeaderGroup *self);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_group_get_decorate_all (HdyHeaderGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_set_decorate_all (HdyHeaderGroup *self,
+ gboolean decorate_all);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad-button-private.h b/subprojects/libhandy/src/hdy-keypad-button-private.h
new file mode 100644
index 0000000..723526a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button-private.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_KEYPAD_BUTTON (hdy_keypad_button_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyKeypadButton, hdy_keypad_button, HDY, KEYPAD_BUTTON, GtkButton)
+
+struct _HdyKeypadButtonClass
+{
+ GtkButtonClass parent_class;
+};
+
+GtkWidget *hdy_keypad_button_new (const gchar *symbols);
+gchar hdy_keypad_button_get_digit (HdyKeypadButton *self);
+const gchar *hdy_keypad_button_get_symbols (HdyKeypadButton *self);
+void hdy_keypad_button_show_symbols (HdyKeypadButton *self,
+ gboolean visible);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad-button.c b/subprojects/libhandy/src/hdy-keypad-button.c
new file mode 100644
index 0000000..436555d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button.c
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-keypad-button-private.h"
+
+/**
+ * PRIVATE:hdy-keypad-button
+ * @short_description: A button on a #HdyKeypad keypad
+ * @Title: HdyKeypadButton
+ *
+ * The #HdyKeypadButton widget is a single button on an #HdyKeypad. It
+ * can represent a single symbol (typically a digit) plus an arbitrary
+ * number of symbols that are displayed below it.
+ */
+
+enum {
+ PROP_0,
+ PROP_DIGIT,
+ PROP_SYMBOLS,
+ PROP_SHOW_SYMBOLS,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+struct _HdyKeypadButton
+{
+ GtkButton parent_instance;
+
+ GtkLabel *label, *secondary_label;
+ gchar *symbols;
+};
+
+G_DEFINE_TYPE (HdyKeypadButton, hdy_keypad_button, GTK_TYPE_BUTTON)
+
+static void
+format_label(HdyKeypadButton *self)
+{
+ g_autofree gchar *text = NULL;
+ gchar *secondary_text = NULL;
+
+ if (self->symbols != NULL && *(self->symbols) != '\0') {
+ secondary_text = g_utf8_find_next_char (self->symbols, NULL);
+ text = g_strndup (self->symbols, 1);
+ }
+
+ gtk_label_set_label (self->label, text);
+ gtk_label_set_label (self->secondary_label, secondary_text);
+}
+
+static void
+hdy_keypad_button_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ switch (property_id) {
+ case PROP_SYMBOLS:
+ if (g_strcmp0 (self->symbols, g_value_get_string (value)) != 0) {
+ g_free (self->symbols);
+ self->symbols = g_value_dup_string (value);
+ format_label(self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS]);
+ }
+ break;
+
+ case PROP_SHOW_SYMBOLS:
+ hdy_keypad_button_show_symbols (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_keypad_button_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ switch (property_id) {
+ case PROP_DIGIT:
+ g_value_set_schar (value, hdy_keypad_button_get_digit (self));
+ break;
+
+ case PROP_SYMBOLS:
+ g_value_set_string (value, hdy_keypad_button_get_symbols (self));
+ break;
+
+ case PROP_SHOW_SYMBOLS:
+ g_value_set_boolean (value, gtk_widget_is_visible (GTK_WIDGET (self->secondary_label)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_keypad_button_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class);
+ gint min1, min2, nat1, nat2;
+
+ if (for_size < 0) {
+ widget_class->get_preferred_width (widget, &min1, &nat1);
+ widget_class->get_preferred_height (widget, &min2, &nat2);
+ }
+ else {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ widget_class->get_preferred_width_for_height (widget, for_size, &min1, &nat1);
+ else
+ widget_class->get_preferred_height_for_width (widget, for_size, &min1, &nat1);
+ min2 = nat2 = for_size;
+ }
+
+ if (minimum)
+ *minimum = MAX (min1, min2);
+ if (natural)
+ *natural = MAX (nat1, nat2);
+}
+
+static GtkSizeRequestMode
+hdy_keypad_button_get_request_mode (GtkWidget *widget)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class);
+ gint min1, min2;
+ widget_class->get_preferred_width (widget, &min1, NULL);
+ widget_class->get_preferred_height (widget, &min2, NULL);
+ if (min1 < min2)
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+ else
+ return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+}
+
+static void
+hdy_keypad_button_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_keypad_button_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_keypad_button_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_keypad_button_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_keypad_button_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ *minimum_width = height;
+ *natural_width = height;
+}
+
+static void
+hdy_keypad_button_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ *minimum_height = width;
+ *natural_height = width;
+}
+
+
+static void
+hdy_keypad_button_finalize (GObject *object)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ g_clear_pointer (&self->symbols, g_free);
+ G_OBJECT_CLASS (hdy_keypad_button_parent_class)->finalize (object);
+}
+
+
+static void
+hdy_keypad_button_class_init (HdyKeypadButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->set_property = hdy_keypad_button_set_property;
+ object_class->get_property = hdy_keypad_button_get_property;
+
+ object_class->finalize = hdy_keypad_button_finalize;
+
+ widget_class->get_request_mode = hdy_keypad_button_get_request_mode;
+ widget_class->get_preferred_width = hdy_keypad_button_get_preferred_width;
+ widget_class->get_preferred_height = hdy_keypad_button_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_keypad_button_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_keypad_button_get_preferred_height_for_width;
+
+ props[PROP_DIGIT] =
+ g_param_spec_int ("digit",
+ _("Digit"),
+ _("The keypad digit of the button"),
+ -1, INT_MAX, 0,
+ G_PARAM_READABLE);
+
+ props[PROP_SYMBOLS] =
+ g_param_spec_string ("symbols",
+ _("Symbols"),
+ _("The keypad symbols of the button. The first symbol is used as the digit"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_SHOW_SYMBOLS] =
+ g_param_spec_boolean ("show-symbols",
+ _("Show symbols"),
+ _("Whether the second line of symbols should be shown or not"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-keypad-button.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, label);
+ gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, secondary_label);
+}
+
+static void
+hdy_keypad_button_init (HdyKeypadButton *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->symbols = NULL;
+}
+
+/**
+ * hdy_keypad_button_new:
+ * @symbols: (nullable): the symbols displayed on the #HdyKeypadButton
+ *
+ * Create a new #HdyKeypadButton which displays @symbols,
+ * where the first char is used as the main and the other symbols are shown below
+ *
+ * Returns: the newly created #HdyKeypadButton widget
+ */
+GtkWidget *
+hdy_keypad_button_new (const gchar *symbols)
+{
+ return g_object_new (HDY_TYPE_KEYPAD_BUTTON, "symbols", symbols, NULL);
+}
+
+/**
+ * hdy_keypad_button_get_digit:
+ * @self: a #HdyKeypadButton
+ *
+ * Get the #HdyKeypadButton's digit.
+ *
+ * Returns: the button's digit
+ */
+char
+hdy_keypad_button_get_digit (HdyKeypadButton *self)
+{
+ g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), '\0');
+
+ if (self->symbols == NULL)
+ return ('\0');
+
+ return *(self->symbols);
+}
+
+/**
+ * hdy_keypad_button_get_symbols:
+ * @self: a #HdyKeypadButton
+ *
+ * Get the #HdyKeypadButton's symbols.
+ *
+ * Returns: the button's symbols including the digit.
+ */
+const char*
+hdy_keypad_button_get_symbols (HdyKeypadButton *self)
+{
+ g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), NULL);
+
+ return self->symbols;
+}
+
+/**
+ * hdy_keypad_button_show_symbols:
+ * @self: a #HdyKeypadButton
+ * @visible: whether the second line should be shown or not
+ *
+ * Sets the visibility of the second line of symbols for #HdyKeypadButton
+ *
+ */
+void
+hdy_keypad_button_show_symbols (HdyKeypadButton *self, gboolean visible)
+{
+ gboolean old_visible;
+
+ g_return_if_fail (HDY_IS_KEYPAD_BUTTON (self));
+
+ old_visible = gtk_widget_get_visible (GTK_WIDGET (self->secondary_label));
+
+ if (old_visible != visible) {
+ gtk_widget_set_visible (GTK_WIDGET (self->secondary_label), visible);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_SYMBOLS]);
+ }
+}
diff --git a/subprojects/libhandy/src/hdy-keypad-button.ui b/subprojects/libhandy/src/hdy-keypad-button.ui
new file mode 100644
index 0000000..27a53dd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyKeypadButton" parent="GtkButton">
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="margin">6</property>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="label"></property>
+ <style>
+ <class name="digit"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="secondary_label">
+ <property name="visible">True</property>
+ <property name="no_show_all">True</property>
+ <property name="label"></property>
+ <style>
+ <class name="dim-label"/>
+ <class name="letters"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-keypad.c b/subprojects/libhandy/src/hdy-keypad.c
new file mode 100644
index 0000000..1714d9a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.c
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-keypad.h"
+#include "hdy-keypad-button-private.h"
+
+/**
+ * SECTION:hdy-keypad
+ * @short_description: A keypad for dialing numbers
+ * @Title: HdyKeypad
+ *
+ * The #HdyKeypad widget is a keypad for entering numbers such as phone numbers
+ * or PIN codes.
+ *
+ * # CSS nodes
+ *
+ * #HdyKeypad has a single CSS node with name keypad.
+ *
+ * Since: 0.0.12
+ */
+
+typedef struct
+{
+ GtkEntry *entry;
+ GtkWidget *grid;
+ GtkWidget *label_asterisk;
+ GtkWidget *label_hash;
+ GtkGesture *long_press_zero_gesture;
+ guint16 row_spacing;
+ guint16 column_spacing;
+ gboolean symbols_visible;
+ gboolean letters_visible;
+} HdyKeypadPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyKeypad, hdy_keypad, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_ROW_SPACING,
+ PROP_COLUMN_SPACING,
+ PROP_LETTERS_VISIBLE,
+ PROP_SYMBOLS_VISIBLE,
+ PROP_ENTRY,
+ PROP_END_ACTION,
+ PROP_START_ACTION,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+static void
+symbol_clicked (HdyKeypad *self,
+ gchar symbol)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+ g_autofree gchar *string = g_strdup_printf ("%c", symbol);
+
+ if (!priv->entry)
+ return;
+
+ g_signal_emit_by_name (priv->entry, "insert-at-cursor", string, NULL);
+ /* Set focus to the entry only when it can get focus
+ * https://gitlab.gnome.org/GNOME/gtk/issues/2204
+ */
+ if (gtk_widget_get_can_focus (GTK_WIDGET (priv->entry)))
+ gtk_entry_grab_focus_without_selecting (priv->entry);
+}
+
+
+static void
+button_clicked_cb (HdyKeypad *self,
+ HdyKeypadButton *btn)
+{
+ gchar digit = hdy_keypad_button_get_digit (btn);
+ symbol_clicked (self, digit);
+ g_debug ("Button with number %c was pressed", digit);
+}
+
+
+static void
+asterisk_button_clicked_cb (HdyKeypad *self,
+ GtkWidget *btn)
+{
+ symbol_clicked (self, '*');
+ g_debug ("Button with * was pressed");
+}
+
+
+static void
+hash_button_clicked_cb (HdyKeypad *self,
+ GtkWidget *btn)
+{
+ symbol_clicked (self, '#');
+ g_debug ("Button with # was pressed");
+}
+
+
+static void
+insert_text_cb (HdyKeypad *self,
+ gchar *text,
+ gint length,
+ gpointer position,
+ GtkEditable *editable)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_assert (length == 1);
+
+ if (g_ascii_isdigit (*text))
+ return;
+
+ if (!priv->symbols_visible && strchr ("#*+", *text))
+ return;
+
+ g_signal_stop_emission_by_name (editable, "insert-text");
+}
+
+
+static void
+long_press_zero_cb (HdyKeypad *self,
+ gdouble x,
+ gdouble y,
+ GtkGesture *gesture)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->symbols_visible)
+ return;
+
+ g_debug ("Long press on zero button");
+ symbol_clicked (self, '+');
+ gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+
+static void
+hdy_keypad_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypad *self = HDY_KEYPAD (object);
+
+ switch (property_id) {
+ case PROP_ROW_SPACING:
+ hdy_keypad_set_row_spacing (self, g_value_get_uint (value));
+ break;
+ case PROP_COLUMN_SPACING:
+ hdy_keypad_set_column_spacing (self, g_value_get_uint (value));
+ break;
+ case PROP_LETTERS_VISIBLE:
+ hdy_keypad_set_letters_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_SYMBOLS_VISIBLE:
+ hdy_keypad_set_symbols_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_ENTRY:
+ hdy_keypad_set_entry (self, g_value_get_object (value));
+ break;
+ case PROP_END_ACTION:
+ hdy_keypad_set_end_action (self, g_value_get_object (value));
+ break;
+ case PROP_START_ACTION:
+ hdy_keypad_set_start_action (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+hdy_keypad_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypad *self = HDY_KEYPAD (object);
+
+ switch (property_id) {
+ case PROP_ROW_SPACING:
+ g_value_set_uint (value, hdy_keypad_get_row_spacing (self));
+ break;
+ case PROP_COLUMN_SPACING:
+ g_value_set_uint (value, hdy_keypad_get_column_spacing (self));
+ break;
+ case PROP_LETTERS_VISIBLE:
+ g_value_set_boolean (value, hdy_keypad_get_letters_visible (self));
+ break;
+ case PROP_SYMBOLS_VISIBLE:
+ g_value_set_boolean (value, hdy_keypad_get_symbols_visible (self));
+ break;
+ case PROP_ENTRY:
+ g_value_set_object (value, hdy_keypad_get_entry (self));
+ break;
+ case PROP_START_ACTION:
+ g_value_set_object (value, hdy_keypad_get_start_action (self));
+ break;
+ case PROP_END_ACTION:
+ g_value_set_object (value, hdy_keypad_get_end_action (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+hdy_keypad_finalize (GObject *object)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (HDY_KEYPAD (object));
+
+ if (priv->long_press_zero_gesture != NULL)
+ g_object_unref (priv->long_press_zero_gesture);
+
+ G_OBJECT_CLASS (hdy_keypad_parent_class)->finalize (object);
+}
+
+
+static void
+hdy_keypad_class_init (HdyKeypadClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_keypad_finalize;
+
+ object_class->set_property = hdy_keypad_set_property;
+ object_class->get_property = hdy_keypad_get_property;
+
+ /**
+ * HdyKeypad:row-spacing:
+ *
+ * The amount of space between two consecutive rows.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ROW_SPACING] =
+ g_param_spec_uint ("row-spacing",
+ _("Row spacing"),
+ _("The amount of space between two consecutive rows"),
+ 0, G_MAXINT16, 6,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:column-spacing:
+ *
+ * The amount of space between two consecutive columns.
+ *
+ * Since: 1.0
+ */
+ props[PROP_COLUMN_SPACING] =
+ g_param_spec_uint ("column-spacing",
+ _("Column spacing"),
+ _("The amount of space between two consecutive columns"),
+ 0, G_MAXINT16, 6,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:letters-visible:
+ *
+ * Whether the keypad should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Since: 1.0
+ */
+ props[PROP_LETTERS_VISIBLE] =
+ g_param_spec_boolean ("letters-visible",
+ _("Letters visible"),
+ _("Whether the letters below the digits should be visible"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:symbols-visible:
+ *
+ * Whether the keypad should display the hash and asterisk buttons, and should
+ * display the plus symbol at the bottom of its 0 button.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SYMBOLS_VISIBLE] =
+ g_param_spec_boolean ("symbols-visible",
+ _("Symbols visible"),
+ _("Whether the hash, plus, and asterisk symbols should be visible"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:entry:
+ *
+ * The entry widget connected to the keypad. See hdy_keypad_set_entry() for
+ * details.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ENTRY] =
+ g_param_spec_object ("entry",
+ _("Entry"),
+ _("The entry widget connected to the keypad"),
+ GTK_TYPE_ENTRY,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:end-action:
+ *
+ * The widget for the lower end corner of @self.
+ *
+ * Since: 1.0
+ */
+ props[PROP_END_ACTION] =
+ g_param_spec_object ("end-action",
+ _("End action"),
+ _("The end action widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:start-action:
+ *
+ * The widget for the lower start corner of @self.
+ *
+ * Since: 1.0
+ */
+ props[PROP_START_ACTION] =
+ g_param_spec_object ("start-action",
+ _("Start action"),
+ _("The start action widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-keypad.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, grid);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_asterisk);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_hash);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, long_press_zero_gesture);
+
+ gtk_widget_class_bind_template_callback (widget_class, button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, asterisk_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, hash_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, long_press_zero_cb);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_DIAL);
+ gtk_widget_class_set_css_name (widget_class, "keypad");
+}
+
+
+static void
+hdy_keypad_init (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ priv->row_spacing = 6;
+ priv->column_spacing = 6;
+ priv->letters_visible = TRUE;
+ priv->symbols_visible = TRUE;
+
+ g_type_ensure (HDY_TYPE_KEYPAD_BUTTON);
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+
+/**
+ * hdy_keypad_new:
+ * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible
+ * @letters_visible: whether the letters below the digits should be visible
+ *
+ * Create a new #HdyKeypad widget.
+ *
+ * Returns: the newly created #HdyKeypad widget
+ *
+ * Since: 0.0.12
+ */
+GtkWidget *
+hdy_keypad_new (gboolean symbols_visible,
+ gboolean letters_visible)
+{
+ return g_object_new (HDY_TYPE_KEYPAD,
+ "symbols-visible", symbols_visible,
+ "letters-visible", letters_visible,
+ NULL);
+}
+
+/**
+ * hdy_keypad_set_row_spacing:
+ * @self: a #HdyKeypad
+ * @spacing: the amount of space to insert between rows
+ *
+ * Sets the amount of space between rows of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_row_spacing (HdyKeypad *self,
+ guint spacing)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (spacing <= G_MAXINT16);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->row_spacing == spacing)
+ return;
+
+ priv->row_spacing = spacing;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ROW_SPACING]);
+}
+
+
+/**
+ * hdy_keypad_get_row_spacing:
+ * @self: a #HdyKeypad
+ *
+ * Returns the amount of space between the rows of @self.
+ *
+ * Returns: the row spacing of @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_keypad_get_row_spacing (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), 0);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->row_spacing;
+}
+
+
+/**
+ * hdy_keypad_set_column_spacing:
+ * @self: a #HdyKeypad
+ * @spacing: the amount of space to insert between columns
+ *
+ * Sets the amount of space between columns of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_column_spacing (HdyKeypad *self,
+ guint spacing)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (spacing <= G_MAXINT16);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->column_spacing == spacing)
+ return;
+
+ priv->column_spacing = spacing;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COLUMN_SPACING]);
+}
+
+
+/**
+ * hdy_keypad_get_column_spacing:
+ * @self: a #HdyKeypad
+ *
+ * Returns the amount of space between the columns of @self.
+ *
+ * Returns: the column spacing of @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_keypad_get_column_spacing (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), 0);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->column_spacing;
+}
+
+
+/**
+ * hdy_keypad_set_letters_visible:
+ * @self: a #HdyKeypad
+ * @letters_visible: whether the letters below the digits should be visible
+ *
+ * Sets whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_letters_visible (HdyKeypad *self,
+ gboolean letters_visible)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+
+ letters_visible = !!letters_visible;
+
+ if (priv->letters_visible == letters_visible)
+ return;
+
+ priv->letters_visible = letters_visible;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LETTERS_VISIBLE]);
+}
+
+
+/**
+ * hdy_keypad_get_letters_visible:
+ * @self: a #HdyKeypad
+ *
+ * Returns whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Returns: whether the letters below the digits should be visible
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_keypad_get_letters_visible (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->letters_visible;
+}
+
+
+/**
+ * hdy_keypad_set_symbols_visible:
+ * @self: a #HdyKeypad
+ * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible
+ *
+ * Sets whether @self should display the hash and asterisk buttons, and should
+ * display the plus symbol at the bottom of its 0 button.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_symbols_visible (HdyKeypad *self,
+ gboolean symbols_visible)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+
+ symbols_visible = !!symbols_visible;
+
+ if (priv->symbols_visible == symbols_visible)
+ return;
+
+ priv->symbols_visible = symbols_visible;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS_VISIBLE]);
+}
+
+
+/**
+ * hdy_keypad_get_symbols_visible:
+ * @self: a #HdyKeypad
+ *
+ * Returns whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Returns Whether @self should display the hash and asterisk buttons, and
+ * should display the plus symbol at the bottom of its 0 button.
+ *
+ * Returns: whether the hash, plus, and asterisk symbols should be visible
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_keypad_get_symbols_visible (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->symbols_visible;
+}
+
+
+/**
+ * hdy_keypad_set_entry:
+ * @self: a #HdyKeypad
+ * @entry: (nullable): a #GtkEntry
+ *
+ * Binds @entry to @self and blocks any input which wouldn't be possible to type
+ * with with the keypad.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_keypad_set_entry (HdyKeypad *self,
+ GtkEntry *entry)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (entry == priv->entry)
+ return;
+
+ g_clear_object (&priv->entry);
+
+ if (entry) {
+ priv->entry = g_object_ref (entry);
+
+ gtk_widget_show (GTK_WIDGET (priv->entry));
+ /* Workaround: To keep the osk closed
+ * https://gitlab.gnome.org/GNOME/gtk/merge_requests/978#note_546576 */
+ g_object_set (priv->entry, "im-module", "gtk-im-context-none", NULL);
+
+ g_signal_connect_swapped (G_OBJECT (priv->entry),
+ "insert-text",
+ G_CALLBACK (insert_text_cb),
+ self);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]);
+}
+
+
+/**
+ * hdy_keypad_get_entry:
+ * @self: a #HdyKeypad
+ *
+ * Get the connected entry. See hdy_keypad_set_entry() for details.
+ *
+ * Returns: (transfer none): the set #GtkEntry or %NULL if no widget was set
+ *
+ * Since: 1.0
+ */
+GtkEntry *
+hdy_keypad_get_entry (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->entry;
+}
+
+
+/**
+ * hdy_keypad_set_start_action:
+ * @self: a #HdyKeypad
+ * @start_action: (nullable): the start action widget
+ *
+ * Sets the widget for the lower left corner (or right, in RTL locales) of
+ * @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_start_action (HdyKeypad *self,
+ GtkWidget *start_action)
+{
+ HdyKeypadPrivate *priv;
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (start_action == NULL || GTK_IS_WIDGET (start_action));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3);
+
+ if (old_widget == start_action)
+ return;
+
+ if (old_widget != NULL)
+ gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget);
+
+ if (start_action != NULL)
+ gtk_grid_attach (GTK_GRID (priv->grid), start_action, 0, 3, 1, 1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION]);
+}
+
+
+/**
+ * hdy_keypad_get_start_action:
+ * @self: a #HdyKeypad
+ *
+ * Returns the widget for the lower left corner (or right, in RTL locales) of
+ * @self.
+ *
+ * Returns: (transfer none) (nullable): the start action widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_keypad_get_start_action (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3);
+}
+
+
+/**
+ * hdy_keypad_set_end_action:
+ * @self: a #HdyKeypad
+ * @end_action: (nullable): the end action widget
+ *
+ * Sets the widget for the lower right corner (or left, in RTL locales) of
+ * @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_end_action (HdyKeypad *self,
+ GtkWidget *end_action)
+{
+ HdyKeypadPrivate *priv;
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (end_action == NULL || GTK_IS_WIDGET (end_action));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3);
+
+ if (old_widget == end_action)
+ return;
+
+ if (old_widget != NULL)
+ gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget);
+
+ if (end_action != NULL)
+ gtk_grid_attach (GTK_GRID (priv->grid), end_action, 2, 3, 1, 1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION]);
+}
+
+
+/**
+ * hdy_keypad_get_end_action:
+ * @self: a #HdyKeypad
+ *
+ * Returns the widget for the lower right corner (or left, in RTL locales) of
+ * @self.
+ *
+ * Returns: (transfer none) (nullable): the end action widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_keypad_get_end_action (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3);
+}
diff --git a/subprojects/libhandy/src/hdy-keypad.h b/subprojects/libhandy/src/hdy-keypad.h
new file mode 100644
index 0000000..267feea
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_KEYPAD (hdy_keypad_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyKeypad, hdy_keypad, HDY, KEYPAD, GtkBin)
+
+/**
+ * HdyKeypadClass:
+ * @parent_class: The parent class
+ */
+struct _HdyKeypadClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_new (gboolean symbols_visible,
+ gboolean letters_visible);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_row_spacing (HdyKeypad *self,
+ guint spacing);
+HDY_AVAILABLE_IN_ALL
+guint hdy_keypad_get_row_spacing (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_column_spacing (HdyKeypad *self,
+ guint spacing);
+HDY_AVAILABLE_IN_ALL
+guint hdy_keypad_get_column_spacing (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_letters_visible (HdyKeypad *self,
+ gboolean letters_visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_keypad_get_letters_visible (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_symbols_visible (HdyKeypad *self,
+ gboolean symbols_visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_keypad_get_symbols_visible (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_entry (HdyKeypad *self,
+ GtkEntry *entry);
+HDY_AVAILABLE_IN_ALL
+GtkEntry *hdy_keypad_get_entry (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_start_action (HdyKeypad *self,
+ GtkWidget *start_action);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_get_start_action (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_end_action (HdyKeypad *self,
+ GtkWidget *end_action);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_get_end_action (HdyKeypad *self);
+
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad.ui b/subprojects/libhandy/src/hdy-keypad.ui
new file mode 100644
index 0000000..3f04532
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.ui
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyKeypad" parent="GtkBin">
+ <child>
+ <object class="GtkGrid" id="grid">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">False</property>
+ <property name="vexpand">False</property>
+ <property name="row-spacing" bind-source="HdyKeypad" bind-property="row-spacing" bind-flags="sync-create"/>
+ <property name="column-spacing" bind-source="HdyKeypad" bind-property="column-spacing" bind-flags="sync-create"/>
+ <property name="column_homogeneous">True</property>
+ <property name="column_homogeneous">True</property>
+ <child>
+ <object class="HdyKeypadButton" id="btn_1">
+ <property name="symbols">1</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_2">
+ <property name="symbols">2ABC</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_3">
+ <property name="symbols">3DEF</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_4">
+ <property name="symbols">4GHI</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_5">
+ <property name="symbols">5JKL</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_6">
+ <property name="symbols">6MNO</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_7">
+ <property name="symbols">7PQRS</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_8">
+ <property name="symbols">8TUV</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_9">
+ <property name="symbols">9WXYZ</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_asterisk">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="asterisk_button_clicked_cb" swapped="true"/>
+ <child>
+ <object class="GtkLabel" id="label_asterisk">
+ <property name="visible">True</property>
+ <property name="label">∗</property>
+ <style>
+ <class name="symbol"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_0">
+ <property name="symbols">0+</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_hash">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="hash_button_clicked_cb" swapped="true"/>
+ <child>
+ <object class="GtkLabel" id="label_hash">
+ <property name="visible">True</property>
+ <property name="label">#</property>
+ <style>
+ <class name="symbol"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkGestureLongPress" id="long_press_zero_gesture">
+ <property name="widget">btn_0</property>
+ <signal name="pressed" handler="long_press_zero_cb" object="HdyKeypad" swapped="true"/>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-leaflet.c b/subprojects/libhandy/src/hdy-leaflet.c
new file mode 100644
index 0000000..8c1ba2a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-leaflet.c
@@ -0,0 +1,1209 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-leaflet.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-leaflet
+ * @short_description: An adaptive container acting like a box or a stack.
+ * @Title: HdyLeaflet
+ *
+ * The #HdyLeaflet widget can display its children like a #GtkBox does or
+ * like a #GtkStack does, adapting to size changes by switching between
+ * the two modes.
+ *
+ * When there is enough space the children are displayed side by side, otherwise
+ * only one is displayed and the leaflet is said to be “folded”.
+ * The threshold is dictated by the preferred minimum sizes of the children.
+ * When a leaflet is folded, the children can be navigated using swipe gestures.
+ *
+ * The “over” and “under” stack the children one on top of the other, while the
+ * “slide” transition puts the children side by side. While navigating to a
+ * child on the side or below can be performed by swiping the current child
+ * away, navigating to an upper child requires dragging it from the edge where
+ * it resides. This doesn't affect non-dragging swipes.
+ *
+ * The “over” and “under” transitions can draw their shadow on top of the
+ * window's transparent areas, like the rounded corners. This is a side-effect
+ * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated
+ * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn
+ * beyond the rounded corners.
+ *
+ * # CSS nodes
+ *
+ * #HdyLeaflet has a single CSS node with name leaflet. The node will get the
+ * style classes .folded when it is folded, .unfolded when it's not, or none if
+ * it didn't compute its fold yet.
+ */
+
+/**
+ * HdyLeafletTransitionType:
+ * @HDY_LEAFLET_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_LEAFLET_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_LEAFLET_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between modes and
+ * children in a #HdyLeaflet widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 0.0.12
+ */
+
+enum {
+ PROP_0,
+ PROP_FOLDED,
+ PROP_HHOMOGENEOUS_FOLDED,
+ PROP_VHOMOGENEOUS_FOLDED,
+ PROP_HHOMOGENEOUS_UNFOLDED,
+ PROP_VHOMOGENEOUS_UNFOLDED,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_MODE_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+
+ /* orientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ORIENTATION,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_NAME,
+ CHILD_PROP_NAVIGATABLE,
+ LAST_CHILD_PROP,
+};
+
+typedef struct
+{
+ HdyStackableBox *box;
+} HdyLeafletPrivate;
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+static void hdy_leaflet_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyLeaflet, hdy_leaflet, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyLeaflet)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_leaflet_swipeable_init))
+
+#define HDY_GET_HELPER(obj) (((HdyLeafletPrivate *) hdy_leaflet_get_instance_private (HDY_LEAFLET (obj)))->box)
+
+/**
+ * hdy_leaflet_get_folded:
+ * @self: a #HdyLeaflet
+ *
+ * Gets whether @self is folded.
+ *
+ * Returns: whether @self is folded.
+ */
+gboolean
+hdy_leaflet_get_folded (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_folded (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_homogeneous:
+ * @self: a #HdyLeaflet
+ * @folded: the fold
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyLeaflet to be homogeneous or not for the given fold and orientation.
+ * If it is homogeneous, the #HdyLeaflet will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't and it is folded, the leaflet may change width or height
+ * when a different child becomes visible.
+ */
+void
+hdy_leaflet_set_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), folded, orientation, homogeneous);
+}
+
+/**
+ * hdy_leaflet_get_homogeneous:
+ * @self: a #HdyLeaflet
+ * @folded: the fold
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given fold and orientation.
+ * See hdy_leaflet_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given fold and orientation.
+ */
+gboolean
+hdy_leaflet_get_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), folded, orientation);
+}
+
+/**
+ * hdy_leaflet_get_transition_type:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the type of animation that will be used
+ * for transitions between modes and children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 0.0.12
+ */
+HdyLeafletTransitionType
+hdy_leaflet_get_transition_type (HdyLeaflet *self)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), HDY_LEAFLET_TRANSITION_TYPE_OVER);
+
+ type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self));
+
+ switch (type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return HDY_LEAFLET_TRANSITION_TYPE_OVER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return HDY_LEAFLET_TRANSITION_TYPE_UNDER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ return HDY_LEAFLET_TRANSITION_TYPE_SLIDE;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+/**
+ * hdy_leaflet_set_transition_type:
+ * @self: a #HdyLeaflet
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between modes
+ * and children in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about to
+ * become current.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_transition_type (HdyLeaflet *self,
+ HdyLeafletTransitionType transition)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+ g_return_if_fail (transition <= HDY_LEAFLET_TRANSITION_TYPE_SLIDE);
+
+ switch (transition) {
+ case HDY_LEAFLET_TRANSITION_TYPE_OVER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ break;
+
+ case HDY_LEAFLET_TRANSITION_TYPE_UNDER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ break;
+
+ case HDY_LEAFLET_TRANSITION_TYPE_SLIDE:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE;
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type);
+}
+
+/**
+ * hdy_leaflet_get_mode_transition_duration:
+ * @self: a #HdyLeaflet
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between modes in @self will take.
+ *
+ * Returns: the mode transition duration
+ */
+guint
+hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), 0);
+
+ return hdy_stackable_box_get_mode_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_mode_transition_duration:
+ * @self: a #HdyLeaflet
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between modes in @self
+ * will take.
+ */
+void
+hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_mode_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_leaflet_get_child_transition_duration:
+ * @self: a #HdyLeaflet
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ */
+guint
+hdy_leaflet_get_child_transition_duration (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), 0);
+
+ return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_child_transition_duration:
+ * @self: a #HdyLeaflet
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ */
+void
+hdy_leaflet_set_child_transition_duration (HdyLeaflet *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_leaflet_get_visible_child:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ */
+GtkWidget *
+hdy_leaflet_get_visible_child (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_visible_child:
+ * @self: a #HdyLeaflet
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyLeaflet:transition-type and HdyLeaflet:child-transition-duration. The
+ * transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ */
+void
+hdy_leaflet_set_visible_child (HdyLeaflet *self,
+ GtkWidget *visible_child)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child);
+}
+
+/**
+ * hdy_leaflet_get_visible_child_name:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ */
+const gchar *
+hdy_leaflet_get_visible_child_name (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_visible_child_name:
+ * @self: a #HdyLeaflet
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_leaflet_set_visible_child() for more details.
+ */
+void
+hdy_leaflet_set_visible_child_name (HdyLeaflet *self,
+ const gchar *name)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name);
+}
+
+/**
+ * hdy_leaflet_get_child_transition_running:
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ */
+gboolean
+hdy_leaflet_get_child_transition_running (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_interpolate_size:
+ * @self: a #HdyLeaflet
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyLeaflet:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ */
+void
+hdy_leaflet_set_interpolate_size (HdyLeaflet *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size);
+}
+
+/**
+ * hdy_leaflet_get_interpolate_size:
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ */
+gboolean
+hdy_leaflet_get_interpolate_size (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_can_swipe_back:
+ * @self: a #HdyLeaflet
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_can_swipe_back (HdyLeaflet *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back);
+}
+
+/**
+ * hdy_leaflet_get_can_swipe_back
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 0.0.12
+ */
+gboolean
+hdy_leaflet_get_can_swipe_back (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_can_swipe_forward:
+ * @self: a #HdyLeaflet
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward);
+}
+
+/**
+ * hdy_leaflet_get_can_swipe_forward
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 0.0.12
+ */
+gboolean
+hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_get_adjacent_child
+ * @self: a #HdyLeaflet
+ * @direction: the direction
+ *
+ * Gets the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, or %NULL if it doesn't exist. This will be the same
+ * widget hdy_leaflet_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_leaflet_get_adjacent_child (HdyLeaflet *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_leaflet_navigate
+ * @self: a #HdyLeaflet
+ * @direction: the direction
+ *
+ * Switches to the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, similar to performing a swipe gesture to go in
+ * @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_leaflet_navigate (HdyLeaflet *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_leaflet_get_child_by_name:
+ * @self: a #HdyLeaflet
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_leaflet_get_child_by_name (HdyLeaflet *self,
+ const gchar *name)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name);
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_leaflet_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ hdy_stackable_box_measure (HDY_GET_HELPER (widget),
+ orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_leaflet_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_leaflet_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation);
+}
+
+static gboolean
+hdy_leaflet_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr);
+}
+
+static void
+hdy_leaflet_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction);
+}
+
+static void
+hdy_leaflet_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_add (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_leaflet_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_remove (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_leaflet_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data);
+}
+
+static void
+hdy_leaflet_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+
+ switch (prop_id) {
+ case PROP_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_folded (self));
+ break;
+ case PROP_HHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_leaflet_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_leaflet_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_leaflet_get_transition_type (self));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_leaflet_get_mode_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_leaflet_get_child_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_leaflet_get_child_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_leaflet_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_leaflet_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_leaflet_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_leaflet_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS_FOLDED:
+ hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_leaflet_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_leaflet_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_leaflet_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ hdy_leaflet_set_mode_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ hdy_leaflet_set_child_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_leaflet_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_leaflet_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_leaflet_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_leaflet_finalize (GObject *object)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+ HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self);
+
+ g_clear_object (&priv->box);
+
+ G_OBJECT_CLASS (hdy_leaflet_parent_class)->finalize (object);
+}
+
+static void
+hdy_leaflet_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget));
+ break;
+
+ case CHILD_PROP_NAVIGATABLE:
+ g_value_set_boolean (value, hdy_stackable_box_get_child_navigatable (HDY_GET_HELPER (container), widget));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_leaflet_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ case CHILD_PROP_NAVIGATABLE:
+ hdy_stackable_box_set_child_navigatable (HDY_GET_HELPER (container), widget, g_value_get_boolean (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_leaflet_realize (GtkWidget *widget)
+{
+ hdy_stackable_box_realize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_unrealize (GtkWidget *widget)
+{
+ hdy_stackable_box_unrealize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_map (GtkWidget *widget)
+{
+ hdy_stackable_box_map (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_unmap (GtkWidget *widget)
+{
+ hdy_stackable_box_unmap (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration);
+}
+
+static HdySwipeTracker *
+hdy_leaflet_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_leaflet_get_distance (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble *
+hdy_leaflet_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points);
+}
+
+static gdouble
+hdy_leaflet_get_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_leaflet_get_cancel_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable));
+}
+
+static void
+hdy_leaflet_get_swipe_area (HdySwipeable *swipeable,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect);
+}
+
+static void
+hdy_leaflet_class_init (HdyLeafletClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = (GtkWidgetClass*) klass;
+ GtkContainerClass *container_class = (GtkContainerClass*) klass;
+
+ object_class->get_property = hdy_leaflet_get_property;
+ object_class->set_property = hdy_leaflet_set_property;
+ object_class->finalize = hdy_leaflet_finalize;
+
+ widget_class->realize = hdy_leaflet_realize;
+ widget_class->unrealize = hdy_leaflet_unrealize;
+ widget_class->map = hdy_leaflet_map;
+ widget_class->unmap = hdy_leaflet_unmap;
+ widget_class->get_preferred_width = hdy_leaflet_get_preferred_width;
+ widget_class->get_preferred_height = hdy_leaflet_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_leaflet_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_leaflet_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_leaflet_size_allocate;
+ widget_class->draw = hdy_leaflet_draw;
+ widget_class->direction_changed = hdy_leaflet_direction_changed;
+
+ container_class->add = hdy_leaflet_add;
+ container_class->remove = hdy_leaflet_remove;
+ container_class->forall = hdy_leaflet_forall;
+ container_class->set_child_property = hdy_leaflet_set_child_property;
+ container_class->get_child_property = hdy_leaflet_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyLeaflet:folded:
+ *
+ * %TRUE if the leaflet is folded.
+ *
+ * The leaflet will be folded if the size allocated to it is smaller than the
+ * sum of the natural size of its children, it will be unfolded otherwise.
+ */
+ props[PROP_FOLDED] =
+ g_param_spec_boolean ("folded",
+ _("Folded"),
+ _("Whether the widget is folded"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:hhomogeneous_folded:
+ *
+ * %TRUE if the leaflet allocates the same width for all children when folded.
+ */
+ props[PROP_HHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("hhomogeneous-folded",
+ _("Horizontally homogeneous folded"),
+ _("Horizontally homogeneous sizing when the leaflet is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:vhomogeneous_folded:
+ *
+ * %TRUE if the leaflet allocates the same height for all children when folded.
+ */
+ props[PROP_VHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("vhomogeneous-folded",
+ _("Vertically homogeneous folded"),
+ _("Vertically homogeneous sizing when the leaflet is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:hhomogeneous_unfolded:
+ *
+ * %TRUE if the leaflet allocates the same width for all children when unfolded.
+ */
+ props[PROP_HHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("hhomogeneous-unfolded",
+ _("Box horizontally homogeneous"),
+ _("Horizontally homogeneous sizing when the leaflet is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:vhomogeneous_unfolded:
+ *
+ * %TRUE if the leaflet allocates the same height for all children when unfolded.
+ */
+ props[PROP_VHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("vhomogeneous-unfolded",
+ _("Box vertically homogeneous"),
+ _("Vertically homogeneous sizing when the leaflet is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible when the leaflet is folded"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible when the children are stacked"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:transition-type:
+ *
+ * The type of animation that will be used for transitions between modes and
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about
+ * to become current.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between modes and children"),
+ HDY_TYPE_LEAFLET_TRANSITION_TYPE, HDY_LEAFLET_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_MODE_TRANSITION_DURATION] =
+ g_param_spec_uint ("mode-transition-duration",
+ _("Mode transition duration"),
+ _("The mode transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 250,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_DURATION] =
+ g_param_spec_uint ("child-transition-duration",
+ _("Child transition duration"),
+ _("The child transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("child-transition-running",
+ _("Child transition running"),
+ _("Whether or not the child transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:can-swipe-back:
+ *
+ * Whether or not the leaflet allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:can-swipe-forward:
+ *
+ * Whether or not the leaflet allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_NAME] =
+ g_param_spec_string ("name",
+ _("Name"),
+ _("The name of the child page"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyLeaflet:navigatable:
+ *
+ * Whether the child can be navigated to when folded.
+ * If %FALSE, the child will be ignored by hdy_leaflet_get_adjacent_child(),
+ * hdy_leaflet_navigate(), and swipe gestures.
+ *
+ * This can be used used to prevent switching to widgets like separators.
+ *
+ * Since: 1.0
+ */
+ child_props[CHILD_PROP_NAVIGATABLE] =
+ g_param_spec_boolean ("navigatable",
+ _("Navigatable"),
+ _("Whether the child can be navigated to"),
+ TRUE,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "leaflet");
+}
+
+GtkWidget *
+hdy_leaflet_new (void)
+{
+ return g_object_new (HDY_TYPE_LEAFLET, NULL);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (HdyLeaflet *self) { \
+ g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \
+}
+
+NOTIFY (notify_folded_cb, PROP_FOLDED);
+NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS_FOLDED);
+NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS_FOLDED);
+NOTIFY (notify_hhomogeneous_unfolded_cb, PROP_HHOMOGENEOUS_UNFOLDED);
+NOTIFY (notify_vhomogeneous_unfolded_cb, PROP_VHOMOGENEOUS_UNFOLDED);
+NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD);
+NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME);
+NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE);
+NOTIFY (notify_mode_transition_duration_cb, PROP_MODE_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_duration_cb, PROP_CHILD_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_running_cb, PROP_CHILD_TRANSITION_RUNNING);
+NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE);
+NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK);
+NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD);
+
+static void
+notify_orientation_cb (HdyLeaflet *self)
+{
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_leaflet_init (HdyLeaflet *self)
+{
+ HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self);
+
+ priv->box = hdy_stackable_box_new (GTK_CONTAINER (self),
+ GTK_CONTAINER_CLASS (hdy_leaflet_parent_class),
+ TRUE);
+
+ g_signal_connect_object (priv->box, "notify::folded", G_CALLBACK (notify_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-unfolded", G_CALLBACK (notify_hhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-unfolded", G_CALLBACK (notify_vhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::mode-transition-duration", G_CALLBACK (notify_mode_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_leaflet_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_leaflet_switch_child;
+ iface->get_swipe_tracker = hdy_leaflet_get_swipe_tracker;
+ iface->get_distance = hdy_leaflet_get_distance;
+ iface->get_snap_points = hdy_leaflet_get_snap_points;
+ iface->get_progress = hdy_leaflet_get_progress;
+ iface->get_cancel_progress = hdy_leaflet_get_cancel_progress;
+ iface->get_swipe_area = hdy_leaflet_get_swipe_area;
+}
diff --git a/subprojects/libhandy/src/hdy-leaflet.h b/subprojects/libhandy/src/hdy-leaflet.h
new file mode 100644
index 0000000..4d9324d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-leaflet.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+#include "hdy-navigation-direction.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_LEAFLET (hdy_leaflet_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyLeaflet, hdy_leaflet, HDY, LEAFLET, GtkContainer)
+
+typedef enum {
+ HDY_LEAFLET_TRANSITION_TYPE_OVER,
+ HDY_LEAFLET_TRANSITION_TYPE_UNDER,
+ HDY_LEAFLET_TRANSITION_TYPE_SLIDE,
+} HdyLeafletTransitionType;
+
+/**
+ * HdyLeafletClass
+ * @parent_class: The parent class
+ */
+struct _HdyLeafletClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_new (void);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_folded (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_visible_child (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_visible_child (HdyLeaflet *self,
+ GtkWidget *visible_child);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_leaflet_get_visible_child_name (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_visible_child_name (HdyLeaflet *self,
+ const gchar *name);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HDY_AVAILABLE_IN_ALL
+HdyLeafletTransitionType hdy_leaflet_get_transition_type (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_transition_type (HdyLeaflet *self,
+ HdyLeafletTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_leaflet_get_child_transition_duration (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_child_transition_duration (HdyLeaflet *self,
+ guint duration);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_child_transition_running (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_interpolate_size (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_interpolate_size (HdyLeaflet *self,
+ gboolean interpolate_size);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_can_swipe_back (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_can_swipe_back (HdyLeaflet *self,
+ gboolean can_swipe_back);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self,
+ gboolean can_swipe_forward);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_adjacent_child (HdyLeaflet *self,
+ HdyNavigationDirection direction);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_navigate (HdyLeaflet *self,
+ HdyNavigationDirection direction);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_child_by_name (HdyLeaflet *self,
+ const gchar *name);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-main-private.h b/subprojects/libhandy/src/hdy-main-private.h
new file mode 100644
index 0000000..3ad6ad1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main-private.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-main.h"
+
+G_BEGIN_DECLS
+
+/* Initializes the public GObject types, which is needed to ensure they are
+ * discoverable, for example so they can easily be used with GtkBuilder.
+ *
+ * The function is implemented in hdy-public-types.c which is generated at
+ * compile time by gen-public-types.sh
+ */
+void hdy_init_public_types (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-main.c b/subprojects/libhandy/src/hdy-main.c
new file mode 100644
index 0000000..6c5df7b
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main.c
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2018-2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#include "config.h"
+#include "hdy-main-private.h"
+#include <gio/gio.h>
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+
+static gint hdy_initialized = FALSE;
+
+/**
+ * SECTION:hdy-main
+ * @short_description: Library initialization.
+ * @Title: hdy-main
+ *
+ * Before using the Handy library you should initialize it by calling the
+ * hdy_init() function.
+ * This makes sure translations, types, themes, and icons for the Handy library
+ * are set up properly.
+ */
+
+/* The style provider priority to use for libhandy widgets custom styling. It is
+ * higher than themes and settings, allowing to override theme defaults, but
+ * lower than applications and user provided styles, so application developers
+ * can nonetheless apply custom styling on top of it. */
+#define HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE (GTK_STYLE_PROVIDER_PRIORITY_SETTINGS + 1)
+
+#define HDY_THEMES_PATH "/sm/puri/handy/themes/"
+
+static inline gboolean
+hdy_resource_exists (const gchar *resource_path)
+{
+ return g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL);
+}
+
+static gchar *
+hdy_themes_get_theme_name (gboolean *prefer_dark_theme)
+{
+ gchar *theme_name = NULL;
+ gchar *p;
+
+ g_assert (prefer_dark_theme);
+
+ theme_name = g_strdup (g_getenv ("GTK_THEME"));
+
+ if (theme_name == NULL) {
+ g_object_get (gtk_settings_get_default (),
+ "gtk-theme-name", &theme_name,
+ "gtk-application-prefer-dark-theme", prefer_dark_theme,
+ NULL);
+
+ return theme_name;
+ }
+
+ /* Theme variants are specified with the syntax
+ * "<theme>:<variant>" e.g. "Adwaita:dark" */
+ if (NULL != (p = strrchr (theme_name, ':'))) {
+ *p = '\0';
+ p++;
+ *prefer_dark_theme = g_strcmp0 (p, "dark") == 0;
+ }
+
+ return theme_name;
+}
+
+static void
+hdy_themes_update (GtkCssProvider *css_provider)
+{
+ g_autofree gchar *theme_name = NULL;
+ g_autofree gchar *resource_path = NULL;
+ gboolean prefer_dark_theme = FALSE;
+
+ g_assert (GTK_IS_CSS_PROVIDER (css_provider));
+
+ theme_name = hdy_themes_get_theme_name (&prefer_dark_theme);
+
+ /* First check with full path to theme+variant */
+ resource_path = g_strdup_printf (HDY_THEMES_PATH"%s%s.css",
+ theme_name, prefer_dark_theme ? "-dark" : "");
+
+ if (!hdy_resource_exists (resource_path)) {
+ /* Now try without the theme variant */
+ g_free (resource_path);
+ resource_path = g_strdup_printf (HDY_THEMES_PATH"%s.css", theme_name);
+
+ if (!hdy_resource_exists (resource_path)) {
+ /* Now fallback to shared styling */
+ g_free (resource_path);
+ resource_path = g_strdup (HDY_THEMES_PATH"shared.css");
+
+ g_assert (hdy_resource_exists (resource_path));
+ }
+ }
+
+ gtk_css_provider_load_from_resource (css_provider, resource_path);
+}
+
+static void
+load_fallback_style (void)
+{
+ g_autoptr (GtkCssProvider) css_provider = NULL;
+
+ css_provider = gtk_css_provider_new ();
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_FALLBACK);
+
+ gtk_css_provider_load_from_resource (css_provider, HDY_THEMES_PATH"fallback.css");
+}
+
+/**
+ * hdy_style_init:
+ *
+ * Initializes the style classes. This must be called once GTK has been
+ * initialized.
+ *
+ * Since: 1.0
+ */
+static void
+hdy_style_init (void)
+{
+ static volatile gsize guard = 0;
+ g_autoptr (GtkCssProvider) css_provider = NULL;
+ GtkSettings *settings;
+
+ if (!g_once_init_enter (&guard))
+ return;
+
+ css_provider = gtk_css_provider_new ();
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (css_provider),
+ HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE);
+
+ settings = gtk_settings_get_default ();
+ g_signal_connect_swapped (settings,
+ "notify::gtk-theme-name",
+ G_CALLBACK (hdy_themes_update),
+ css_provider);
+ g_signal_connect_swapped (settings,
+ "notify::gtk-application-prefer-dark-theme",
+ G_CALLBACK (hdy_themes_update),
+ css_provider);
+
+ hdy_themes_update (css_provider);
+
+ load_fallback_style ();
+
+ g_once_init_leave (&guard, 1);
+}
+
+/**
+ * hdy_icons_init:
+ *
+ * Initializes the embedded icons. This must be called once GTK has been
+ * initialized.
+ *
+ * Since: 1.0
+ */
+static void
+hdy_icons_init (void)
+{
+ static volatile gsize guard = 0;
+
+ if (!g_once_init_enter (&guard))
+ return;
+
+ gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (),
+ "/sm/puri/handy/icons");
+
+ g_once_init_leave (&guard, 1);
+}
+
+/**
+ * hdy_init:
+ *
+ * Call this function just after initializing GTK, if you are using
+ * #GtkApplication it means it must be called when the #GApplication::startup
+ * signal is emitted. If libhandy has already been initialized, the function
+ * will simply return.
+ *
+ * This makes sure translations, types, themes, and icons for the Handy library
+ * are set up properly.
+ */
+void
+hdy_init (void)
+{
+ if (hdy_initialized)
+ return;
+
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ hdy_init_public_types ();
+
+ hdy_style_init ();
+ hdy_icons_init ();
+
+ hdy_initialized = TRUE;
+}
diff --git a/subprojects/libhandy/src/hdy-main.h b/subprojects/libhandy/src/hdy-main.h
new file mode 100644
index 0000000..f960a69
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+HDY_AVAILABLE_IN_ALL
+void hdy_init (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-navigation-direction.c b/subprojects/libhandy/src/hdy-navigation-direction.c
new file mode 100644
index 0000000..b4a2d23
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-navigation-direction.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-navigation-direction.h"
+
+/**
+ * SECTION:hdy-navigation-direction
+ * @short_description: Swipe navigation directions.
+ * @title: HdyNavigationDirection
+ * @See_also: #HdyDeck, #HdyLeaflet
+ */
+
+/**
+ * HdyNavigationDirection:
+ * @HDY_NAVIGATION_DIRECTION_BACK: Corresponds to start or top, depending on orientation and text direction
+ * @HDY_NAVIGATION_DIRECTION_FORWARD: Corresponds to end or bottom, depending on orientation and text direction
+ *
+ * Represents direction of a swipe navigation gesture in #HdyDeck and
+ * #HdyLeaflet.
+ *
+ * Since: 1.0
+ */
diff --git a/subprojects/libhandy/src/hdy-navigation-direction.h b/subprojects/libhandy/src/hdy-navigation-direction.h
new file mode 100644
index 0000000..ea63ef5
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-navigation-direction.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ HDY_NAVIGATION_DIRECTION_BACK,
+ HDY_NAVIGATION_DIRECTION_FORWARD,
+} HdyNavigationDirection;
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-nothing-private.h b/subprojects/libhandy/src/hdy-nothing-private.h
new file mode 100644
index 0000000..19d35c9
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-nothing-private.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_NOTHING (hdy_nothing_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyNothing, hdy_nothing, HDY, NOTHING, GtkWidget)
+
+GtkWidget *hdy_nothing_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-nothing.c b/subprojects/libhandy/src/hdy-nothing.c
new file mode 100644
index 0000000..036537a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-nothing.c
@@ -0,0 +1,47 @@
+#include "hdy-nothing-private.h"
+
+/**
+ * PRIVATE:hdy-nothing
+ * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow
+ * @title: HdyNothing
+ * @See_also: #HdyApplicationWindow, #HdyWindow, #HdyWindowMixin
+ * @stability: Private
+ *
+ * The HdyNothing widget does nothing. It's used as the titlebar for
+ * #HdyWindow and #HdyApplicationWindow.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyNothing
+{
+ GtkWidget parent_instance;
+};
+
+G_DEFINE_TYPE (HdyNothing, hdy_nothing, GTK_TYPE_WIDGET)
+
+static void
+hdy_nothing_class_init (HdyNothingClass *klass)
+{
+}
+
+static void
+hdy_nothing_init (HdyNothing *self)
+{
+}
+
+/**
+ * hdy_nothing_new:
+ *
+ * Creates a new #HdyNothing.
+ *
+ * Returns: (transfer full): a newly created #HdyNothing
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_nothing_new (void)
+{
+ return g_object_new (HDY_TYPE_NOTHING, NULL);
+}
+
diff --git a/subprojects/libhandy/src/hdy-preferences-group-private.h b/subprojects/libhandy/src/hdy-preferences-group-private.h
new file mode 100644
index 0000000..731e2fa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group-private.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include "hdy-preferences-group.h"
+
+G_BEGIN_DECLS
+
+void hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self,
+ GListStore *model);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-group.c b/subprojects/libhandy/src/hdy-preferences-group.c
new file mode 100644
index 0000000..8000627
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.c
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-group-private.h"
+
+#include "hdy-preferences-row.h"
+
+/**
+ * SECTION:hdy-preferences-group
+ * @short_description: A group gathering preferences rows.
+ * @Title: HdyPreferencesGroup
+ *
+ * A #HdyPreferencesGroup represents a group or tightly related preferences,
+ * which in turn are represented by HdyPreferencesRow.
+ *
+ * To summarize the role of the preferences it gathers, a group can have both a
+ * title and a description. The title will be used by #HdyPreferencesWindow to
+ * let the user look for a preference.
+ *
+ * # CSS nodes
+ *
+ * #HdyPreferencesGroup has a single CSS node with name preferencesgroup.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkLabel *description;
+ GtkListBox *listbox;
+ GtkBox *listbox_box;
+ GtkLabel *title;
+} HdyPreferencesGroupPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesGroup, hdy_preferences_group, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_DESCRIPTION,
+ PROP_TITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_title_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ /* Show the listbox only if it has children to avoid having showing the
+ * listbox as an empty frame, parasiting the look of non-GtkListBoxRow
+ * children.
+ */
+ gtk_widget_set_visible (GTK_WIDGET (priv->title),
+ gtk_label_get_text (priv->title) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->title), "") != 0);
+}
+
+static void
+update_description_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->description),
+ gtk_label_get_text (priv->description) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->description), "") != 0);
+}
+
+static void
+update_listbox_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+ g_autoptr(GList) children = NULL;
+
+ /* We must wait until listob has been built and added. */
+ if (priv->listbox == NULL)
+ return;
+
+ children = gtk_container_get_children (GTK_CONTAINER (priv->listbox));
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->listbox), children != NULL);
+}
+
+typedef struct {
+ HdyPreferencesGroup *group;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (data->group);
+
+ if (widget != (GtkWidget *) priv->listbox)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_preferences_group_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.group = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ if (priv->listbox)
+ GTK_CONTAINER_GET_CLASS (priv->listbox)->forall (GTK_CONTAINER (priv->listbox), include_internals, callback, callback_data);
+ if (priv->listbox_box)
+ GTK_CONTAINER_GET_CLASS (priv->listbox_box)->forall (GTK_CONTAINER (priv->listbox_box), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_preferences_group_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, hdy_preferences_group_get_description (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_group_get_title (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_group_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ hdy_preferences_group_set_description (self, g_value_get_string (value));
+ break;
+ case PROP_TITLE:
+ hdy_preferences_group_set_title (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_group_dispose (GObject *object)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ /*
+ * Since we overload forall(), the inherited destroy() won't work as normal.
+ * Remove internal widgets ourself.
+ */
+ g_clear_pointer ((GtkWidget **) &priv->description, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->listbox, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->listbox_box, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->title, gtk_widget_destroy);
+
+ G_OBJECT_CLASS (hdy_preferences_group_parent_class)->dispose (object);
+}
+
+static void
+hdy_preferences_group_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ if (priv->title == NULL || priv->description == NULL || priv->listbox_box == NULL) {
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->add (container, child);
+
+ return;
+ }
+
+ if (HDY_IS_PREFERENCES_ROW (child))
+ gtk_container_add (GTK_CONTAINER (priv->listbox), child);
+ else
+ gtk_container_add (GTK_CONTAINER (priv->listbox_box), child);
+}
+
+static void
+hdy_preferences_group_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->box))
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->remove (container, child);
+ else if (HDY_IS_PREFERENCES_ROW (child))
+ gtk_container_remove (GTK_CONTAINER (priv->listbox), child);
+ else if (child != GTK_WIDGET (priv->listbox))
+ gtk_container_remove (GTK_CONTAINER (priv->listbox_box), child);
+}
+
+static void
+hdy_preferences_group_class_init (HdyPreferencesGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_group_get_property;
+ object_class->set_property = hdy_preferences_group_set_property;
+ object_class->dispose = hdy_preferences_group_dispose;
+
+ container_class->add = hdy_preferences_group_add;
+ container_class->remove = hdy_preferences_group_remove;
+ container_class->forall = hdy_preferences_group_forall;
+
+ /**
+ * HdyPreferencesGroup:description:
+ *
+ * The description for this group of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DESCRIPTION] =
+ g_param_spec_string ("description",
+ _("Description"),
+ _("Description"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyPreferencesGroup:title:
+ *
+ * The title for this group of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("Title"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "preferencesgroup");
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-group.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, description);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox_box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, title);
+ gtk_widget_class_bind_template_callback (widget_class, update_listbox_visibility);
+}
+
+static void
+hdy_preferences_group_init (HdyPreferencesGroup *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ update_description_visibility (self);
+ update_title_visibility (self);
+ update_listbox_visibility (self);
+}
+
+/**
+ * hdy_preferences_group_new:
+ *
+ * Creates a new #HdyPreferencesGroup.
+ *
+ * Returns: a new #HdyPreferencesGroup
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_group_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_GROUP, NULL);
+}
+
+/**
+ * hdy_preferences_group_get_title:
+ * @self: a #HdyPreferencesGroup
+ *
+ * Gets the title of @self.
+ *
+ * Returns: the title of @self.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_group_get_title (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL);
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ return gtk_label_get_text (priv->title);
+}
+
+/**
+ * hdy_preferences_group_set_title:
+ * @self: a #HdyPreferencesGroup
+ * @title: the title
+ *
+ * Sets the title for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_set_title (HdyPreferencesGroup *self,
+ const gchar *title)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_label (priv->title), title) == 0)
+ return;
+
+ gtk_label_set_label (priv->title, title);
+ update_title_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_preferences_group_get_description:
+ * @self: a #HdyPreferencesGroup
+ *
+ *
+ * Returns: the description of @self.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_group_get_description (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL);
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ return gtk_label_get_text (priv->description);
+}
+
+/**
+ * hdy_preferences_group_set_description:
+ * @self: a #HdyPreferencesGroup
+ * @description: the description
+ *
+ * Sets the description for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_set_description (HdyPreferencesGroup *self,
+ const gchar *description)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_label (priv->description), description) == 0)
+ return;
+
+ gtk_label_set_label (priv->description, description);
+ update_description_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]);
+}
+
+static void
+add_preferences_to_model (HdyPreferencesRow *row,
+ GListStore *model)
+{
+ const gchar *title;
+
+ g_assert (HDY_IS_PREFERENCES_ROW (row));
+ g_assert (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (row)))
+ return;
+
+ title = hdy_preferences_row_get_title (row);
+
+ if (!title || !*title)
+ return;
+
+ g_list_store_append (model, row);
+}
+
+/**
+ * hdy_preferences_group_add_preferences_to_model: (skip)
+ * @self: a #HdyPreferencesGroup
+ * @model: the model
+ *
+ * Add preferences from @self to the model.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self,
+ GListStore *model)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+ g_return_if_fail (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+ return;
+
+ gtk_container_foreach (GTK_CONTAINER (priv->listbox), (GtkCallback) add_preferences_to_model, model);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-group.h b/subprojects/libhandy/src/hdy-preferences-group.h
new file mode 100644
index 0000000..2a7952c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_GROUP (hdy_preferences_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesGroup, hdy_preferences_group, HDY, PREFERENCES_GROUP, GtkBin)
+
+/**
+ * HdyPreferencesGroupClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesGroupClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_group_get_title (HdyPreferencesGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_group_set_title (HdyPreferencesGroup *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_group_get_description (HdyPreferencesGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_group_set_description (HdyPreferencesGroup *self,
+ const gchar *description);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-group.ui b/subprojects/libhandy/src/hdy-preferences-group.ui
new file mode 100644
index 0000000..60a5ae1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.ui
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesGroup" parent="GtkBin">
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="xalign">0</property>
+ <style>
+ <!-- Requires Adwaita from GTK 3.24.14. -->
+ <class name="heading"/>
+ <!-- Matching elementary class. -->
+ <class name="h4"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="description">
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="listbox_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="selection_mode">none</property>
+ <property name="visible">True</property>
+ <signal name="add" handler="update_listbox_visibility" after="yes" swapped="yes"/>
+ <signal name="remove" handler="update_listbox_visibility" after="yes" swapped="yes"/>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-preferences-page-private.h b/subprojects/libhandy/src/hdy-preferences-page-private.h
new file mode 100644
index 0000000..a93ccfd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page-private.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include "hdy-preferences-page.h"
+
+G_BEGIN_DECLS
+
+GtkAdjustment *hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self);
+
+void hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self,
+ GListStore *model);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-page.c b/subprojects/libhandy/src/hdy-preferences-page.c
new file mode 100644
index 0000000..51fdc0d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.c
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-page-private.h"
+
+#include "hdy-preferences-group-private.h"
+
+/**
+ * SECTION:hdy-preferences-page
+ * @short_description: A page from the preferences window.
+ * @Title: HdyPreferencesPage
+ *
+ * The #HdyPreferencesPage widget gathers preferences groups into a single page
+ * of a preferences window.
+ *
+ * # CSS nodes
+ *
+ * #HdyPreferencesPage has a single CSS node with name preferencespage.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkScrolledWindow *scrolled_window;
+
+ gchar *icon_name;
+ gchar *title;
+} HdyPreferencesPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesPage, hdy_preferences_page, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_TITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+typedef struct {
+ HdyPreferencesPage *preferences_page;
+ GtkCallback callback;
+ gpointer data;
+} CallbackData;
+
+static void
+hdy_preferences_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_preferences_page_get_icon_name (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_page_get_title (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_preferences_page_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_TITLE:
+ hdy_preferences_page_set_title (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_page_finalize (GObject *object)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ g_clear_pointer (&priv->icon_name, g_free);
+ g_clear_pointer (&priv->title, g_free);
+
+ G_OBJECT_CLASS (hdy_preferences_page_parent_class)->finalize (object);
+}
+
+static void
+hdy_preferences_page_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (priv->scrolled_window == NULL)
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->add (container, child);
+ else if (HDY_IS_PREFERENCES_GROUP (child))
+ gtk_container_add (GTK_CONTAINER (priv->box), child);
+ else
+ g_warning ("Can't add children of type %s to %s",
+ G_OBJECT_TYPE_NAME (child),
+ G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+hdy_preferences_page_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->scrolled_window))
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->remove (container, child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->box), child);
+}
+
+static void
+hdy_preferences_page_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else if (priv->box)
+ gtk_container_foreach (GTK_CONTAINER (priv->box), callback, callback_data);
+}
+
+static void
+hdy_preferences_page_class_init (HdyPreferencesPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_page_get_property;
+ object_class->set_property = hdy_preferences_page_set_property;
+ object_class->finalize = hdy_preferences_page_finalize;
+
+ container_class->add = hdy_preferences_page_add;
+ container_class->remove = hdy_preferences_page_remove;
+ container_class->forall = hdy_preferences_page_forall;
+
+ /**
+ * HdyPreferencesPage:icon-name:
+ *
+ * The icon name for this page of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesPage:title:
+ *
+ * The title for this page of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("Title"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-page.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, scrolled_window);
+
+ gtk_widget_class_set_css_name (widget_class, "preferencespage");
+}
+
+static void
+hdy_preferences_page_init (HdyPreferencesPage *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_preferences_page_new:
+ *
+ * Creates a new #HdyPreferencesPage.
+ *
+ * Returns: a new #HdyPreferencesPage
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_page_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_PAGE, NULL);
+}
+
+/**
+ * hdy_preferences_page_get_icon_name:
+ * @self: a #HdyPreferencesPage
+ *
+ * Gets the icon name for @self, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): the icon name for @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_page_get_icon_name (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return priv->icon_name;
+}
+
+/**
+ * hdy_preferences_page_set_icon_name:
+ * @self: a #HdyPreferencesPage
+ * @icon_name: (nullable): the icon name, or %NULL
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_set_icon_name (HdyPreferencesPage *self,
+ const gchar *icon_name)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ if (g_strcmp0 (priv->icon_name, icon_name) == 0)
+ return;
+
+ g_clear_pointer (&priv->icon_name, g_free);
+ priv->icon_name = g_strdup (icon_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_preferences_page_get_title:
+ * @self: a #HdyPreferencesPage
+ *
+ * Gets the title of @self, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): the title of the @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_page_get_title (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return priv->title;
+}
+
+/**
+ * hdy_preferences_page_set_title:
+ * @self: a #HdyPreferencesPage
+ * @title: (nullable): the title of the page, or %NULL
+ *
+ * Sets the title of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_set_title (HdyPreferencesPage *self,
+ const gchar *title)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ if (g_strcmp0 (priv->title, title) == 0)
+ return;
+
+ g_clear_pointer (&priv->title, g_free);
+ priv->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+GtkAdjustment *
+hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return gtk_scrolled_window_get_vadjustment (priv->scrolled_window);
+}
+
+/**
+ * hdy_preferences_page_add_preferences_to_model: (skip)
+ * @self: a #HdyPreferencesPage
+ * @model: the model
+ *
+ * Add preferences from @self to the model.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self,
+ GListStore *model)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+ g_return_if_fail (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+ return;
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->box), (GtkCallback) hdy_preferences_group_add_preferences_to_model, model);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-page.h b/subprojects/libhandy/src/hdy-preferences-page.h
new file mode 100644
index 0000000..158c18c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_PAGE (hdy_preferences_page_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesPage, hdy_preferences_page, HDY, PREFERENCES_PAGE, GtkBin)
+
+/**
+ * HdyPreferencesPageClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesPageClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_page_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_page_get_icon_name (HdyPreferencesPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_page_set_icon_name (HdyPreferencesPage *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_page_get_title (HdyPreferencesPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_page_set_title (HdyPreferencesPage *self,
+ const gchar *title);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-page.ui b/subprojects/libhandy/src/hdy-preferences-page.ui
new file mode 100644
index 0000000..809dee7
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesPage" parent="GtkBin">
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="shadow-type">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="margin-bottom">18</property>
+ <property name="margin-end">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-top">18</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-preferences-row.c b/subprojects/libhandy/src/hdy-preferences-row.c
new file mode 100644
index 0000000..9327509
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-row.c
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-row.h"
+
+/**
+ * SECTION:hdy-preferences-row
+ * @short_description: A #GtkListBox row used to present preferences.
+ * @Title: HdyPreferencesRow
+ *
+ * The #HdyPreferencesRow widget has a title that #HdyPreferencesWindow will use
+ * to let the user look for a preference. It doesn't present the title in any
+ * way and it lets you present the preference as you please.
+ *
+ * #HdyActionRow and its derivatives are convenient to use as preference rows as
+ * they take care of presenting the preference's title while letting you compose
+ * the inputs of the preference around it.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ gchar *title;
+
+ gboolean use_underline;
+} HdyPreferencesRowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesRow, hdy_preferences_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_USE_UNDERLINE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+hdy_preferences_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_row_get_title (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_preferences_row_get_use_underline (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ hdy_preferences_row_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_preferences_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_row_finalize (GObject *object)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+ HdyPreferencesRowPrivate *priv = hdy_preferences_row_get_instance_private (self);
+
+ g_free (priv->title);
+
+ G_OBJECT_CLASS (hdy_preferences_row_parent_class)->finalize (object);
+}
+
+static void
+hdy_preferences_row_class_init (HdyPreferencesRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_row_get_property;
+ object_class->set_property = hdy_preferences_row_set_property;
+ object_class->finalize = hdy_preferences_row_finalize;
+
+ /**
+ * HdyPreferencesRow:title:
+ *
+ * The title of the preference represented by this row.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title of the preference"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title indicates a
+ * mnemonic.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+hdy_preferences_row_init (HdyPreferencesRow *self)
+{
+}
+
+/**
+ * hdy_preferences_row_new:
+ *
+ * Creates a new #HdyPreferencesRow.
+ *
+ * Returns: a new #HdyPreferencesRow
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_row_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_ROW, NULL);
+}
+
+/**
+ * hdy_preferences_row_get_title:
+ * @self: a #HdyPreferencesRow
+ *
+ * Gets the title of the preference represented by @self.
+ *
+ * Returns: (transfer none) (nullable): the title of the preference represented
+ * by @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_row_get_title (HdyPreferencesRow *self)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), NULL);
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ return priv->title;
+}
+
+/**
+ * hdy_preferences_row_set_title:
+ * @self: a #HdyPreferencesRow
+ * @title: (nullable): the title, or %NULL.
+ *
+ * Sets the title of the preference represented by @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_row_set_title (HdyPreferencesRow *self,
+ const gchar *title)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_ROW (self));
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ if (g_strcmp0 (priv->title, title) == 0)
+ return;
+
+ g_free (priv->title);
+ priv->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_preferences_row_get_use_underline:
+ * @self: a #HdyPreferencesRow
+ *
+ * Gets whether an embedded underline in the text of the title indicates a
+ * mnemonic. See hdy_preferences_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title indicates the mnemonic
+ * accelerator keys.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_preferences_row_get_use_underline (HdyPreferencesRow *self)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), FALSE);
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ return priv->use_underline;
+}
+
+/**
+ * hdy_preferences_row_set_use_underline:
+ * @self: a #HdyPreferencesRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title indicates the next character
+ * should be used for the mnemonic accelerator key.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_row_set_use_underline (HdyPreferencesRow *self,
+ gboolean use_underline)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_ROW (self));
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ if (priv->use_underline == !!use_underline)
+ return;
+
+ priv->use_underline = !!use_underline;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-row.h b/subprojects/libhandy/src/hdy-preferences-row.h
new file mode 100644
index 0000000..f5e926b
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-row.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_ROW (hdy_preferences_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesRow, hdy_preferences_row, HDY, PREFERENCES_ROW, GtkListBoxRow)
+
+/**
+ * HdyPreferencesRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesRowClass
+{
+ GtkListBoxRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_row_get_title (HdyPreferencesRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_row_set_title (HdyPreferencesRow *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_row_get_use_underline (HdyPreferencesRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_row_set_use_underline (HdyPreferencesRow *self,
+ gboolean use_underline);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-window.c b/subprojects/libhandy/src/hdy-preferences-window.c
new file mode 100644
index 0000000..3618d58
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.c
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-window.h"
+
+#include "hdy-animation.h"
+#include "hdy-action-row.h"
+#include "hdy-deck.h"
+#include "hdy-preferences-group-private.h"
+#include "hdy-preferences-page-private.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-bar.h"
+#include "hdy-view-switcher-title.h"
+
+/**
+ * SECTION:hdy-preferences-window
+ * @short_description: A window to present an application's preferences.
+ * @Title: HdyPreferencesWindow
+ *
+ * The #HdyPreferencesWindow widget presents an application's preferences
+ * gathered into pages and groups. The preferences are searchable by the user.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ HdyDeck *subpages_deck;
+ GtkWidget *preferences;
+ GtkStack *content_stack;
+ GtkStack *pages_stack;
+ GtkToggleButton *search_button;
+ GtkSearchEntry *search_entry;
+ GtkListBox *search_results;
+ GtkStack *search_stack;
+ GtkStack *title_stack;
+ HdyViewSwitcherBar *view_switcher_bar;
+ HdyViewSwitcherTitle *view_switcher_title;
+
+ gboolean search_enabled;
+ gboolean can_swipe_back;
+ gint n_last_search_results;
+ GtkWidget *subpage;
+} HdyPreferencesWindowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesWindow, hdy_preferences_window, HDY_TYPE_WINDOW)
+
+enum {
+ PROP_0,
+ PROP_SEARCH_ENABLED,
+ PROP_CAN_SWIPE_BACK,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+filter_search_results (HdyActionRow *row,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ g_autofree gchar *text = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (priv->search_entry)), -1);
+ g_autofree gchar *title = g_utf8_casefold (hdy_preferences_row_get_title (HDY_PREFERENCES_ROW (row)), -1);
+ g_autofree gchar *subtitle = NULL;
+
+ /* The CSS engine works in such a way that invisible children are treated as
+ * visible widgets, which breaks the expectations of the .preferences style
+ * class when filtering a row, leading to straight corners when the first row
+ * or last row are filtered out.
+ *
+ * This works around it by explicitly toggling the row's visibility, while
+ * keeping GtkListBox's filtering logic.
+ *
+ * See https://gitlab.gnome.org/GNOME/libhandy/-/merge_requests/424
+ */
+
+ if (strstr (title, text)) {
+ priv->n_last_search_results++;
+ gtk_widget_show (GTK_WIDGET (row));
+
+ return TRUE;
+ }
+
+ subtitle = g_utf8_casefold (hdy_action_row_get_subtitle (row), -1);
+
+ if (!!strstr (subtitle, text)) {
+ priv->n_last_search_results++;
+ gtk_widget_show (GTK_WIDGET (row));
+
+ return TRUE;
+ }
+
+ gtk_widget_hide (GTK_WIDGET (row));
+
+ return FALSE;
+}
+
+static GtkWidget *
+new_search_row_for_preference (HdyPreferencesRow *row,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ HdyActionRow *widget;
+ HdyPreferencesGroup *group;
+ HdyPreferencesPage *page;
+ const gchar *group_title, *page_title;
+ GtkWidget *parent;
+
+ g_assert (HDY_IS_PREFERENCES_ROW (row));
+
+ widget = HDY_ACTION_ROW (hdy_action_row_new ());
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (widget), TRUE);
+ g_object_bind_property (row, "title", widget, "title", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (row, "use-underline", widget, "use-underline", G_BINDING_SYNC_CREATE);
+
+ for (parent = gtk_widget_get_parent (GTK_WIDGET (row));
+ parent != NULL && !HDY_IS_PREFERENCES_GROUP (parent);
+ parent = gtk_widget_get_parent (parent));
+ group = parent != NULL ? HDY_PREFERENCES_GROUP (parent) : NULL;
+ group_title = group != NULL ? hdy_preferences_group_get_title (group) : NULL;
+ if (g_strcmp0 (group_title, "") == 0)
+ group_title = NULL;
+
+ for (parent = gtk_widget_get_parent (GTK_WIDGET (group));
+ parent != NULL && !HDY_IS_PREFERENCES_PAGE (parent);
+ parent = gtk_widget_get_parent (parent));
+ page = parent != NULL ? HDY_PREFERENCES_PAGE (parent) : NULL;
+ page_title = page != NULL ? hdy_preferences_page_get_title (page) : NULL;
+ if (g_strcmp0 (page_title, "") == 0)
+ page_title = NULL;
+
+ if (group_title && !hdy_view_switcher_title_get_title_visible (priv->view_switcher_title))
+ hdy_action_row_set_subtitle (widget, group_title);
+ if (group_title) {
+ g_autofree gchar *subtitle = g_strdup_printf ("%s → %s", page_title != NULL ? page_title : _("Untitled page"), group_title);
+ hdy_action_row_set_subtitle (widget, subtitle);
+ } else if (page_title)
+ hdy_action_row_set_subtitle (widget, page_title);
+
+ gtk_widget_show (GTK_WIDGET (widget));
+
+ g_object_set_data (G_OBJECT (widget), "page", page);
+ g_object_set_data (G_OBJECT (widget), "row", row);
+
+ return GTK_WIDGET (widget);
+}
+
+static void
+update_search_results (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ g_autoptr (GListStore) model;
+
+ model = g_list_store_new (HDY_TYPE_PREFERENCES_ROW);
+ gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), (GtkCallback) hdy_preferences_page_add_preferences_to_model, model);
+ gtk_container_foreach (GTK_CONTAINER (priv->search_results), (GtkCallback) gtk_widget_destroy, NULL);
+ for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
+ gtk_container_add (GTK_CONTAINER (priv->search_results),
+ new_search_row_for_preference ((HdyPreferencesRow *) g_list_model_get_item (G_LIST_MODEL (model), i), self));
+}
+
+static void
+search_result_activated_cb (HdyPreferencesWindow *self,
+ HdyActionRow *widget)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ HdyPreferencesPage *page;
+ HdyPreferencesRow *row;
+ GtkAdjustment *adjustment;
+ GtkAllocation allocation;
+ gint y = 0;
+
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+ page = HDY_PREFERENCES_PAGE (g_object_get_data (G_OBJECT (widget), "page"));
+ row = HDY_PREFERENCES_ROW (g_object_get_data (G_OBJECT (widget), "row"));
+
+ g_assert (page != NULL);
+ g_assert (row != NULL);
+
+ adjustment = hdy_preferences_page_get_vadjustment (page);
+
+ g_assert (adjustment != NULL);
+
+ gtk_stack_set_visible_child (priv->pages_stack, GTK_WIDGET (page));
+ gtk_widget_set_can_focus (GTK_WIDGET (row), TRUE);
+ gtk_widget_grab_focus (GTK_WIDGET (row));
+
+ if (!gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (page), 0, 0, NULL, &y))
+ return;
+
+ gtk_container_set_focus_child (GTK_CONTAINER (page), GTK_WIDGET (row));
+ y += gtk_adjustment_get_value (adjustment);
+ gtk_widget_get_allocation (GTK_WIDGET (row), &allocation);
+ gtk_adjustment_clamp_page (adjustment, y, y + allocation.height);
+}
+
+static gboolean
+key_press_event_cb (GtkWidget *sender,
+ GdkEvent *event,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask ();
+ guint keyval;
+ GdkModifierType state;
+
+ if (priv->subpage)
+ return GDK_EVENT_PROPAGATE;
+
+ gdk_event_get_keyval (event, &keyval);
+ gdk_event_get_state (event, &state);
+
+ if (priv->search_enabled &&
+ (keyval == GDK_KEY_f || keyval == GDK_KEY_F) &&
+ (state & default_modifiers) == GDK_CONTROL_MASK) {
+ gtk_toggle_button_set_active (priv->search_button, TRUE);
+
+ return GDK_EVENT_STOP;
+ }
+
+ if (priv->search_enabled &&
+ gtk_search_entry_handle_event (priv->search_entry, event)) {
+ gtk_toggle_button_set_active (priv->search_button, TRUE);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+try_remove_subpages (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (hdy_deck_get_transition_running (priv->subpages_deck))
+ return;
+
+ if (hdy_deck_get_visible_child (priv->subpages_deck) == priv->preferences)
+ priv->subpage = NULL;
+
+ for (GList *child = gtk_container_get_children (GTK_CONTAINER (priv->subpages_deck));
+ child;
+ child = child->next)
+ if (child->data != priv->preferences && child->data != priv->subpage)
+ gtk_container_remove (GTK_CONTAINER (priv->subpages_deck), child->data);
+}
+
+static void
+subpages_deck_transition_running_cb (HdyPreferencesWindow *self)
+{
+ try_remove_subpages (self);
+}
+
+static void
+subpages_deck_visible_child_cb (HdyPreferencesWindow *self)
+{
+ try_remove_subpages (self);
+}
+
+static void
+header_bar_size_allocate_cb (HdyPreferencesWindow *self,
+ GdkRectangle *allocation)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ hdy_view_switcher_title_set_view_switcher_enabled (priv->view_switcher_title, allocation->width > 360);
+}
+
+static void
+title_stack_notify_transition_running_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (gtk_stack_get_transition_running (priv->title_stack) ||
+ gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title))
+ return;
+
+ gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+}
+
+static void
+title_stack_notify_visible_child_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (hdy_get_enable_animations (GTK_WIDGET (priv->title_stack)) ||
+ gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title))
+ return;
+
+ gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+}
+
+
+static void
+search_button_notify_active_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (gtk_toggle_button_get_active (priv->search_button)) {
+ update_search_results (self);
+ gtk_stack_set_visible_child_name (priv->title_stack, "search");
+ gtk_stack_set_visible_child_name (priv->content_stack, "search");
+ gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->search_entry));
+ /* Grabbing without selecting puts the cursor at the start of the buffer, so
+ * for "type to search" to work we must move the cursor at the end. We can't
+ * use GTK_MOVEMENT_BUFFER_ENDS because it causes a sound to be played.
+ */
+ g_signal_emit_by_name (priv->search_entry, "move-cursor",
+ GTK_MOVEMENT_LOGICAL_POSITIONS, G_MAXINT, FALSE, NULL);
+ } else {
+ gtk_stack_set_visible_child_name (priv->title_stack, "pages");
+ gtk_stack_set_visible_child_name (priv->content_stack, "pages");
+ }
+}
+
+static void
+search_changed_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ priv->n_last_search_results = 0;
+ gtk_list_box_invalidate_filter (priv->search_results);
+ gtk_stack_set_visible_child_name (priv->search_stack,
+ priv->n_last_search_results > 0 ? "results" : "no-results");
+}
+
+static void
+on_page_icon_name_changed (HdyPreferencesPage *page,
+ GParamSpec *pspec,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
+ "icon-name", hdy_preferences_page_get_icon_name (page),
+ NULL);
+}
+
+static void
+stop_search_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+}
+
+static void
+on_page_title_changed (HdyPreferencesPage *page,
+ GParamSpec *pspec,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
+ "title", hdy_preferences_page_get_title (page),
+ NULL);
+}
+
+static void
+hdy_preferences_window_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_ENABLED:
+ g_value_set_boolean (value, hdy_preferences_window_get_search_enabled (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_preferences_window_get_can_swipe_back (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_window_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_ENABLED:
+ hdy_preferences_window_set_search_enabled (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_preferences_window_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_window_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->content_stack == NULL)
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->add (container, child);
+ else if (HDY_IS_PREFERENCES_PAGE (child)) {
+ gtk_container_add (GTK_CONTAINER (priv->pages_stack), child);
+ on_page_icon_name_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
+ on_page_title_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
+ g_signal_connect (child, "notify::icon-name",
+ G_CALLBACK (on_page_icon_name_changed), self);
+ g_signal_connect (child, "notify::title",
+ G_CALLBACK (on_page_title_changed), self);
+ } else
+ g_warning ("Can't add children of type %s to %s",
+ G_OBJECT_TYPE_NAME (child),
+ G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+hdy_preferences_window_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->content_stack))
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->remove (container, child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->pages_stack), child);
+}
+
+static void
+hdy_preferences_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else if (priv->pages_stack)
+ gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), callback, callback_data);
+}
+
+static void
+hdy_preferences_window_class_init (HdyPreferencesWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_window_get_property;
+ object_class->set_property = hdy_preferences_window_set_property;
+
+ container_class->add = hdy_preferences_window_add;
+ container_class->remove = hdy_preferences_window_remove;
+ container_class->forall = hdy_preferences_window_forall;
+
+ /**
+ * HdyPreferencesWindow:search-enabled:
+ *
+ * Whether search is enabled.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SEARCH_ENABLED] =
+ g_param_spec_boolean ("search-enabled",
+ _("Search enabled"),
+ _("Whether search is enabled"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesWindow:can-swipe-back:
+ *
+ * Whether or not the window allows closing the subpage via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch from a subpage to the preferences"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-window.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, subpages_deck);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, preferences);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, content_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, pages_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_button);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_results);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, title_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_bar);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_title);
+ gtk_widget_class_bind_template_callback (widget_class, subpages_deck_transition_running_cb);
+ gtk_widget_class_bind_template_callback (widget_class, subpages_deck_visible_child_cb);
+ gtk_widget_class_bind_template_callback (widget_class, header_bar_size_allocate_cb);
+ gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_transition_running_cb);
+ gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_visible_child_cb);
+ gtk_widget_class_bind_template_callback (widget_class, key_press_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_button_notify_active_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_result_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, stop_search_cb);
+}
+
+static void
+hdy_preferences_window_init (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ priv->search_enabled = TRUE;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_filter_func (priv->search_results, (GtkListBoxFilterFunc) filter_search_results, self, NULL);
+}
+
+/**
+ * hdy_preferences_window_new:
+ *
+ * Creates a new #HdyPreferencesWindow.
+ *
+ * Returns: a new #HdyPreferencesWindow
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_window_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_WINDOW, NULL);
+}
+
+/**
+ * hdy_preferences_window_get_search_enabled:
+ * @self: a #HdyPreferencesWindow
+ *
+ * Gets whether search is enabled for @self.
+ *
+ * Returns: whether search is enabled for @self.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE);
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ return priv->search_enabled;
+}
+
+/**
+ * hdy_preferences_window_set_search_enabled:
+ * @self: a #HdyPreferencesWindow
+ * @search_enabled: %TRUE to enable search, %FALSE to disable it
+ *
+ * Sets whether search is enabled for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self,
+ gboolean search_enabled)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ search_enabled = !!search_enabled;
+
+ if (priv->search_enabled == search_enabled)
+ return;
+
+ priv->search_enabled = search_enabled;
+ gtk_widget_set_visible (GTK_WIDGET (priv->search_button), search_enabled);
+ if (!search_enabled)
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_ENABLED]);
+}
+
+/**
+ * hdy_preferences_window_set_can_swipe_back:
+ * @self: a #HdyPreferencesWindow
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching from a subpage to the preferences
+ * via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self,
+ gboolean can_swipe_back)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ can_swipe_back = !!can_swipe_back;
+
+ if (priv->can_swipe_back == can_swipe_back)
+ return;
+
+ priv->can_swipe_back = can_swipe_back;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]);
+}
+
+/**
+ * hdy_preferences_window_get_can_swipe_back
+ * @self: a #HdyPreferencesWindow
+ *
+ * Returns whether or not @self allows switching from a subpage to the
+ * preferences via a swipe gesture.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE);
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ return priv->can_swipe_back;
+}
+
+/**
+ * hdy_preferences_window_present_subpage:
+ * @self: a #HdyPreferencesWindow
+ * @subpage: the subpage
+ *
+ * Sets @subpage as the window's subpage and present it.
+ * The transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_present_subpage (HdyPreferencesWindow *self,
+ GtkWidget *subpage)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+ g_return_if_fail (GTK_IS_WIDGET (subpage));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->subpage == subpage)
+ return;
+
+ priv->subpage = subpage;
+
+ /* The check below avoids a warning when re-entering a subpage during the
+ * transition between the that subpage to the preferences.
+ */
+ if (gtk_widget_get_parent (subpage) != GTK_WIDGET (priv->subpages_deck))
+ gtk_container_add (GTK_CONTAINER (priv->subpages_deck), subpage);
+
+ hdy_deck_set_visible_child (priv->subpages_deck, subpage);
+}
+
+/**
+ * hdy_preferences_window_close_subpage:
+ * @self: a #HdyPreferencesWindow
+ *
+ * Closes the current subpage to return back to the preferences, if there is no
+ * presented subpage, this does nothing.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_close_subpage (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->subpage == NULL)
+ return;
+
+ hdy_deck_set_visible_child (priv->subpages_deck, priv->preferences);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-window.h b/subprojects/libhandy/src/hdy-preferences-window.h
new file mode 100644
index 0000000..427a94a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-window.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_WINDOW (hdy_preferences_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesWindow, hdy_preferences_window, HDY, PREFERENCES_WINDOW, HdyWindow)
+
+/**
+ * HdyPreferencesWindowClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesWindowClass
+{
+ HdyWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_window_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self,
+ gboolean search_enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self,
+ gboolean can_swipe_back);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_present_subpage (HdyPreferencesWindow *self,
+ GtkWidget *subpage);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_close_subpage (HdyPreferencesWindow *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-window.ui b/subprojects/libhandy/src/hdy-preferences-window.ui
new file mode 100644
index 0000000..5f764fc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.ui
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesWindow" parent="HdyWindow">
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="icon_name">gtk-preferences</property>
+ <property name="title" translatable="yes">Preferences</property>
+ <property name="type_hint">dialog</property>
+ <property name="default-width">640</property>
+ <property name="default-height">576</property>
+ <signal name="key-press-event" handler="key_press_event_cb" after="yes" swapped="no"/>
+ <child>
+ <object class="HdyDeck" id="subpages_deck">
+ <property name="can-swipe-back" bind-source="HdyPreferencesWindow" bind-property="can-swipe-back" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <property name="width-request">360</property>
+ <signal name="notify::transition-running" handler="subpages_deck_transition_running_cb" swapped="yes"/>
+ <signal name="notify::visible-child" handler="subpages_deck_visible_child_cb" swapped="yes"/>
+ <child>
+ <object class="GtkBox" id="preferences">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="HdyHeaderBar">
+ <property name="centering_policy">strict</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <signal name="size-allocate" handler="header_bar_size_allocate_cb" swapped="yes"/>
+ <child type="title">
+ <object class="GtkStack" id="title_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="visible">True</property>
+ <signal name="notify::visible-child" handler="title_stack_notify_visible_child_cb" swapped="true"/>
+ <signal name="notify::transition-running" handler="title_stack_notify_transition_running_cb" swapped="true"/>
+ <child>
+ <object class="HdyViewSwitcherTitle" id="view_switcher_title">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="stack">pages_stack</property>
+ <property name="title" bind-source="HdyPreferencesWindow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">pages</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="tightening-threshold">300</property>
+ <property name="maximum-size">400</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ <signal name="search-changed" handler="search_changed_cb" swapped="yes"/>
+ <signal name="stop-search" handler="stop_search_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="search_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <signal name="notify::active" handler="search_button_notify_active_cb" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-search">
+ <property name="accessible-name" translatable="yes">Search</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="content_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="vhomogeneous">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkStack" id="pages_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyViewSwitcherBar" id="view_switcher_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stack">pages_stack</property>
+ <property name="reveal" bind-source="view_switcher_title" bind-property="title-visible" bind-flags="sync-create"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">pages</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="search_stack">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="margin_bottom">18</property>
+ <property name="margin_end">12</property>
+ <property name="margin_start">12</property>
+ <property name="margin_top">18</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="search_results">
+ <property name="selection-mode">none</property>
+ <property name="valign">start</property>
+ <property name="visible">True</property>
+ <signal name="row-activated" handler="search_result_activated_cb" swapped="yes"/>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">results</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">0</property>
+ <property name="margin_bottom">18</property>
+ <property name="pixel_size">128</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="margin_end">12</property>
+ <property name="margin_start">12</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="justify">center</property>
+ <property name="label" translatable="yes">No Results Found</property>
+ <property name="margin_bottom">12</property>
+ <property name="opacity">0.5</property>
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="scale" value="2"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="can_focus">False</property>
+ <property name="justify">center</property>
+ <property name="label" translatable="yes">Try a different search</property>
+ <property name="margin_bottom">6</property>
+ <property name="opacity">0.5</property>
+ <property name="use_markup">True</property>
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">no-results</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-search-bar.c b/subprojects/libhandy/src/hdy-search-bar.c
new file mode 100644
index 0000000..decbda4
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.c
@@ -0,0 +1,659 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 2013 Red Hat, Inc.
+ * Copyright (C) 2018 Purism SPC
+ *
+ * Authors:
+ * - Bastien Nocera <bnocera@redhat.com>
+ * - Adrien Plazas <adrien.plazas@puri.sm>
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 2013. See the AUTHORS
+ * file for a list of people on the GTK Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/*
+ * Forked from the GTK+ 3.94.0 GtkSearchBar widget and modified for libhandy by
+ * Adrien Plazas on behalf of Purism SPC 2018.
+ *
+ * The AUTHORS file referenced above is part of GTK and not present in
+ * libhandy. At the time of the fork it was available here:
+ * https://gitlab.gnome.org/GNOME/gtk/blob/faba0f0145b1281facba20fb90699e3db594fbb0/AUTHORS
+ *
+ * The ChangeLog file referenced above was not present in GTK+ at the time of
+ * the fork.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-search-bar.h"
+
+/**
+ * SECTION:hdy-search-bar
+ * @short_description: A toolbar to integrate a search entry with.
+ * @Title: HdySearchBar
+ *
+ * #HdySearchBar is a container made to have a search entry (possibly
+ * with additional connex widgets, such as drop-down menus, or buttons)
+ * built-in. The search bar would appear when a search is started through
+ * typing on the keyboard, or the application’s search mode is toggled on.
+ *
+ * For keyboard presses to start a search, events will need to be
+ * forwarded from the top-level window that contains the search bar.
+ * See hdy_search_bar_handle_event() for example code. Common shortcuts
+ * such as Ctrl+F should be handled as an application action, or through
+ * the menu items.
+ *
+ * You will also need to tell the search bar about which entry you
+ * are using as your search entry using hdy_search_bar_connect_entry().
+ * The following example shows you how to create a more complex search
+ * entry.
+ *
+ * HdySearchBar is very similar to #GtkSearchBar, the main difference being that
+ * it allows the search entry to fill all the available space. This allows you
+ * to control your search entry's width with a #HdyClamp.
+ *
+ * # CSS nodes
+ *
+ * #HdySearchBar has a single CSS node with name searchbar.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct {
+ /* Template widgets */
+ GtkWidget *revealer;
+ GtkWidget *tool_box;
+ GtkWidget *start;
+ GtkWidget *end;
+ GtkWidget *close_button;
+
+ GtkWidget *entry;
+ gboolean reveal_child;
+ gboolean show_close_button;
+} HdySearchBarPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdySearchBar, hdy_search_bar, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_SEARCH_MODE_ENABLED,
+ PROP_SHOW_CLOSE_BUTTON,
+ LAST_PROPERTY
+};
+
+static GParamSpec *props[LAST_PROPERTY] = { NULL, };
+
+/* This comes from gtksearchentry.c in GTK. */
+static gboolean
+gtk_search_entry_is_keynav_event (GdkEvent *event)
+{
+ GdkModifierType state = 0;
+ guint keyval;
+
+ if (!gdk_event_get_keyval (event, &keyval))
+ return FALSE;
+
+ gdk_event_get_state (event, &state);
+
+ if (keyval == GDK_KEY_Tab || keyval == GDK_KEY_KP_Tab ||
+ keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up ||
+ keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down ||
+ keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left ||
+ keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right ||
+ keyval == GDK_KEY_Home || keyval == GDK_KEY_KP_Home ||
+ keyval == GDK_KEY_End || keyval == GDK_KEY_KP_End ||
+ keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_KP_Page_Up ||
+ keyval == GDK_KEY_Page_Down || keyval == GDK_KEY_KP_Page_Down ||
+ ((state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) != 0))
+ return TRUE;
+
+ /* Other navigation events should get automatically
+ * ignored as they will not change the content of the entry
+ */
+ return FALSE;
+}
+
+static void
+stop_search_cb (GtkWidget *entry,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE);
+}
+
+static gboolean
+entry_key_pressed_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ HdySearchBar *self)
+{
+ if (event->key.keyval == GDK_KEY_Escape) {
+ stop_search_cb (widget, self);
+
+ return GDK_EVENT_STOP;
+ } else {
+ return GDK_EVENT_PROPAGATE;
+ }
+}
+
+static void
+preedit_changed_cb (GtkEntry *entry,
+ GtkWidget *popup,
+ gboolean *preedit_changed)
+{
+ *preedit_changed = TRUE;
+}
+
+static gboolean
+hdy_search_bar_handle_event_for_entry (HdySearchBar *self,
+ GdkEvent *event)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean handled;
+ gboolean preedit_changed;
+ guint preedit_change_id;
+ gboolean res;
+ char *old_text, *new_text;
+
+ if (gtk_search_entry_is_keynav_event (event) ||
+ event->key.keyval == GDK_KEY_space ||
+ event->key.keyval == GDK_KEY_Menu)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!gtk_widget_get_realized (priv->entry))
+ gtk_widget_realize (priv->entry);
+
+ handled = GDK_EVENT_PROPAGATE;
+ preedit_changed = FALSE;
+ preedit_change_id = g_signal_connect (priv->entry, "preedit-changed",
+ G_CALLBACK (preedit_changed_cb), &preedit_changed);
+
+ old_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry)));
+ res = gtk_widget_event (priv->entry, event);
+ new_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry)));
+
+ g_signal_handler_disconnect (priv->entry, preedit_change_id);
+
+ if ((res && g_strcmp0 (new_text, old_text) != 0) || preedit_changed)
+ handled = GDK_EVENT_STOP;
+
+ g_free (old_text);
+ g_free (new_text);
+
+ return handled;
+}
+
+/**
+ * hdy_search_bar_handle_event:
+ * @self: a #HdySearchBar
+ * @event: a #GdkEvent containing key press events
+ *
+ * This function should be called when the top-level
+ * window which contains the search bar received a key event.
+ *
+ * If the key event is handled by the search bar, the bar will
+ * be shown, the entry populated with the entered text and %GDK_EVENT_STOP
+ * will be returned. The caller should ensure that events are
+ * not propagated further.
+ *
+ * If no entry has been connected to the search bar, using
+ * hdy_search_bar_connect_entry(), this function will return
+ * immediately with a warning.
+ *
+ * ## Showing the search bar on key presses
+ *
+ * |[<!-- language="C" -->
+ * static gboolean
+ * on_key_press_event (GtkWidget *widget,
+ * GdkEvent *event,
+ * gpointer user_data)
+ * {
+ * HdySearchBar *bar = HDY_SEARCH_BAR (user_data);
+ * return hdy_search_bar_handle_event (self, event);
+ * }
+ *
+ * static void
+ * create_toplevel (void)
+ * {
+ * GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ * GtkWindow *search_bar = hdy_search_bar_new ();
+ *
+ * // Add more widgets to the window...
+ *
+ * g_signal_connect (window,
+ * "key-press-event",
+ * G_CALLBACK (on_key_press_event),
+ * search_bar);
+ * }
+ * ]|
+ *
+ * Returns: %GDK_EVENT_STOP if the key press event resulted
+ * in text being entered in the search entry (and revealing
+ * the search bar if necessary), %GDK_EVENT_PROPAGATE otherwise.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_handle_event (HdySearchBar *self,
+ GdkEvent *event)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean handled;
+
+ if (priv->reveal_child)
+ return GDK_EVENT_PROPAGATE;
+
+ if (priv->entry == NULL) {
+ g_warning ("The search bar does not have an entry connected to it. Call hdy_search_bar_connect_entry() to connect one.");
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ handled = gtk_search_entry_handle_event (GTK_SEARCH_ENTRY (priv->entry), event);
+ else
+ handled = hdy_search_bar_handle_event_for_entry (self, event);
+
+ if (handled == GDK_EVENT_STOP)
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), TRUE);
+
+ return handled;
+}
+
+static void
+reveal_child_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean reveal_child;
+
+ g_object_get (object, "reveal-child", &reveal_child, NULL);
+ if (reveal_child)
+ gtk_widget_set_child_visible (priv->revealer, TRUE);
+
+ if (reveal_child == priv->reveal_child)
+ return;
+
+ priv->reveal_child = reveal_child;
+
+ if (priv->entry) {
+ if (reveal_child)
+ gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->entry));
+ else
+ gtk_entry_set_text (GTK_ENTRY (priv->entry), "");
+ }
+
+ g_object_notify (G_OBJECT (self), "search-mode-enabled");
+}
+
+static void
+child_revealed_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean val;
+
+ g_object_get (object, "child-revealed", &val, NULL);
+ if (!val)
+ gtk_widget_set_child_visible (priv->revealer, FALSE);
+}
+
+static void
+close_button_clicked_cb (GtkWidget *button,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE);
+}
+
+static void
+hdy_search_bar_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (container);
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ if (priv->revealer == NULL) {
+ GTK_CONTAINER_CLASS (hdy_search_bar_parent_class)->add (container, child);
+ } else {
+ gtk_box_set_center_widget (GTK_BOX (priv->tool_box), child);
+ gtk_container_child_set (GTK_CONTAINER (priv->tool_box), child,
+ "expand", TRUE,
+ NULL);
+ /* If an entry is the only child, save the developer a couple of
+ * lines of code
+ */
+ if (GTK_IS_ENTRY (child))
+ hdy_search_bar_connect_entry (self, GTK_ENTRY (child));
+ }
+}
+
+static void
+hdy_search_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_MODE_ENABLED:
+ hdy_search_bar_set_search_mode (self, g_value_get_boolean (value));
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ hdy_search_bar_set_show_close_button (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_search_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_SEARCH_MODE_ENABLED:
+ g_value_set_boolean (value, priv->reveal_child);
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ g_value_set_boolean (value, hdy_search_bar_get_show_close_button (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void hdy_search_bar_set_entry (HdySearchBar *self,
+ GtkEntry *entry);
+
+static void
+hdy_search_bar_dispose (GObject *object)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+
+ hdy_search_bar_set_entry (self, NULL);
+
+ G_OBJECT_CLASS (hdy_search_bar_parent_class)->dispose (object);
+}
+
+static gboolean
+hdy_search_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ gint width, height;
+ GtkStyleContext *context;
+
+ width = gtk_widget_get_allocated_width (widget);
+ height = gtk_widget_get_allocated_height (widget);
+ context = gtk_widget_get_style_context (widget);
+
+ gtk_render_background (context, cr, 0, 0, width, height);
+ gtk_render_frame (context, cr, 0, 0, width, height);
+
+ GTK_WIDGET_CLASS (hdy_search_bar_parent_class)->draw (widget, cr);
+
+ return FALSE;
+}
+
+static void
+hdy_search_bar_class_init (HdySearchBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->dispose = hdy_search_bar_dispose;
+ object_class->set_property = hdy_search_bar_set_property;
+ object_class->get_property = hdy_search_bar_get_property;
+ widget_class->draw = hdy_search_bar_draw;
+
+ container_class->add = hdy_search_bar_add;
+
+ /**
+ * HdySearchBar:search-mode-enabled:
+ *
+ * Whether the search mode is on and the search bar shown.
+ *
+ * See hdy_search_bar_set_search_mode() for details.
+ */
+ props[PROP_SEARCH_MODE_ENABLED] =
+ g_param_spec_boolean ("search-mode-enabled",
+ _("Search Mode Enabled"),
+ _("Whether the search mode is on and the search bar shown"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySearchBar:show-close-button:
+ *
+ * Whether to show the close button in the toolbar.
+ */
+ props[PROP_SHOW_CLOSE_BUTTON] =
+ g_param_spec_boolean ("show-close-button",
+ _("Show Close Button"),
+ _("Whether to show the close button in the toolbar"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROPERTY, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-search-bar.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, tool_box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, revealer);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, start);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, end);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, close_button);
+
+ gtk_widget_class_set_css_name (widget_class, "searchbar");
+}
+
+static void
+hdy_search_bar_init (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* We use child-visible to avoid the unexpanded revealer
+ * peaking out by 1 pixel
+ */
+ gtk_widget_set_child_visible (priv->revealer, FALSE);
+
+ g_signal_connect (priv->revealer, "notify::reveal-child",
+ G_CALLBACK (reveal_child_changed_cb), self);
+ g_signal_connect (priv->revealer, "notify::child-revealed",
+ G_CALLBACK (child_revealed_changed_cb), self);
+
+ gtk_widget_set_no_show_all (priv->start, TRUE);
+ gtk_widget_set_no_show_all (priv->end, TRUE);
+ g_signal_connect (priv->close_button, "clicked",
+ G_CALLBACK (close_button_clicked_cb), self);
+};
+
+/**
+ * hdy_search_bar_new:
+ *
+ * Creates a #HdySearchBar. You will need to tell it about
+ * which widget is going to be your text entry using
+ * hdy_search_bar_connect_entry().
+ *
+ * Returns: a new #HdySearchBar
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_search_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_SEARCH_BAR, NULL);
+}
+
+static void
+hdy_search_bar_set_entry (HdySearchBar *self,
+ GtkEntry *entry)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ if (priv->entry != NULL) {
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ g_signal_handlers_disconnect_by_func (priv->entry, stop_search_cb, self);
+ else
+ g_signal_handlers_disconnect_by_func (priv->entry, entry_key_pressed_event_cb, self);
+ g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry);
+ }
+
+ priv->entry = GTK_WIDGET (entry);
+
+ if (priv->entry != NULL) {
+ g_object_add_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry);
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ g_signal_connect (priv->entry, "stop-search",
+ G_CALLBACK (stop_search_cb), self);
+ else
+ g_signal_connect (priv->entry, "key-press-event",
+ G_CALLBACK (entry_key_pressed_event_cb), self);
+ }
+}
+
+/**
+ * hdy_search_bar_connect_entry:
+ * @self: a #HdySearchBar
+ * @entry: a #GtkEntry
+ *
+ * Connects the #GtkEntry widget passed as the one to be used in
+ * this search bar. The entry should be a descendant of the search bar.
+ * This is only required if the entry isn’t the direct child of the
+ * search bar (as in our main example).
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_connect_entry (HdySearchBar *self,
+ GtkEntry *entry)
+{
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+ g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry));
+
+ hdy_search_bar_set_entry (self, entry);
+}
+
+/**
+ * hdy_search_bar_get_search_mode:
+ * @self: a #HdySearchBar
+ *
+ * Returns whether the search mode is on or off.
+ *
+ * Returns: whether search mode is toggled on
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_get_search_mode (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE);
+
+ return priv->reveal_child;
+}
+
+/**
+ * hdy_search_bar_set_search_mode:
+ * @self: a #HdySearchBar
+ * @search_mode: the new state of the search mode
+ *
+ * Switches the search mode on or off.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_set_search_mode (HdySearchBar *self,
+ gboolean search_mode)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), search_mode);
+}
+
+/**
+ * hdy_search_bar_get_show_close_button:
+ * @self: a #HdySearchBar
+ *
+ * Returns whether the close button is shown.
+ *
+ * Returns: whether the close button is shown
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_get_show_close_button (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE);
+
+ return priv->show_close_button;
+}
+
+/**
+ * hdy_search_bar_set_show_close_button:
+ * @self: a #HdySearchBar
+ * @visible: whether the close button will be shown or not
+ *
+ * Shows or hides the close button. Applications that
+ * already have a “search” toggle button should not show a close
+ * button in their search bar, as it duplicates the role of the
+ * toggle button.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_set_show_close_button (HdySearchBar *self,
+ gboolean visible)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+
+ visible = visible != FALSE;
+
+ if (priv->show_close_button == visible)
+ return;
+
+ priv->show_close_button = visible;
+ gtk_widget_set_visible (priv->start, visible);
+ gtk_widget_set_visible (priv->end, visible);
+ g_object_notify (G_OBJECT (self), "show-close-button");
+}
diff --git a/subprojects/libhandy/src/hdy-search-bar.h b/subprojects/libhandy/src/hdy-search-bar.h
new file mode 100644
index 0000000..fc6aa72
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SEARCH_BAR (hdy_search_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdySearchBar, hdy_search_bar, HDY, SEARCH_BAR, GtkBin)
+
+struct _HdySearchBarClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_search_bar_new (void);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_connect_entry (HdySearchBar *self,
+ GtkEntry *entry);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_get_search_mode (HdySearchBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_set_search_mode (HdySearchBar *self,
+ gboolean search_mode);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_get_show_close_button (HdySearchBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_set_show_close_button (HdySearchBar *self,
+ gboolean visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_handle_event (HdySearchBar *self,
+ GdkEvent *event);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-search-bar.ui b/subprojects/libhandy/src/hdy-search-bar.ui
new file mode 100644
index 0000000..5e79042
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdySearchBar" parent="GtkBin">
+ <child>
+ <object class="GtkRevealer" id="revealer">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkBox" id="tool_box">
+ <property name="visible">True</property>
+ <property name="border-width">6</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="start">
+ <property name="visible">False</property>
+ <property name="halign">start</property>
+ <property name="orientation">vertical</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="end">
+ <property name="visible">False</property>
+ <property name="halign">end</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButton" id="close_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">1</property>
+ <property name="relief">none</property>
+ <style>
+ <class name="close"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="close_image">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ <property name="icon-name">window-close-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="start"/>
+ <widget name="end"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-shadow-helper-private.h b/subprojects/libhandy/src/hdy-shadow-helper-private.h
new file mode 100644
index 0000000..4d96e11
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-shadow-helper-private.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SHADOW_HELPER (hdy_shadow_helper_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyShadowHelper, hdy_shadow_helper, HDY, SHADOW_HELPER, GObject)
+
+HdyShadowHelper *hdy_shadow_helper_new (GtkWidget *widget);
+
+void hdy_shadow_helper_clear_cache (HdyShadowHelper *self);
+
+void hdy_shadow_helper_draw_shadow (HdyShadowHelper *self,
+ cairo_t *cr,
+ gint width,
+ gint height,
+ gdouble progress,
+ GtkPanDirection direction);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-shadow-helper.c b/subprojects/libhandy/src/hdy-shadow-helper.c
new file mode 100644
index 0000000..929f04a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-shadow-helper.c
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-cairo-private.h"
+#include "hdy-shadow-helper-private.h"
+
+#include <math.h>
+
+/**
+ * PRIVATE:hdy-shadow-helper
+ * @short_description: Shadow helper used in #HdyLeaflet
+ * @title: HdyShadowHelper
+ * @See_also: #HdyLeaflet
+ * @stability: Private
+ *
+ * A helper class for drawing #HdyLeaflet transition shadow.
+ *
+ * Since: 0.0.12
+ */
+
+struct _HdyShadowHelper
+{
+ GObject parent_instance;
+
+ GtkWidget *widget;
+
+ gboolean is_cache_valid;
+
+ cairo_pattern_t *dimming_pattern;
+ cairo_pattern_t *shadow_pattern;
+ cairo_pattern_t *border_pattern;
+ cairo_pattern_t *outline_pattern;
+ gint shadow_size;
+ gint border_size;
+ gint outline_size;
+
+ GtkPanDirection last_direction;
+ gint last_width;
+ gint last_height;
+ gint last_scale;
+};
+
+G_DEFINE_TYPE (HdyShadowHelper, hdy_shadow_helper, G_TYPE_OBJECT);
+
+enum {
+ PROP_0,
+ PROP_WIDGET,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+
+static GtkStyleContext *
+create_context (HdyShadowHelper *self,
+ const gchar *name,
+ GtkPanDirection direction)
+{
+ g_autoptr(GtkWidgetPath) path = NULL;
+ GtkStyleContext *context;
+ gint pos;
+ const gchar *direction_name;
+ GEnumClass *enum_class;
+
+ enum_class = g_type_class_ref (GTK_TYPE_PAN_DIRECTION);
+ direction_name = g_enum_get_value (enum_class, direction)->value_nick;
+
+ path = gtk_widget_path_copy (gtk_widget_get_path (self->widget));
+
+ pos = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET);
+ gtk_widget_path_iter_set_object_name (path, pos, name);
+
+ gtk_widget_path_iter_add_class (path, pos, direction_name);
+
+ context = gtk_style_context_new ();
+ gtk_style_context_set_path (context, path);
+
+ g_type_class_unref (enum_class);
+
+ return context;
+}
+
+static gint
+get_element_size (GtkStyleContext *context,
+ GtkPanDirection direction)
+{
+ gint width, height;
+
+ gtk_style_context_get (context,
+ gtk_style_context_get_state (context),
+ "min-width", &width,
+ "min-height", &height,
+ NULL);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_RIGHT:
+ return width;
+ case GTK_PAN_DIRECTION_UP:
+ case GTK_PAN_DIRECTION_DOWN:
+ return height;
+ default:
+ g_assert_not_reached ();
+ }
+
+ return 0;
+}
+
+static cairo_pattern_t *
+create_element_pattern (GtkStyleContext *context,
+ gint width,
+ gint height)
+{
+ g_autoptr (cairo_surface_t) surface =
+ cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+ g_autoptr (cairo_t) cr = cairo_create (surface);
+ cairo_pattern_t *pattern;
+
+ gtk_render_background (context, cr, 0, 0, width, height);
+ gtk_render_frame (context, cr, 0, 0, width, height);
+
+ pattern = cairo_pattern_create_for_surface (surface);
+
+ return pattern;
+}
+
+static void
+cache_shadow (HdyShadowHelper *self,
+ gint width,
+ gint height,
+ GtkPanDirection direction)
+{
+ g_autoptr(GtkStyleContext) dim_context = NULL;
+ g_autoptr(GtkStyleContext) shadow_context = NULL;
+ g_autoptr(GtkStyleContext) border_context = NULL;
+ g_autoptr(GtkStyleContext) outline_context = NULL;
+ gint shadow_size, border_size, outline_size, scale;
+
+ scale = gtk_widget_get_scale_factor (self->widget);
+
+ if (self->last_direction == direction &&
+ self->last_width == width &&
+ self->last_height == height &&
+ self->last_scale == scale &&
+ self->is_cache_valid)
+ return;
+
+ hdy_shadow_helper_clear_cache (self);
+
+ dim_context = create_context (self, "dimming", direction);
+ shadow_context = create_context (self, "shadow", direction);
+ border_context = create_context (self, "border", direction);
+ outline_context = create_context (self, "outline", direction);
+
+ shadow_size = get_element_size (shadow_context, direction);
+ border_size = get_element_size (border_context, direction);
+ outline_size = get_element_size (outline_context, direction);
+
+ self->dimming_pattern = create_element_pattern (dim_context, width, height);
+ if (direction == GTK_PAN_DIRECTION_LEFT || direction == GTK_PAN_DIRECTION_RIGHT) {
+ self->shadow_pattern = create_element_pattern (shadow_context, shadow_size, height);
+ self->border_pattern = create_element_pattern (border_context, border_size, height);
+ self->outline_pattern = create_element_pattern (outline_context, outline_size, height);
+ } else {
+ self->shadow_pattern = create_element_pattern (shadow_context, width, shadow_size);
+ self->border_pattern = create_element_pattern (border_context, width, border_size);
+ self->outline_pattern = create_element_pattern (outline_context, width, outline_size);
+ }
+
+ self->border_size = border_size;
+ self->shadow_size = shadow_size;
+ self->outline_size = outline_size;
+
+ self->is_cache_valid = TRUE;
+ self->last_direction = direction;
+ self->last_width = width;
+ self->last_height = height;
+ self->last_scale = scale;
+}
+
+static void
+hdy_shadow_helper_dispose (GObject *object)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ hdy_shadow_helper_clear_cache (self);
+
+ if (self->widget)
+ g_clear_object (&self->widget);
+
+ G_OBJECT_CLASS (hdy_shadow_helper_parent_class)->dispose (object);
+}
+
+static void
+hdy_shadow_helper_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ switch (prop_id) {
+ case PROP_WIDGET:
+ g_value_set_object (value, self->widget);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_shadow_helper_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ switch (prop_id) {
+ case PROP_WIDGET:
+ self->widget = GTK_WIDGET (g_object_ref (g_value_get_object (value)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_shadow_helper_class_init (HdyShadowHelperClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_shadow_helper_dispose;
+ object_class->get_property = hdy_shadow_helper_get_property;
+ object_class->set_property = hdy_shadow_helper_set_property;
+
+ /**
+ * HdyShadowHelper:widget:
+ *
+ * The widget the shadow will be drawn for. Must not be %NULL
+ *
+ * Since: 0.0.11
+ */
+ props[PROP_WIDGET] =
+ g_param_spec_object ("widget",
+ _("Widget"),
+ _("The widget the shadow will be drawn for"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+hdy_shadow_helper_init (HdyShadowHelper *self)
+{
+}
+
+/**
+ * hdy_shadow_helper_new:
+ *
+ * Creates a new #HdyShadowHelper object.
+ *
+ * Returns: The newly created #HdyShadowHelper object
+ *
+ * Since: 0.0.12
+ */
+HdyShadowHelper *
+hdy_shadow_helper_new (GtkWidget *widget)
+{
+ return g_object_new (HDY_TYPE_SHADOW_HELPER,
+ "widget", widget,
+ NULL);
+}
+
+/**
+ * hdy_shadow_helper_clear_cache:
+ * @self: a #HdyShadowHelper
+ *
+ * Clears shadow cache. This should be used after a transition is done.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_shadow_helper_clear_cache (HdyShadowHelper *self)
+{
+ if (!self->is_cache_valid)
+ return;
+
+ cairo_pattern_destroy (self->dimming_pattern);
+ cairo_pattern_destroy (self->shadow_pattern);
+ cairo_pattern_destroy (self->border_pattern);
+ cairo_pattern_destroy (self->outline_pattern);
+ self->border_size = 0;
+ self->shadow_size = 0;
+ self->outline_size = 0;
+
+ self->last_direction = 0;
+ self->last_width = 0;
+ self->last_height = 0;
+ self->last_scale = 0;
+
+ self->is_cache_valid = FALSE;
+}
+
+/**
+ * hdy_shadow_helper_draw_shadow:
+ * @self: a #HdyShadowHelper
+ * @cr: a Cairo context to draw to
+ * @width: the width of the shadow rectangle
+ * @height: the height of the shadow rectangle
+ * @progress: transition progress, changes from 0 to 1
+ * @direction: shadow direction
+ *
+ * Draws a transition shadow. For caching to work, @width, @height and
+ * @direction shouldn't change between calls.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_shadow_helper_draw_shadow (HdyShadowHelper *self,
+ cairo_t *cr,
+ gint width,
+ gint height,
+ gdouble progress,
+ GtkPanDirection direction)
+{
+ gdouble remaining_distance, shadow_opacity;
+ gint shadow_size, border_size, outline_size, distance;
+
+ if (progress <= 0 || progress >= 1)
+ return;
+
+ cache_shadow (self, width, height, direction);
+
+ shadow_size = self->shadow_size;
+ border_size = self->border_size;
+ outline_size = self->outline_size;
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_RIGHT:
+ distance = width;
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ case GTK_PAN_DIRECTION_DOWN:
+ distance = height;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ remaining_distance = (1 - progress) * (gdouble) distance;
+ shadow_opacity = 1;
+ if (remaining_distance < shadow_size)
+ shadow_opacity = (remaining_distance / shadow_size);
+
+ cairo_save (cr);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ cairo_rectangle (cr, -outline_size, 0, width + outline_size, height);
+ break;
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_rectangle (cr, 0, 0, width + outline_size, height);
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ cairo_rectangle (cr, 0, -outline_size, width, height + outline_size);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_rectangle (cr, 0, 0, width, height + outline_size);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ cairo_clip (cr);
+ gdk_window_mark_paint_from_clip (gtk_widget_get_window (self->widget), cr);
+
+ cairo_set_source (cr, self->dimming_pattern);
+ cairo_paint_with_alpha (cr, 1 - progress);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, width - shadow_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, height - shadow_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_UP:
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->shadow_pattern);
+ cairo_paint_with_alpha (cr, shadow_opacity);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, shadow_size - border_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, shadow_size - border_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_UP:
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->border_pattern);
+ cairo_paint (cr);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, border_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, border_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ cairo_translate (cr, -outline_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ cairo_translate (cr, 0, -outline_size);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->outline_pattern);
+ cairo_paint (cr);
+
+ cairo_restore (cr);
+}
diff --git a/subprojects/libhandy/src/hdy-squeezer.c b/subprojects/libhandy/src/hdy-squeezer.c
new file mode 100644
index 0000000..1995661
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-squeezer.c
@@ -0,0 +1,1576 @@
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * Author: Alexander Larsson <alexl@redhat.com>
+ * Author: Adrien Plazas <adrien.plazas@puri.sm>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Forked from the GTK+ 3.24.2 GtkStack widget initially written by Alexander
+ * Larsson, and heavily modified for libhandy by Adrien Plazas on behalf of
+ * Purism SPC 2019.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-squeezer.h"
+
+#include "gtkprogresstrackerprivate.h"
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-css-private.h"
+
+/**
+ * SECTION:hdy-squeezer
+ * @short_description: A best fit container.
+ * @Title: HdySqueezer
+ *
+ * The HdySqueezer widget is a container which only shows the first of its
+ * children that fits in the available size. It is convenient to offer different
+ * widgets to represent the same data with different levels of detail, making
+ * the widget seem to squeeze itself to fit in the available space.
+ *
+ * Transitions between children can be animated as fades. This can be controlled
+ * with hdy_squeezer_set_transition_type().
+ *
+ * # CSS nodes
+ *
+ * #HdySqueezer has a single CSS node with name squeezer.
+ */
+
+/**
+ * HdySqueezerTransitionType:
+ * @HDY_SQUEEZER_TRANSITION_TYPE_NONE: No transition
+ * @HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE: A cross-fade
+ *
+ * These enumeration values describe the possible transitions between children
+ * in a #HdySqueezer widget.
+ */
+
+enum {
+ PROP_0,
+ PROP_HOMOGENEOUS,
+ PROP_VISIBLE_CHILD,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_TYPE,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_XALIGN,
+ PROP_YALIGN,
+
+ /* Overridden properties */
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_YALIGN + 1,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_ENABLED,
+
+ LAST_CHILD_PROP,
+};
+
+typedef struct {
+ GtkWidget *widget;
+ gboolean enabled;
+ GtkWidget *last_focus;
+} HdySqueezerChildInfo;
+
+struct _HdySqueezer
+{
+ GtkContainer parent_instance;
+
+ GList *children;
+
+ GdkWindow* bin_window;
+ GdkWindow* view_window;
+
+ HdySqueezerChildInfo *visible_child;
+
+ gboolean homogeneous;
+
+ HdySqueezerTransitionType transition_type;
+ guint transition_duration;
+
+ HdySqueezerChildInfo *last_visible_child;
+ cairo_surface_t *last_visible_surface;
+ GtkAllocation last_visible_surface_allocation;
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ gint last_visible_widget_width;
+ gint last_visible_widget_height;
+
+ HdySqueezerTransitionType active_transition_type;
+
+ gboolean interpolate_size;
+
+ gfloat xalign;
+ gfloat yalign;
+
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdySqueezer, hdy_squeezer, GTK_TYPE_CONTAINER,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static GtkOrientation
+get_orientation (HdySqueezer *self)
+{
+ return self->orientation;
+}
+
+static void
+set_orientation (HdySqueezer *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static HdySqueezerChildInfo *
+find_child_info_for_widget (HdySqueezer *self,
+ GtkWidget *child)
+{
+ HdySqueezerChildInfo *info;
+ GList *l;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ if (info->widget == child)
+ return info;
+ }
+
+ return NULL;
+}
+
+static void
+hdy_squeezer_progress_updated (HdySqueezer *self)
+{
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ if (!self->homogeneous)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (self->last_visible_surface != NULL) {
+ cairo_surface_destroy (self->last_visible_surface);
+ self->last_visible_surface = NULL;
+ }
+
+ if (self->last_visible_child != NULL) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+ }
+}
+
+static gboolean
+hdy_squeezer_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ if (self->first_frame_skipped) {
+ gtk_progress_tracker_advance_frame (&self->tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ } else {
+ self->first_frame_skipped = TRUE;
+ }
+
+ /* Finish the animation early if the widget isn't mapped anymore. */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&self->tracker);
+
+ hdy_squeezer_progress_updated (HDY_SQUEEZER (widget));
+
+ if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_squeezer_schedule_ticks (HdySqueezer *self)
+{
+ if (self->tick_id == 0) {
+ self->tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_squeezer_transition_cb, self, NULL);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_squeezer_unschedule_ticks (HdySqueezer *self)
+{
+ if (self->tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_id);
+ self->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_squeezer_start_transition (HdySqueezer *self,
+ HdySqueezerTransitionType transition_type,
+ guint transition_duration)
+{
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ hdy_get_enable_animations (widget) &&
+ transition_type != HDY_SQUEEZER_TRANSITION_TYPE_NONE &&
+ transition_duration != 0 &&
+ self->last_visible_child != NULL) {
+ self->active_transition_type = transition_type;
+ self->first_frame_skipped = FALSE;
+ hdy_squeezer_schedule_ticks (self);
+ gtk_progress_tracker_start (&self->tracker,
+ self->transition_duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ hdy_squeezer_unschedule_ticks (self);
+ self->active_transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE;
+ gtk_progress_tracker_finish (&self->tracker);
+ }
+
+ hdy_squeezer_progress_updated (HDY_SQUEEZER (widget));
+}
+
+static void
+set_visible_child (HdySqueezer *self,
+ HdySqueezerChildInfo *child_info,
+ HdySqueezerTransitionType transition_type,
+ guint transition_duration)
+{
+ HdySqueezerChildInfo *info;
+ GtkWidget *widget = GTK_WIDGET (self);
+ GList *l;
+ GtkWidget *toplevel;
+ GtkWidget *focus;
+ gboolean contains_focus = FALSE;
+
+ /* If we are being destroyed, do not bother with transitions and
+ * notifications.
+ */
+ if (gtk_widget_in_destruction (widget))
+ return;
+
+ /* If none, pick the first visible. */
+ if (child_info == NULL) {
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ if (gtk_widget_get_visible (info->widget)) {
+ child_info = info;
+ break;
+ }
+ }
+ }
+
+ if (child_info == self->visible_child)
+ return;
+
+ toplevel = gtk_widget_get_toplevel (widget);
+ if (GTK_IS_WINDOW (toplevel)) {
+ focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+ if (focus &&
+ self->visible_child &&
+ self->visible_child->widget &&
+ gtk_widget_is_ancestor (focus, self->visible_child->widget)) {
+ contains_focus = TRUE;
+
+ if (self->visible_child->last_focus)
+ g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus),
+ (gpointer *)&self->visible_child->last_focus);
+ self->visible_child->last_focus = focus;
+ g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus),
+ (gpointer *)&self->visible_child->last_focus);
+ }
+ }
+
+ if (self->last_visible_child != NULL)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+
+ if (self->last_visible_surface != NULL)
+ cairo_surface_destroy (self->last_visible_surface);
+ self->last_visible_surface = NULL;
+
+ if (self->visible_child && self->visible_child->widget) {
+ if (gtk_widget_is_visible (widget)) {
+ GtkAllocation allocation;
+
+ self->last_visible_child = self->visible_child;
+ gtk_widget_get_allocated_size (self->last_visible_child->widget, &allocation, NULL);
+ self->last_visible_widget_width = allocation.width;
+ self->last_visible_widget_height = allocation.height;
+ } else {
+ gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
+ }
+ }
+
+ self->visible_child = child_info;
+
+ if (child_info) {
+ gtk_widget_set_child_visible (child_info->widget, TRUE);
+
+ if (contains_focus) {
+ if (child_info->last_focus)
+ gtk_widget_grab_focus (child_info->last_focus);
+ else
+ gtk_widget_child_focus (child_info->widget, GTK_DIR_TAB_FORWARD);
+ }
+ }
+
+ if (self->homogeneous)
+ gtk_widget_queue_allocate (widget);
+ else
+ gtk_widget_queue_resize (widget);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+
+ hdy_squeezer_start_transition (self, transition_type, transition_duration);
+}
+
+static void
+stack_child_visibility_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (user_data);
+ GtkWidget *child = GTK_WIDGET (obj);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, child);
+
+ if (self->visible_child == NULL &&
+ gtk_widget_get_visible (child))
+ set_visible_child (self, child_info, self->transition_type, self->transition_duration);
+ else if (self->visible_child == child_info &&
+ !gtk_widget_get_visible (child))
+ set_visible_child (self, NULL, self->transition_type, self->transition_duration);
+
+ if (child_info == self->last_visible_child) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+}
+
+static void
+hdy_squeezer_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ g_return_if_fail (child != NULL);
+
+ child_info = g_slice_new (HdySqueezerChildInfo);
+ child_info->widget = child;
+ child_info->enabled = TRUE;
+ child_info->last_focus = NULL;
+
+ self->children = g_list_append (self->children, child_info);
+
+ gtk_widget_set_child_visible (child, FALSE);
+ gtk_widget_set_parent_window (child, self->bin_window);
+ gtk_widget_set_parent (child, GTK_WIDGET (self));
+
+ if (self->bin_window != NULL) {
+ gdk_window_set_events (self->bin_window,
+ gdk_window_get_events (self->bin_window) |
+ gtk_widget_get_events (child));
+ }
+
+ g_signal_connect (child, "notify::visible",
+ G_CALLBACK (stack_child_visibility_notify_cb), self);
+
+ if (self->visible_child == NULL &&
+ gtk_widget_get_visible (child))
+ set_visible_child (self, child_info, self->transition_type, self->transition_duration);
+
+ if (self->visible_child == child_info)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_squeezer_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+ gboolean was_visible;
+
+ child_info = find_child_info_for_widget (self, child);
+ if (child_info == NULL)
+ return;
+
+ self->children = g_list_remove (self->children, child_info);
+
+ g_signal_handlers_disconnect_by_func (child,
+ stack_child_visibility_notify_cb,
+ self);
+
+ was_visible = gtk_widget_get_visible (child);
+
+ child_info->widget = NULL;
+
+ if (self->visible_child == child_info)
+ set_visible_child (self, NULL, self->transition_type, self->transition_duration);
+
+ if (self->last_visible_child == child_info)
+ self->last_visible_child = NULL;
+
+ gtk_widget_unparent (child);
+
+ if (child_info->last_focus)
+ g_object_remove_weak_pointer (G_OBJECT (child_info->last_focus),
+ (gpointer *)&child_info->last_focus);
+
+ g_slice_free (HdySqueezerChildInfo, child_info);
+
+ if (self->homogeneous && was_visible)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_squeezer_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ switch (property_id) {
+ case PROP_HOMOGENEOUS:
+ g_value_set_boolean (value, hdy_squeezer_get_homogeneous (self));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_squeezer_get_visible_child (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_squeezer_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_squeezer_get_transition_type (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_squeezer_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_squeezer_get_interpolate_size (self));
+ break;
+ case PROP_XALIGN:
+ g_value_set_float (value, hdy_squeezer_get_xalign (self));
+ break;
+ case PROP_YALIGN:
+ g_value_set_float (value, hdy_squeezer_get_yalign (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ switch (property_id) {
+ case PROP_HOMOGENEOUS:
+ hdy_squeezer_set_homogeneous (self, g_value_get_boolean (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_squeezer_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_squeezer_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_squeezer_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_XALIGN:
+ hdy_squeezer_set_xalign (self, g_value_get_float (value));
+ break;
+ case PROP_YALIGN:
+ hdy_squeezer_set_yalign (self, g_value_get_float (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_realize (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+ HdySqueezerChildInfo *info;
+ GList *l;
+
+ gtk_widget_set_realized (widget, TRUE);
+ gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget)));
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask =
+ gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ self->view_window =
+ gdk_window_new (gtk_widget_get_window (GTK_WIDGET (self)),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->view_window);
+
+ attributes.x = 0;
+ attributes.y = 0;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ attributes.event_mask |= gtk_widget_get_events (info->widget);
+ }
+
+ self->bin_window =
+ gdk_window_new (self->view_window, &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->bin_window);
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+
+ gtk_widget_set_parent_window (info->widget, self->bin_window);
+ }
+
+ gdk_window_show (self->bin_window);
+}
+
+static void
+hdy_squeezer_unrealize (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ gtk_widget_unregister_window (widget, self->bin_window);
+ gdk_window_destroy (self->bin_window);
+ self->bin_window = NULL;
+ gtk_widget_unregister_window (widget, self->view_window);
+ gdk_window_destroy (self->view_window);
+ self->view_window = NULL;
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_squeezer_map (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->map (widget);
+
+ gdk_window_show (self->view_window);
+}
+
+static void
+hdy_squeezer_unmap (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ gdk_window_hide (self->view_window);
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unmap (widget);
+}
+
+static void
+hdy_squeezer_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+ GList *l;
+
+ l = self->children;
+ while (l) {
+ child_info = l->data;
+ l = l->next;
+
+ (* callback) (child_info->widget, callback_data);
+ }
+}
+
+static void
+hdy_squeezer_compute_expand (GtkWidget *widget,
+ gboolean *hexpand_p,
+ gboolean *vexpand_p)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ gboolean hexpand, vexpand;
+ HdySqueezerChildInfo *child_info;
+ GtkWidget *child;
+ GList *l;
+
+ hexpand = FALSE;
+ vexpand = FALSE;
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (!hexpand &&
+ gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL))
+ hexpand = TRUE;
+
+ if (!vexpand &&
+ gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL))
+ vexpand = TRUE;
+
+ if (hexpand && vexpand)
+ break;
+ }
+
+ *hexpand_p = hexpand;
+ *vexpand_p = vexpand;
+}
+
+static void
+hdy_squeezer_draw_crossfade (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ gdouble progress = gtk_progress_tracker_get_progress (&self->tracker, FALSE);
+
+ cairo_push_group (cr);
+ gtk_container_propagate_draw (GTK_CONTAINER (self),
+ self->visible_child->widget,
+ cr);
+ cairo_save (cr);
+
+ /* Multiply alpha by progress. */
+ cairo_set_source_rgba (cr, 1, 1, 1, progress);
+ cairo_set_operator (cr, CAIRO_OPERATOR_DEST_IN);
+ cairo_paint (cr);
+
+ if (self->last_visible_surface != NULL) {
+ gint width_diff = gtk_widget_get_allocated_width (widget) - self->last_visible_surface_allocation.width;
+ gint height_diff = gtk_widget_get_allocated_height (widget) - self->last_visible_surface_allocation.height;
+
+ cairo_set_source_surface (cr, self->last_visible_surface,
+ width_diff * self->xalign,
+ height_diff * self->yalign);
+ cairo_set_operator (cr, CAIRO_OPERATOR_ADD);
+ cairo_paint_with_alpha (cr, MAX (1.0 - progress, 0));
+ }
+
+ cairo_restore (cr);
+
+ cairo_pop_group_to_source (cr);
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+ cairo_paint (cr);
+}
+
+static gboolean
+hdy_squeezer_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ if (gtk_cairo_should_draw_window (cr, self->view_window)) {
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ }
+
+ if (self->visible_child) {
+ if (gtk_progress_tracker_get_state (&self->tracker) != GTK_PROGRESS_STATE_AFTER) {
+ if (self->last_visible_surface == NULL &&
+ self->last_visible_child != NULL) {
+ g_autoptr (cairo_t) pattern_cr = NULL;
+
+ gtk_widget_get_allocation (self->last_visible_child->widget,
+ &self->last_visible_surface_allocation);
+ self->last_visible_surface =
+ gdk_window_create_similar_surface (gtk_widget_get_window (widget),
+ CAIRO_CONTENT_COLOR_ALPHA,
+ self->last_visible_surface_allocation.width,
+ self->last_visible_surface_allocation.height);
+ pattern_cr = cairo_create (self->last_visible_surface);
+ /* We don't use propagate_draw here, because we don't want to apply the
+ * bin_window offset.
+ */
+ gtk_widget_draw (self->last_visible_child->widget, pattern_cr);
+ }
+
+ cairo_rectangle (cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ cairo_clip (cr);
+
+ switch (self->active_transition_type) {
+ case HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE:
+ if (gtk_cairo_should_draw_window (cr, self->bin_window))
+ hdy_squeezer_draw_crossfade (widget, cr);
+ break;
+ case HDY_SQUEEZER_TRANSITION_TYPE_NONE:
+ default:
+ g_assert_not_reached ();
+ }
+
+ } else if (gtk_cairo_should_draw_window (cr, self->bin_window))
+ gtk_container_propagate_draw (GTK_CONTAINER (self),
+ self->visible_child->widget,
+ cr);
+ }
+
+ return FALSE;
+}
+
+static void
+hdy_squeezer_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ HdySqueezerChildInfo *child_info = NULL;
+ GtkWidget *child = NULL;
+ gint child_min;
+ GList *l;
+ GtkAllocation child_allocation;
+
+ hdy_css_size_allocate (widget, allocation);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ if (!child_info->enabled)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL) {
+ if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
+ gtk_widget_get_preferred_height (child, &child_min, NULL);
+ else
+ gtk_widget_get_preferred_height_for_width (child, allocation->width, &child_min, NULL);
+
+ if (child_min <= allocation->height)
+ break;
+ } else {
+ if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT)
+ gtk_widget_get_preferred_width (child, &child_min, NULL);
+ else
+ gtk_widget_get_preferred_width_for_height (child, allocation->height, &child_min, NULL);
+
+ if (child_min <= allocation->width)
+ break;
+ }
+ }
+
+ set_visible_child (self, child_info,
+ self->transition_type,
+ self->transition_duration);
+
+ child_allocation.x = 0;
+ child_allocation.y = 0;
+
+ if (gtk_widget_get_realized (widget)) {
+ gdk_window_move_resize (self->view_window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+ gdk_window_move_resize (self->bin_window,
+ 0, 0,
+ allocation->width, allocation->height);
+ }
+
+ if (self->last_visible_child != NULL) {
+ int min, nat;
+ gtk_widget_get_preferred_width (self->last_visible_child->widget, &min, &nat);
+ child_allocation.width = MAX (min, allocation->width);
+ gtk_widget_get_preferred_height_for_width (self->last_visible_child->widget,
+ child_allocation.width,
+ &min, &nat);
+ child_allocation.height = MAX (min, allocation->height);
+
+ gtk_widget_size_allocate (self->last_visible_child->widget, &child_allocation);
+ }
+
+ child_allocation.width = allocation->width;
+ child_allocation.height = allocation->height;
+
+ if (self->visible_child) {
+ int min, nat;
+ GtkAlign valign;
+
+ gtk_widget_get_preferred_height_for_width (self->visible_child->widget,
+ allocation->width,
+ &min, &nat);
+ if (self->interpolate_size) {
+ valign = gtk_widget_get_valign (self->visible_child->widget);
+ child_allocation.height = MAX (nat, allocation->height);
+ if (valign == GTK_ALIGN_END &&
+ child_allocation.height > allocation->height)
+ child_allocation.y -= nat - allocation->height;
+ else if (valign == GTK_ALIGN_CENTER &&
+ child_allocation.height > allocation->height)
+ child_allocation.y -= (nat - allocation->height) / 2;
+ }
+
+ gtk_widget_size_allocate (self->visible_child->widget, &child_allocation);
+ }
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_squeezer_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ HdySqueezerChildInfo *child_info;
+ GtkWidget *child;
+ gint child_min, child_nat;
+ GList *l;
+
+ *minimum = 0;
+ *natural = 0;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (self->orientation != orientation && !self->homogeneous &&
+ self->visible_child != child_info)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ /* Disabled children are taken into account when measuring the widget, to
+ * keep its size request and allocation consistent. This avoids the
+ * appearant size and position of a child to changes suddenly when a larger
+ * child gets enabled/disabled.
+ */
+
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ if (for_size < 0)
+ gtk_widget_get_preferred_height (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat);
+ } else {
+ if (for_size < 0)
+ gtk_widget_get_preferred_width (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat);
+ }
+
+ if (self->orientation == orientation)
+ *minimum = *minimum == 0 ? child_min : MIN (*minimum, child_min);
+ else
+ *minimum = MAX (*minimum, child_min);
+ *natural = MAX (*natural, child_nat);
+ }
+
+ if (self->orientation != orientation && !self->homogeneous &&
+ self->interpolate_size &&
+ self->last_visible_child != NULL) {
+ gdouble t = gtk_progress_tracker_get_ease_out_cubic (&self->tracker, FALSE);
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ *minimum = hdy_lerp (self->last_visible_widget_height, *minimum, t);
+ *natural = hdy_lerp (self->last_visible_widget_height, *natural, t);
+ } else {
+ *minimum = hdy_lerp (self->last_visible_widget_width, *minimum, t);
+ *natural = hdy_lerp (self->last_visible_widget_width, *natural, t);
+ }
+ }
+
+ hdy_css_measure (widget, orientation, minimum, natural);
+}
+
+static void
+hdy_squeezer_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+ if (child_info == NULL) {
+ g_param_value_set_default (pspec, value);
+
+ return;
+ }
+
+ switch (property_id) {
+ case CHILD_PROP_ENABLED:
+ g_value_set_boolean (value, hdy_squeezer_get_child_enabled (self, widget));
+ break;
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+ if (child_info == NULL)
+ return;
+
+ switch (property_id) {
+ case CHILD_PROP_ENABLED:
+ hdy_squeezer_set_child_enabled (self, widget, g_value_get_boolean (value));
+ break;
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_dispose (GObject *object)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ self->visible_child = NULL;
+
+ G_OBJECT_CLASS (hdy_squeezer_parent_class)->dispose (object);
+}
+
+static void
+hdy_squeezer_finalize (GObject *object)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ hdy_squeezer_unschedule_ticks (self);
+
+ if (self->last_visible_surface != NULL)
+ cairo_surface_destroy (self->last_visible_surface);
+
+ G_OBJECT_CLASS (hdy_squeezer_parent_class)->finalize (object);
+}
+
+static void
+hdy_squeezer_class_init (HdySqueezerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_squeezer_get_property;
+ object_class->set_property = hdy_squeezer_set_property;
+ object_class->dispose = hdy_squeezer_dispose;
+ object_class->finalize = hdy_squeezer_finalize;
+
+ widget_class->size_allocate = hdy_squeezer_size_allocate;
+ widget_class->draw = hdy_squeezer_draw;
+ widget_class->realize = hdy_squeezer_realize;
+ widget_class->unrealize = hdy_squeezer_unrealize;
+ widget_class->map = hdy_squeezer_map;
+ widget_class->unmap = hdy_squeezer_unmap;
+ widget_class->get_preferred_height = hdy_squeezer_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_squeezer_get_preferred_height_for_width;
+ widget_class->get_preferred_width = hdy_squeezer_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_squeezer_get_preferred_width_for_height;
+ widget_class->compute_expand = hdy_squeezer_compute_expand;
+
+ container_class->add = hdy_squeezer_add;
+ container_class->remove = hdy_squeezer_remove;
+ container_class->forall = hdy_squeezer_forall;
+ container_class->set_child_property = hdy_squeezer_set_child_property;
+ container_class->get_child_property = hdy_squeezer_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ props[PROP_HOMOGENEOUS] =
+ g_param_spec_boolean ("homogeneous",
+ _("Homogeneous"),
+ _("Homogeneous sizing"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible in the squeezer"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition"),
+ HDY_TYPE_SQUEEZER_TRANSITION_TYPE,
+ HDY_SQUEEZER_TRANSITION_TYPE_NONE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySqueezer:xalign:
+ *
+ * The xalign property determines the horizontal aligment of the children
+ * inside the squeezer's size allocation.
+ * Compare this to #GtkWidget:halign, which determines how the squeezer's size
+ * allocation is positioned in the space available for the squeezer.
+ * The range goes from 0 (start) to 1 (end).
+ *
+ * This will affect the position of children too wide to fit in the squeezer
+ * as they are fading out.
+ *
+ * Since: 1.0
+ */
+ props[PROP_XALIGN] =
+ g_param_spec_float ("xalign",
+ _("X align"),
+ _("The horizontal alignment, from 0 (start) to 1 (end)"),
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySqueezer:yalign:
+ *
+ * The yalign property determines the vertical aligment of the children inside
+ * the squeezer's size allocation.
+ * Compare this to #GtkWidget:valign, which determines how the squeezer's size
+ * allocation is positioned in the space available for the squeezer.
+ * The range goes from 0 (top) to 1 (bottom).
+ *
+ * This will affect the position of children too tall to fit in the squeezer
+ * as they are fading out.
+ *
+ * Since: 1.0
+ */
+ props[PROP_YALIGN] =
+ g_param_spec_float ("yalign",
+ _("Y align"),
+ _("The vertical alignment, from 0 (top) to 1 (bottom)"),
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_ENABLED] =
+ g_param_spec_boolean ("enabled",
+ _("Enabled"),
+ _("Whether the child can be picked or should be ignored when looking for the child fitting the available size best"),
+ TRUE,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_css_name (widget_class, "squeezer");
+}
+
+static void
+hdy_squeezer_init (HdySqueezer *self)
+{
+
+ gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);
+
+ self->homogeneous = TRUE;
+ self->transition_duration = 200;
+ self->transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE;
+ self->xalign = 0.5;
+ self->yalign = 0.5;
+}
+
+/**
+ * hdy_squeezer_new:
+ *
+ * Creates a new #HdySqueezer container.
+ *
+ * Returns: a new #HdySqueezer
+ */
+GtkWidget *
+hdy_squeezer_new (void)
+{
+ return g_object_new (HDY_TYPE_SQUEEZER, NULL);
+}
+
+/**
+ * hdy_squeezer_get_homogeneous:
+ * @self: a #HdySqueezer
+ *
+ * Gets whether @self is homogeneous.
+ *
+ * See hdy_squeezer_set_homogeneous().
+ *
+ * Returns: %TRUE if @self is homogeneous, %FALSE is not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_squeezer_get_homogeneous (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return self->homogeneous;
+}
+
+/**
+ * hdy_squeezer_set_homogeneous:
+ * @self: a #HdySqueezer
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets @self to be homogeneous or not. If it is homogeneous, @self will request
+ * the same size for all its children for its opposite orientation, e.g. if
+ * @self is oriented horizontally and is homogeneous, it will request the same
+ * height for all its children. If it isn't, @self may change size when a
+ * different child becomes visible.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_squeezer_set_homogeneous (HdySqueezer *self,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ homogeneous = !!homogeneous;
+
+ if (self->homogeneous == homogeneous)
+ return;
+
+ self->homogeneous = homogeneous;
+
+ if (gtk_widget_get_visible (GTK_WIDGET(self)))
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOMOGENEOUS]);
+}
+
+/**
+ * hdy_squeezer_get_transition_duration:
+ * @self: a #HdySqueezer
+ *
+ * Gets the amount of time (in milliseconds) that transitions between children
+ * in @self will take.
+ *
+ * Returns: the transition duration
+ */
+guint
+hdy_squeezer_get_transition_duration (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0);
+
+ return self->transition_duration;
+}
+
+/**
+ * hdy_squeezer_set_transition_duration:
+ * @self: a #HdySqueezer
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self will take.
+ */
+void
+hdy_squeezer_set_transition_duration (HdySqueezer *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ if (self->transition_duration == duration)
+ return;
+
+ self->transition_duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_squeezer_get_transition_type:
+ * @self: a #HdySqueezer
+ *
+ * Gets the type of animation that will be used for transitions between children
+ * in @self.
+ *
+ * Returns: the current transition type of @self
+ */
+HdySqueezerTransitionType
+hdy_squeezer_get_transition_type (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), HDY_SQUEEZER_TRANSITION_TYPE_NONE);
+
+ return self->transition_type;
+}
+
+/**
+ * hdy_squeezer_set_transition_type:
+ * @self: a #HdySqueezer
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between children
+ * in @self. Available types include various kinds of fades and slides.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about to become
+ * current.
+ */
+void
+hdy_squeezer_set_transition_type (HdySqueezer *self,
+ HdySqueezerTransitionType transition)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ if (self->transition_type == transition)
+ return;
+
+ self->transition_type = transition;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_TYPE]);
+}
+
+/**
+ * hdy_squeezer_get_transition_running:
+ * @self: a #HdySqueezer
+ *
+ * Gets whether @self is currently in a transition from one child to another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ */
+gboolean
+hdy_squeezer_get_transition_running (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return (self->tick_id != 0);
+}
+
+/**
+ * hdy_squeezer_get_interpolate_size:
+ * @self: A #HdySqueezer
+ *
+ * Gets whether @self should interpolate its size on visible child change.
+ *
+ * See hdy_squeezer_set_interpolate_size().
+ *
+ * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_squeezer_get_interpolate_size (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return self->interpolate_size;
+}
+
+/**
+ * hdy_squeezer_set_interpolate_size:
+ * @self: A #HdySqueezer
+ * @interpolate_size: %TRUE to interpolate the size
+ *
+ * Sets whether or not @self will interpolate the size of its opposing
+ * orientation when changing the visible child. If %TRUE, @self will interpolate
+ * its size between the one of the previous visible child and the one of the new
+ * visible child, according to the set transition duration and the orientation,
+ * e.g. if @self is horizontal, it will interpolate the its height.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_squeezer_set_interpolate_size (HdySqueezer *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ interpolate_size = !!interpolate_size;
+
+ if (self->interpolate_size == interpolate_size)
+ return;
+
+ self->interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
+
+/**
+ * hdy_squeezer_get_visible_child:
+ * @self: a #HdySqueezer
+ *
+ * Gets the currently visible child of @self, or %NULL if there are no visible
+ * children.
+ *
+ * Returns: (transfer none) (nullable): the visible child of the #HdySqueezer
+ */
+GtkWidget *
+hdy_squeezer_get_visible_child (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), NULL);
+
+ return self->visible_child ? self->visible_child->widget : NULL;
+}
+
+/**
+ * hdy_squeezer_get_child_enabled:
+ * @self: a #HdySqueezer
+ * @child: a child of @self
+ *
+ * Gets whether @child is enabled.
+ *
+ * See hdy_squeezer_set_child_enabled().
+ *
+ * Returns: %TRUE if @child is enabled, %FALSE otherwise.
+ */
+gboolean
+hdy_squeezer_get_child_enabled (HdySqueezer *self,
+ GtkWidget *child)
+{
+ HdySqueezerChildInfo *child_info;
+
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+ g_return_val_if_fail (GTK_IS_WIDGET (child), FALSE);
+
+ child_info = find_child_info_for_widget (self, child);
+
+ g_return_val_if_fail (child_info != NULL, FALSE);
+
+ return child_info->enabled;
+}
+
+/**
+ * hdy_squeezer_set_child_enabled:
+ * @self: a #HdySqueezer
+ * @child: a child of @self
+ * @enabled: %TRUE to enable the child, %FALSE to disable it
+ *
+ * Make @self enable or disable @child. If a child is disabled, it will be
+ * ignored when looking for the child fitting the available size best. This
+ * allows to programmatically and prematurely hide a child of @self even if it
+ * fits in the available space.
+ *
+ * This can be used e.g. to ensure a certain child is hidden below a certain
+ * window width, or any other constraint you find suitable.
+ */
+void
+hdy_squeezer_set_child_enabled (HdySqueezer *self,
+ GtkWidget *child,
+ gboolean enabled)
+{
+ HdySqueezerChildInfo *child_info;
+
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+
+ child_info = find_child_info_for_widget (self, child);
+
+ g_return_if_fail (child_info != NULL);
+
+ enabled = !!enabled;
+
+ if (child_info->enabled == enabled)
+ return;
+
+ child_info->enabled = enabled;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_squeezer_get_xalign:
+ * @self: a #HdySqueezer
+ *
+ * Gets the #HdySqueezer:xalign property for @self.
+ *
+ * Returns: the xalign property
+ *
+ * Since: 1.0
+ */
+gfloat
+hdy_squeezer_get_xalign (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5);
+
+ return self->xalign;
+}
+
+/**
+ * hdy_squeezer_set_xalign:
+ * @self: a #HdySqueezer
+ * @xalign: the new xalign value, between 0 and 1
+ *
+ * Sets the #HdySqueezer:xalign property for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_squeezer_set_xalign (HdySqueezer *self,
+ gfloat xalign)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ xalign = CLAMP (xalign, 0.0, 1.0);
+
+ if (self->xalign == xalign)
+ return;
+
+ self->xalign = xalign;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_XALIGN]);
+}
+
+/**
+ * hdy_squeezer_get_yalign:
+ * @self: a #HdySqueezer
+ *
+ * Gets the #HdySqueezer:yalign property for @self.
+ *
+ * Returns: the yalign property
+ *
+ * Since: 1.0
+ */
+gfloat
+hdy_squeezer_get_yalign (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5);
+
+ return self->yalign;
+}
+
+/**
+ * hdy_squeezer_set_yalign:
+ * @self: a #HdySqueezer
+ * @yalign: the new yalign value, between 0 and 1
+ *
+ * Sets the #HdySqueezer:yalign property for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_squeezer_set_yalign (HdySqueezer *self,
+ gfloat yalign)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ yalign = CLAMP (yalign, 0.0, 1.0);
+
+ if (self->yalign == yalign)
+ return;
+
+ self->yalign = yalign;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_YALIGN]);
+}
diff --git a/subprojects/libhandy/src/hdy-squeezer.h b/subprojects/libhandy/src/hdy-squeezer.h
new file mode 100644
index 0000000..9b98116
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-squeezer.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SQUEEZER (hdy_squeezer_get_type ())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySqueezer, hdy_squeezer, HDY, SQUEEZER, GtkContainer)
+
+typedef enum {
+ HDY_SQUEEZER_TRANSITION_TYPE_NONE,
+ HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE,
+} HdySqueezerTransitionType;
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_squeezer_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_homogeneous (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_homogeneous (HdySqueezer *self,
+ gboolean homogeneous);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_squeezer_get_transition_duration (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_transition_duration (HdySqueezer *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+HdySqueezerTransitionType hdy_squeezer_get_transition_type (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_transition_type (HdySqueezer *self,
+ HdySqueezerTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_transition_running (HdySqueezer *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_interpolate_size (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_interpolate_size (HdySqueezer *self,
+ gboolean interpolate_size);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_squeezer_get_visible_child (HdySqueezer *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_child_enabled (HdySqueezer *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_child_enabled (HdySqueezer *self,
+ GtkWidget *child,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gfloat hdy_squeezer_get_xalign (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_xalign (HdySqueezer *self,
+ gfloat xalign);
+
+HDY_AVAILABLE_IN_ALL
+gfloat hdy_squeezer_get_yalign (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_yalign (HdySqueezer *self,
+ gfloat yalign);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-stackable-box-private.h b/subprojects/libhandy/src/hdy-stackable-box-private.h
new file mode 100644
index 0000000..d72c75a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-stackable-box-private.h
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_STACKABLE_BOX (hdy_stackable_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyStackableBox, hdy_stackable_box, HDY, STACKABLE_BOX, GObject)
+
+typedef enum {
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER,
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER,
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE,
+} HdyStackableBoxTransitionType;
+
+HdyStackableBox *hdy_stackable_box_new (GtkContainer *container,
+ GtkContainerClass *klass,
+ gboolean can_unfold);
+gboolean hdy_stackable_box_get_folded (HdyStackableBox *self);
+GtkWidget *hdy_stackable_box_get_visible_child (HdyStackableBox *self);
+void hdy_stackable_box_set_visible_child (HdyStackableBox *self,
+ GtkWidget *visible_child);
+const gchar *hdy_stackable_box_get_visible_child_name (HdyStackableBox *self);
+void hdy_stackable_box_set_visible_child_name (HdyStackableBox *self,
+ const gchar *name);
+gboolean hdy_stackable_box_get_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation);
+void hdy_stackable_box_set_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HdyStackableBoxTransitionType hdy_stackable_box_get_transition_type (HdyStackableBox *self);
+void hdy_stackable_box_set_transition_type (HdyStackableBox *self,
+ HdyStackableBoxTransitionType transition);
+
+guint hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self);
+void hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self,
+ guint duration);
+
+guint hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self);
+void hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self,
+ guint duration);
+gboolean hdy_stackable_box_get_child_transition_running (HdyStackableBox *self);
+gboolean hdy_stackable_box_get_interpolate_size (HdyStackableBox *self);
+void hdy_stackable_box_set_interpolate_size (HdyStackableBox *self,
+ gboolean interpolate_size);
+gboolean hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self);
+void hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self,
+ gboolean can_swipe_back);
+gboolean hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self);
+void hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self,
+ gboolean can_swipe_forward);
+
+GtkWidget *hdy_stackable_box_get_adjacent_child (HdyStackableBox *self,
+ HdyNavigationDirection direction);
+gboolean hdy_stackable_box_navigate (HdyStackableBox *self,
+ HdyNavigationDirection direction);
+
+GtkWidget *hdy_stackable_box_get_child_by_name (HdyStackableBox *self,
+ const gchar *name);
+
+GtkOrientation hdy_stackable_box_get_orientation (HdyStackableBox *self);
+void hdy_stackable_box_set_orientation (HdyStackableBox *self,
+ GtkOrientation orientation);
+
+const gchar *hdy_stackable_box_get_child_name (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_set_child_name (HdyStackableBox *self,
+ GtkWidget *widget,
+ const gchar *name);
+gboolean hdy_stackable_box_get_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_set_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget,
+ gboolean navigatable);
+
+void hdy_stackable_box_switch_child (HdyStackableBox *self,
+ guint index,
+ gint64 duration);
+
+HdySwipeTracker *hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self);
+gdouble hdy_stackable_box_get_distance (HdyStackableBox *self);
+gdouble *hdy_stackable_box_get_snap_points (HdyStackableBox *self,
+ gint *n_snap_points);
+gdouble hdy_stackable_box_get_progress (HdyStackableBox *self);
+gdouble hdy_stackable_box_get_cancel_progress (HdyStackableBox *self);
+void hdy_stackable_box_get_swipe_area (HdyStackableBox *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+void hdy_stackable_box_add (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_remove (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_forall (HdyStackableBox *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data);
+
+void hdy_stackable_box_measure (HdyStackableBox *self,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline);
+void hdy_stackable_box_size_allocate (HdyStackableBox *self,
+ GtkAllocation *allocation);
+gboolean hdy_stackable_box_draw (HdyStackableBox *self,
+ cairo_t *cr);
+void hdy_stackable_box_realize (HdyStackableBox *self);
+void hdy_stackable_box_unrealize (HdyStackableBox *self);
+void hdy_stackable_box_map (HdyStackableBox *self);
+void hdy_stackable_box_unmap (HdyStackableBox *self);
+void hdy_stackable_box_direction_changed (HdyStackableBox *self,
+ GtkTextDirection previous_direction);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-stackable-box.c b/subprojects/libhandy/src/hdy-stackable-box.c
new file mode 100644
index 0000000..4eb8fa3
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-stackable-box.c
@@ -0,0 +1,3151 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "gtkprogresstrackerprivate.h"
+#include "hdy-animation-private.h"
+#include "hdy-enums-private.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-shadow-helper-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * PRIVATE:hdy-stackable-box
+ * @short_description: An adaptive container acting like a box or a stack.
+ * @Title: HdyStackableBox
+ * @stability: Private
+ * @See_also: #HdyDeck, #HdyLeaflet
+ *
+ * The #HdyStackableBox object can arrange the widgets it manages like #GtkBox
+ * does or like a #GtkStack does, adapting to size changes by switching between
+ * the two modes. These modes are named respectively “unfoled” and “folded”.
+ *
+ * When there is enough space the children are displayed side by side, otherwise
+ * only one is displayed. The threshold is dictated by the preferred minimum
+ * sizes of the children.
+ *
+ * #HdyStackableBox is used as an internal implementation of #HdyDeck and
+ * #HdyLeaflet.
+ *
+ * Since: 1.0
+ */
+
+/**
+ * HdyStackableBoxTransitionType:
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between modes and
+ * children in a #HdyStackableBox widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_FOLDED,
+ PROP_HHOMOGENEOUS_FOLDED,
+ PROP_VHOMOGENEOUS_FOLDED,
+ PROP_HHOMOGENEOUS_UNFOLDED,
+ PROP_VHOMOGENEOUS_UNFOLDED,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_MODE_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+ PROP_ORIENTATION,
+ LAST_PROP,
+};
+
+#define HDY_FOLD_UNFOLDED FALSE
+#define HDY_FOLD_FOLDED TRUE
+#define HDY_FOLD_MAX 2
+#define GTK_ORIENTATION_MAX 2
+#define HDY_SWIPE_BORDER 16
+
+typedef struct _HdyStackableBoxChildInfo HdyStackableBoxChildInfo;
+
+struct _HdyStackableBoxChildInfo
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ gchar *name;
+ gboolean navigatable;
+
+ /* Convenience storage for per-child temporary frequently computed values. */
+ GtkAllocation alloc;
+ GtkRequisition min;
+ GtkRequisition nat;
+ gboolean visible;
+};
+
+struct _HdyStackableBox
+{
+ GObject parent;
+
+ GtkContainer *container;
+ GtkContainerClass *klass;
+ gboolean can_unfold;
+
+ GList *children;
+ /* It is probably cheaper to store and maintain a reversed copy of the
+ * children list that to reverse the list every time we need to allocate or
+ * draw children for RTL languages on a horizontal widget.
+ */
+ GList *children_reversed;
+ HdyStackableBoxChildInfo *visible_child;
+ HdyStackableBoxChildInfo *last_visible_child;
+
+ GdkWindow* view_window;
+
+ gboolean folded;
+
+ gboolean homogeneous[HDY_FOLD_MAX][GTK_ORIENTATION_MAX];
+
+ GtkOrientation orientation;
+
+ HdyStackableBoxTransitionType transition_type;
+
+ HdySwipeTracker *tracker;
+
+ struct {
+ guint duration;
+
+ gdouble current_pos;
+ gdouble source_pos;
+ gdouble target_pos;
+
+ gdouble start_progress;
+ gdouble end_progress;
+ guint tick_id;
+ GtkProgressTracker tracker;
+ } mode_transition;
+
+ /* Child transition variables. */
+ struct {
+ guint duration;
+
+ gdouble progress;
+ gdouble start_progress;
+ gdouble end_progress;
+
+ gboolean is_gesture_active;
+ gboolean is_cancelled;
+
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ gboolean interpolate_size;
+ gboolean can_swipe_back;
+ gboolean can_swipe_forward;
+
+ GtkPanDirection active_direction;
+ gboolean is_direct_swipe;
+ gint swipe_direction;
+ } child_transition;
+
+ HdyShadowHelper *shadow_helper;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gint HOMOGENEOUS_PROP[HDY_FOLD_MAX][GTK_ORIENTATION_MAX] = {
+ { PROP_HHOMOGENEOUS_UNFOLDED, PROP_VHOMOGENEOUS_UNFOLDED},
+ { PROP_HHOMOGENEOUS_FOLDED, PROP_VHOMOGENEOUS_FOLDED},
+};
+
+G_DEFINE_TYPE (HdyStackableBox, hdy_stackable_box, G_TYPE_OBJECT);
+
+static void
+free_child_info (HdyStackableBoxChildInfo *child_info)
+{
+ g_free (child_info->name);
+ g_free (child_info);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (HdyStackableBoxChildInfo, free_child_info)
+
+static HdyStackableBoxChildInfo *
+find_child_info_for_widget (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info->widget == widget)
+ return child_info;
+ }
+
+ return NULL;
+}
+
+static HdyStackableBoxChildInfo *
+find_child_info_for_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (g_strcmp0 (child_info->name, name) == 0)
+ return child_info;
+ }
+
+ return NULL;
+}
+
+static GList *
+get_directed_children (HdyStackableBox *self)
+{
+ return self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL ?
+ self->children_reversed : self->children;
+}
+
+static GtkPanDirection
+get_pan_direction (HdyStackableBox *self,
+ gboolean new_child_first)
+{
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL)
+ return new_child_first ? GTK_PAN_DIRECTION_LEFT : GTK_PAN_DIRECTION_RIGHT;
+ else
+ return new_child_first ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT;
+ }
+ else
+ return new_child_first ? GTK_PAN_DIRECTION_DOWN : GTK_PAN_DIRECTION_UP;
+}
+
+static gint
+get_child_window_x (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child_info,
+ gint width)
+{
+ gboolean is_rtl;
+ gint rtl_multiplier;
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ if (self->child_transition.active_direction != GTK_PAN_DIRECTION_LEFT &&
+ self->child_transition.active_direction != GTK_PAN_DIRECTION_RIGHT)
+ return 0;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+ rtl_multiplier = is_rtl ? -1 : 1;
+
+ if ((self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT) == is_rtl) {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return width * (1 - self->child_transition.progress) * rtl_multiplier;
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return -width * self->child_transition.progress * rtl_multiplier;
+ } else {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return -width * (1 - self->child_transition.progress) * rtl_multiplier;
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return width * self->child_transition.progress * rtl_multiplier;
+ }
+
+ return 0;
+}
+
+static gint
+get_child_window_y (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child_info,
+ gint height)
+{
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ if (self->child_transition.active_direction != GTK_PAN_DIRECTION_UP &&
+ self->child_transition.active_direction != GTK_PAN_DIRECTION_DOWN)
+ return 0;
+
+ if (self->child_transition.active_direction == GTK_PAN_DIRECTION_UP) {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return height * (1 - self->child_transition.progress);
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return -height * self->child_transition.progress;
+ } else {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return -height * (1 - self->child_transition.progress);
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return height * self->child_transition.progress;
+ }
+
+ return 0;
+}
+
+static void
+hdy_stackable_box_child_progress_updated (HdyStackableBox *self)
+{
+ gtk_widget_queue_draw (GTK_WIDGET (self->container));
+
+ if (!self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] ||
+ !self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL])
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+ else
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (self->child_transition.is_cancelled) {
+ if (self->last_visible_child != NULL) {
+ if (self->folded) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, TRUE);
+ gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
+ }
+ self->visible_child = self->last_visible_child;
+ self->last_visible_child = NULL;
+ }
+
+ self->child_transition.is_cancelled = FALSE;
+
+ g_object_freeze_notify (G_OBJECT (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]);
+ g_object_thaw_notify (G_OBJECT (self));
+ } else {
+ if (self->last_visible_child != NULL) {
+ if (self->folded)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+ self->child_transition.swipe_direction = 0;
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+ }
+}
+
+static gboolean
+hdy_stackable_box_child_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ gdouble progress;
+
+ if (self->child_transition.first_frame_skipped) {
+ gtk_progress_tracker_advance_frame (&self->child_transition.tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ progress = gtk_progress_tracker_get_ease_out_cubic (&self->child_transition.tracker, FALSE);
+ self->child_transition.progress =
+ hdy_lerp (self->child_transition.start_progress,
+ self->child_transition.end_progress, progress);
+ } else
+ self->child_transition.first_frame_skipped = TRUE;
+
+ /* Finish animation early if not mapped anymore */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+
+ hdy_stackable_box_child_progress_updated (self);
+
+ if (gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->child_transition.tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_stackable_box_schedule_child_ticks (HdyStackableBox *self)
+{
+ if (self->child_transition.tick_id == 0) {
+ self->child_transition.tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self->container),
+ hdy_stackable_box_child_transition_cb,
+ self, NULL);
+ if (!self->child_transition.is_gesture_active)
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_stackable_box_unschedule_child_ticks (HdyStackableBox *self)
+{
+ if (self->child_transition.tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->child_transition.tick_id);
+ self->child_transition.tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_stackable_box_stop_child_transition (HdyStackableBox *self)
+{
+ hdy_stackable_box_unschedule_child_ticks (self);
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ if (self->last_visible_child != NULL) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+
+ self->child_transition.swipe_direction = 0;
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+}
+
+static void
+hdy_stackable_box_start_child_transition (HdyStackableBox *self,
+ guint transition_duration,
+ GtkPanDirection transition_direction)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (gtk_widget_get_mapped (widget) &&
+ ((hdy_get_enable_animations (widget) &&
+ transition_duration != 0) ||
+ self->child_transition.is_gesture_active) &&
+ self->last_visible_child != NULL &&
+ /* Don't animate child transition when a mode transition is ongoing. */
+ self->mode_transition.tick_id == 0) {
+ self->child_transition.active_direction = transition_direction;
+ self->child_transition.first_frame_skipped = FALSE;
+ self->child_transition.start_progress = 0;
+ self->child_transition.end_progress = 1;
+ self->child_transition.progress = 0;
+ self->child_transition.is_cancelled = FALSE;
+
+ if (!self->child_transition.is_gesture_active) {
+ hdy_stackable_box_schedule_child_ticks (self);
+ gtk_progress_tracker_start (&self->child_transition.tracker,
+ transition_duration * 1000,
+ 0,
+ 1.0);
+ }
+ }
+ else {
+ hdy_stackable_box_unschedule_child_ticks (self);
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ }
+
+ hdy_stackable_box_child_progress_updated (self);
+}
+
+static void
+set_visible_child_info (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *new_visible_child,
+ HdyStackableBoxTransitionType transition_type,
+ guint transition_duration,
+ gboolean emit_child_switched)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+ GtkPanDirection transition_direction = GTK_PAN_DIRECTION_LEFT;
+
+ /* If we are being destroyed, do not bother with transitions and
+ * notifications.
+ */
+ if (gtk_widget_in_destruction (widget))
+ return;
+
+ /* If none, pick first visible. */
+ if (new_visible_child == NULL) {
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (gtk_widget_get_visible (child_info->widget)) {
+ new_visible_child = child_info;
+
+ break;
+ }
+ }
+ }
+
+ if (new_visible_child == self->visible_child)
+ return;
+
+ /* FIXME Probably copied from Gtk Stack, should check whether it's needed. */
+ /* toplevel = gtk_widget_get_toplevel (widget); */
+ /* if (GTK_IS_WINDOW (toplevel)) { */
+ /* focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); */
+ /* if (focus && */
+ /* self->visible_child && */
+ /* self->visible_child->widget && */
+ /* gtk_widget_is_ancestor (focus, self->visible_child->widget)) { */
+ /* contains_focus = TRUE; */
+
+ /* if (self->visible_child->last_focus) */
+ /* g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus), */
+ /* (gpointer *)&self->visible_child->last_focus); */
+ /* self->visible_child->last_focus = focus; */
+ /* g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus), */
+ /* (gpointer *)&self->visible_child->last_focus); */
+ /* } */
+ /* } */
+
+ if (self->last_visible_child)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded);
+ self->last_visible_child = NULL;
+
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+
+ if (self->visible_child && self->visible_child->widget) {
+ if (gtk_widget_is_visible (widget))
+ self->last_visible_child = self->visible_child;
+ else
+ gtk_widget_set_child_visible (self->visible_child->widget, !self->folded);
+ }
+
+ /* FIXME This comes from GtkStack and should be adapted. */
+ /* hdy_stackable_box_accessible_update_visible_child (stack, */
+ /* self->visible_child ? self->visible_child->widget : NULL, */
+ /* new_visible_child ? new_visible_child->widget : NULL); */
+
+ self->visible_child = new_visible_child;
+
+ if (new_visible_child) {
+ gtk_widget_set_child_visible (new_visible_child->widget, TRUE);
+
+ /* FIXME This comes from GtkStack and should be adapted. */
+ /* if (contains_focus) { */
+ /* if (new_visible_child->last_focus) */
+ /* gtk_widget_grab_focus (new_visible_child->last_focus); */
+ /* else */
+ /* gtk_widget_child_focus (new_visible_child->widget, GTK_DIR_TAB_FORWARD); */
+ /* } */
+ }
+
+ if (new_visible_child == NULL || self->last_visible_child == NULL)
+ transition_duration = 0;
+ else {
+ gboolean new_first = FALSE;
+ for (children = self->children; children; children = children->next) {
+ if (new_visible_child == children->data) {
+ new_first = TRUE;
+
+ break;
+ }
+ if (self->last_visible_child == children->data)
+ break;
+ }
+
+ transition_direction = get_pan_direction (self, new_first);
+ }
+
+ if (self->folded) {
+ if (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] &&
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL])
+ gtk_widget_queue_allocate (widget);
+ else
+ gtk_widget_queue_resize (widget);
+
+ hdy_stackable_box_start_child_transition (self, transition_duration, transition_direction);
+ }
+
+ if (emit_child_switched) {
+ gint index = 0;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->navigatable)
+ continue;
+
+ if (child_info == new_visible_child)
+ break;
+
+ index++;
+ }
+
+ hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self->container), index,
+ transition_duration);
+ }
+
+ g_object_freeze_notify (G_OBJECT (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+hdy_stackable_box_set_position (HdyStackableBox *self,
+ gdouble pos)
+{
+ self->mode_transition.current_pos = pos;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+}
+
+static void
+hdy_stackable_box_mode_progress_updated (HdyStackableBox *self)
+{
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+}
+
+static gboolean
+hdy_stackable_box_mode_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ gdouble ease;
+
+ gtk_progress_tracker_advance_frame (&self->mode_transition.tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ ease = gtk_progress_tracker_get_ease_out_cubic (&self->mode_transition.tracker, FALSE);
+ hdy_stackable_box_set_position (self,
+ self->mode_transition.source_pos + (ease * (self->mode_transition.target_pos - self->mode_transition.source_pos)));
+
+ hdy_stackable_box_mode_progress_updated (self);
+
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->mode_transition.tick_id = 0;
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_stackable_box_start_mode_transition (HdyStackableBox *self,
+ gdouble target)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (self->mode_transition.target_pos == target)
+ return;
+
+ self->mode_transition.target_pos = target;
+ /* FIXME PROP_REVEAL_CHILD needs to be implemented. */
+ /* g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_REVEAL_CHILD]); */
+
+ hdy_stackable_box_stop_child_transition (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ self->mode_transition.duration != 0 &&
+ hdy_get_enable_animations (widget) &&
+ self->can_unfold) {
+ self->mode_transition.source_pos = self->mode_transition.current_pos;
+ if (self->mode_transition.tick_id == 0)
+ self->mode_transition.tick_id = gtk_widget_add_tick_callback (widget, hdy_stackable_box_mode_transition_cb, self, NULL);
+ gtk_progress_tracker_start (&self->mode_transition.tracker,
+ self->mode_transition.duration * 1000,
+ 0,
+ 1.0);
+ }
+ else
+ hdy_stackable_box_set_position (self, target);
+}
+
+/* FIXME Use this to stop the mode transition animation when it makes sense (see *
+ * GtkRevealer for exmples).
+ */
+/* static void */
+/* hdy_stackable_box_stop_mode_animation (HdyStackableBox *self) */
+/* { */
+/* if (self->mode_transition.current_pos != self->mode_transition.target_pos) { */
+/* self->mode_transition.current_pos = self->mode_transition.target_pos; */
+ /* g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_REVEALED]); */
+/* } */
+/* if (self->mode_transition.tick_id != 0) { */
+/* gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->mode_transition.tick_id); */
+/* self->mode_transition.tick_id = 0; */
+/* } */
+/* } */
+
+/**
+ * hdy_stackable_box_get_folded:
+ * @self: a #HdyStackableBox
+ *
+ * Gets whether @self is folded.
+ *
+ * Returns: whether @self is folded.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_folded (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->folded;
+}
+
+static void
+hdy_stackable_box_set_folded (HdyStackableBox *self,
+ gboolean folded)
+{
+ GtkStyleContext *context;
+
+ if (self->folded == folded)
+ return;
+
+ self->folded = folded;
+
+ hdy_stackable_box_start_mode_transition (self, folded ? 0.0 : 1.0);
+
+ if (self->can_unfold) {
+ context = gtk_widget_get_style_context (GTK_WIDGET (self->container));
+ if (folded) {
+ gtk_style_context_add_class (context, "folded");
+ gtk_style_context_remove_class (context, "unfolded");
+ } else {
+ gtk_style_context_remove_class (context, "folded");
+ gtk_style_context_add_class (context, "unfolded");
+ }
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_FOLDED]);
+}
+
+/**
+ * hdy_stackable_box_set_homogeneous:
+ * @self: a #HdyStackableBox
+ * @folded: the fold
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyStackableBox to be homogeneous or not for the given fold and orientation.
+ * If it is homogeneous, the #HdyStackableBox will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't and it is folded, the widget may change width or height
+ * when a different child becomes visible.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ folded = !!folded;
+ homogeneous = !!homogeneous;
+
+ if (self->homogeneous[folded][orientation] == homogeneous)
+ return;
+
+ self->homogeneous[folded][orientation] = homogeneous;
+
+ if (gtk_widget_get_visible (GTK_WIDGET (self->container)))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[HOMOGENEOUS_PROP[folded][orientation]]);
+}
+
+/**
+ * hdy_stackable_box_get_homogeneous:
+ * @self: a #HdyStackableBox
+ * @folded: the fold
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given fold and orientation.
+ * See hdy_stackable_box_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given fold and orientation.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ folded = !!folded;
+
+ return self->homogeneous[folded][orientation];
+}
+
+/**
+ * hdy_stackable_box_get_transition_type:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the type of animation that will be used
+ * for transitions between modes and children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 1.0
+ */
+HdyStackableBoxTransitionType
+hdy_stackable_box_get_transition_type (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER);
+
+ return self->transition_type;
+}
+
+/**
+ * hdy_stackable_box_set_transition_type:
+ * @self: a #HdyStackableBox
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between modes
+ * and children in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about to
+ * become current.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_transition_type (HdyStackableBox *self,
+ HdyStackableBoxTransitionType transition)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->transition_type == transition)
+ return;
+
+ self->transition_type = transition;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_TRANSITION_TYPE]);
+}
+
+/**
+ * hdy_stackable_box_get_mode_transition_duration:
+ * @self: a #HdyStackableBox
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between modes in @self will take.
+ *
+ * Returns: the mode transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0);
+
+ return self->mode_transition.duration;
+}
+
+/**
+ * hdy_stackable_box_set_mode_transition_duration:
+ * @self: a #HdyStackableBox
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between modes in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->mode_transition.duration == duration)
+ return;
+
+ self->mode_transition.duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_MODE_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_stackable_box_get_child_transition_duration:
+ * @self: a #HdyStackableBox
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0);
+
+ return self->child_transition.duration;
+}
+
+/**
+ * hdy_stackable_box_set_child_transition_duration:
+ * @self: a #HdyStackableBox
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->child_transition.duration == duration)
+ return;
+
+ self->child_transition.duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_CHILD_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_stackable_box_get_visible_child:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_visible_child (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ if (self->visible_child == NULL)
+ return NULL;
+
+ return self->visible_child->widget;
+}
+
+/**
+ * hdy_stackable_box_set_visible_child:
+ * @self: a #HdyStackableBox
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyStackableBox:transition-type and HdyStackableBox:child-transition-duration.
+ * The transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_visible_child (HdyStackableBox *self,
+ GtkWidget *visible_child)
+{
+ HdyStackableBoxChildInfo *child_info;
+ gboolean contains_child;
+
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (visible_child));
+
+ child_info = find_child_info_for_widget (self, visible_child);
+ contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+/**
+ * hdy_stackable_box_get_visible_child_name:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_stackable_box_get_visible_child_name (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ if (self->visible_child == NULL)
+ return NULL;
+
+ return self->visible_child->name;
+}
+
+/**
+ * hdy_stackable_box_set_visible_child_name:
+ * @self: a #HdyStackableBox
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_stackable_box_set_visible_child() for more details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_visible_child_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+ gboolean contains_child;
+
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+ g_return_if_fail (name != NULL);
+
+ child_info = find_child_info_for_name (self, name);
+ contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+/**
+ * hdy_stackable_box_get_child_transition_running:
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_child_transition_running (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return (self->child_transition.tick_id != 0 ||
+ self->child_transition.is_gesture_active);
+}
+
+/**
+ * hdy_stackable_box_set_interpolate_size:
+ * @self: a #HdyStackableBox
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyStackableBox:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_interpolate_size (HdyStackableBox *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ interpolate_size = !!interpolate_size;
+
+ if (self->child_transition.interpolate_size == interpolate_size)
+ return;
+
+ self->child_transition.interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
+
+/**
+ * hdy_stackable_box_get_interpolate_size:
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_interpolate_size (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.interpolate_size;
+}
+
+/**
+ * hdy_stackable_box_set_can_swipe_back:
+ * @self: a #HdyStackableBox
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ can_swipe_back = !!can_swipe_back;
+
+ if (self->child_transition.can_swipe_back == can_swipe_back)
+ return;
+
+ self->child_transition.can_swipe_back = can_swipe_back;
+ hdy_swipe_tracker_set_enabled (self->tracker, can_swipe_back || self->child_transition.can_swipe_forward);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]);
+}
+
+/**
+ * hdy_stackable_box_get_can_swipe_back
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.can_swipe_back;
+}
+
+/**
+ * hdy_stackable_box_set_can_swipe_forward:
+ * @self: a #HdyStackableBox
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ can_swipe_forward = !!can_swipe_forward;
+
+ if (self->child_transition.can_swipe_forward == can_swipe_forward)
+ return;
+
+ self->child_transition.can_swipe_forward = can_swipe_forward;
+ hdy_swipe_tracker_set_enabled (self->tracker, self->child_transition.can_swipe_back || can_swipe_forward);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_FORWARD]);
+}
+
+/**
+ * hdy_stackable_box_get_can_swipe_forward
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.can_swipe_forward;
+}
+
+static HdyStackableBoxChildInfo *
+find_swipeable_child (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child = NULL;
+
+ children = g_list_find (self->children, self->visible_child);
+ do {
+ children = (direction == HDY_NAVIGATION_DIRECTION_BACK) ? children->prev : children->next;
+
+ if (children == NULL)
+ break;
+
+ child = children->data;
+ } while (child && !child->navigatable);
+
+ return child;
+}
+
+/**
+ * hdy_stackable_box_get_adjacent_child
+ * @self: a #HdyStackableBox
+ * @direction: the direction
+ *
+ * Gets the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, or %NULL if it doesn't exist. This will be the same
+ * widget hdy_stackable_box_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_adjacent_child (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ HdyStackableBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ child = find_swipeable_child (self, direction);
+
+ if (!child)
+ return NULL;
+
+ return child->widget;
+}
+
+/**
+ * hdy_stackable_box_navigate
+ * @self: a #HdyStackableBox
+ * @direction: the direction
+ *
+ * Switches to the previous or next child that doesn't have 'navigatable'
+ * child property set to %FALSE, similar to performing a swipe gesture to go
+ * in @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_navigate (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ HdyStackableBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ child = find_swipeable_child (self, direction);
+
+ if (!child)
+ return FALSE;
+
+ set_visible_child_info (self, child, self->transition_type, self->child_transition.duration, TRUE);
+
+ return TRUE;
+}
+
+/**
+ * hdy_stackable_box_get_child_by_name:
+ * @self: a #HdyStackableBox
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_child_by_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ child_info = find_child_info_for_name (self, name);
+
+ return child_info ? child_info->widget : NULL;
+}
+
+static void
+get_preferred_size (gint *min,
+ gint *nat,
+ gboolean same_orientation,
+ gboolean homogeneous_folded,
+ gboolean homogeneous_unfolded,
+ gint visible_children,
+ gdouble visible_child_progress,
+ gint sum_nat,
+ gint max_min,
+ gint max_nat,
+ gint visible_min,
+ gint last_visible_min)
+{
+ if (same_orientation) {
+ *min = homogeneous_folded ?
+ max_min :
+ hdy_lerp (last_visible_min, visible_min, visible_child_progress);
+ *nat = homogeneous_unfolded ?
+ max_nat * visible_children :
+ sum_nat;
+ }
+ else {
+ *min = homogeneous_folded ?
+ max_min :
+ hdy_lerp (last_visible_min, visible_min, visible_child_progress);
+ *nat = max_nat;
+ }
+}
+
+void
+hdy_stackable_box_measure (HdyStackableBox *self,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+ gint visible_children;
+ gdouble visible_child_progress;
+ gint child_min, max_min, visible_min, last_visible_min;
+ gint child_nat, max_nat, sum_nat;
+ void (*get_preferred_size_static) (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width);
+ void (*get_preferred_size_for_size) (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width);
+
+ get_preferred_size_static = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ gtk_widget_get_preferred_width :
+ gtk_widget_get_preferred_height;
+ get_preferred_size_for_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ gtk_widget_get_preferred_width_for_height :
+ gtk_widget_get_preferred_height_for_width;
+
+ visible_children = 0;
+ child_min = max_min = visible_min = last_visible_min = 0;
+ child_nat = max_nat = sum_nat = 0;
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info->widget == NULL || !gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ visible_children++;
+ if (for_size < 0)
+ get_preferred_size_static (child_info->widget,
+ &child_min, &child_nat);
+ else
+ get_preferred_size_for_size (child_info->widget, for_size,
+ &child_min, &child_nat);
+
+ max_min = MAX (max_min, child_min);
+ max_nat = MAX (max_nat, child_nat);
+ sum_nat += child_nat;
+ }
+
+ if (self->visible_child != NULL) {
+ if (for_size < 0)
+ get_preferred_size_static (self->visible_child->widget,
+ &visible_min, NULL);
+ else
+ get_preferred_size_for_size (self->visible_child->widget, for_size,
+ &visible_min, NULL);
+ }
+
+ if (self->last_visible_child != NULL) {
+ if (for_size < 0)
+ get_preferred_size_static (self->last_visible_child->widget,
+ &last_visible_min, NULL);
+ else
+ get_preferred_size_for_size (self->last_visible_child->widget, for_size,
+ &last_visible_min, NULL);
+ }
+
+ visible_child_progress = self->child_transition.interpolate_size ? self->child_transition.progress : 1.0;
+
+ get_preferred_size (minimum, natural,
+ gtk_orientable_get_orientation (GTK_ORIENTABLE (self->container)) == orientation,
+ self->homogeneous[HDY_FOLD_FOLDED][orientation],
+ self->homogeneous[HDY_FOLD_UNFOLDED][orientation],
+ visible_children, visible_child_progress,
+ sum_nat, max_min, max_nat, visible_min, last_visible_min);
+}
+
+static void
+hdy_stackable_box_size_allocate_folded (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info, *visible_child;
+ gint start_size, end_size, visible_size;
+ gint remaining_start_size, remaining_end_size, remaining_size;
+ gint current_pad;
+ gint max_child_size = 0;
+ gint start_position, end_position;
+ gboolean box_homogeneous;
+ HdyStackableBoxTransitionType mode_transition_type;
+ GtkTextDirection direction;
+ gboolean under;
+
+ directed_children = get_directed_children (self);
+ visible_child = self->visible_child;
+
+ if (!visible_child)
+ return;
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->widget == visible_child->widget)
+ continue;
+
+ if (self->last_visible_child &&
+ child_info->widget == self->last_visible_child->widget)
+ continue;
+
+ child_info->visible = FALSE;
+ }
+
+ if (visible_child->widget == NULL)
+ return;
+
+ /* FIXME is this needed? */
+ if (!gtk_widget_get_visible (visible_child->widget)) {
+ visible_child->visible = FALSE;
+
+ return;
+ }
+
+ visible_child->visible = TRUE;
+
+ mode_transition_type = self->transition_type;
+
+ /* Avoid useless computations and allow visible child transitions. */
+ if (self->mode_transition.current_pos <= 0.0) {
+ /* Child transitions should be applied only when folded and when no mode
+ * transition is ongoing.
+ */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info != visible_child &&
+ child_info != self->last_visible_child) {
+ child_info->visible = FALSE;
+
+ continue;
+ }
+
+ child_info->alloc.x = get_child_window_x (self, child_info, allocation->width);
+ child_info->alloc.y = get_child_window_y (self, child_info, allocation->height);
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = allocation->height;
+ child_info->visible = TRUE;
+ }
+
+ return;
+ }
+
+ /* Compute visible child size. */
+ visible_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ MIN (allocation->width, MAX (visible_child->nat.width, (gint) (allocation->width * (1.0 - self->mode_transition.current_pos)))) :
+ MIN (allocation->height, MAX (visible_child->nat.height, (gint) (allocation->height * (1.0 - self->mode_transition.current_pos))));
+
+ /* Compute homogeneous box child size. */
+ box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) ||
+ (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL);
+ if (box_homogeneous) {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ max_child_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ MAX (max_child_size, child_info->nat.width) :
+ MAX (max_child_size, child_info->nat.height);
+ }
+ }
+
+ /* Compute the start size. */
+ start_size = 0;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ start_size += orientation == GTK_ORIENTATION_HORIZONTAL ?
+ (box_homogeneous ? max_child_size : child_info->nat.width) :
+ (box_homogeneous ? max_child_size : child_info->nat.height);
+ }
+
+ /* Compute the end size. */
+ end_size = 0;
+ for (children = g_list_last (directed_children); children; children = children->prev) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ end_size += orientation == GTK_ORIENTATION_HORIZONTAL ?
+ (box_homogeneous ? max_child_size : child_info->nat.width) :
+ (box_homogeneous ? max_child_size : child_info->nat.height);
+ }
+
+ /* Compute pads. */
+ remaining_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ allocation->width - visible_size :
+ allocation->height - visible_size;
+ remaining_start_size = (gint) (remaining_size * ((gdouble) start_size / (gdouble) (start_size + end_size)));
+ remaining_end_size = remaining_size - remaining_start_size;
+
+ /* Store start and end allocations. */
+ switch (orientation) {
+ case GTK_ORIENTATION_HORIZONTAL:
+ direction = gtk_widget_get_direction (GTK_WIDGET (self->container));
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL);
+ start_position = under ? 0 : remaining_start_size - start_size;
+ self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1;
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL);
+ end_position = under ? allocation->width - end_size : remaining_start_size + visible_size;
+ self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1;
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ start_position = under ? 0 : remaining_start_size - start_size;
+ self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1;
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ end_position = remaining_start_size + visible_size;
+ self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ /* Allocate visible child. */
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ visible_child->alloc.width = visible_size;
+ visible_child->alloc.height = allocation->height;
+ visible_child->alloc.x = remaining_start_size;
+ visible_child->alloc.y = 0;
+ visible_child->visible = TRUE;
+ }
+ else {
+ visible_child->alloc.width = allocation->width;
+ visible_child->alloc.height = visible_size;
+ visible_child->alloc.x = 0;
+ visible_child->alloc.y = remaining_start_size;
+ visible_child->visible = TRUE;
+ }
+
+ /* Allocate starting children. */
+ current_pad = start_position;
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_info->alloc.width = box_homogeneous ?
+ max_child_size :
+ child_info->nat.width;
+ child_info->alloc.height = allocation->height;
+ child_info->alloc.x = current_pad;
+ child_info->alloc.y = 0;
+ child_info->visible = child_info->alloc.x + child_info->alloc.width > 0;
+
+ current_pad += child_info->alloc.width;
+ }
+ else {
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = box_homogeneous ?
+ max_child_size :
+ child_info->nat.height;
+ child_info->alloc.x = 0;
+ child_info->alloc.y = current_pad;
+ child_info->visible = child_info->alloc.y + child_info->alloc.height > 0;
+
+ current_pad += child_info->alloc.height;
+ }
+ }
+
+ /* Allocate ending children. */
+ current_pad = end_position;
+
+ if (!children || !children->next)
+ return;
+
+ for (children = children->next; children; children = children->next) {
+ child_info = children->data;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_info->alloc.width = box_homogeneous ?
+ max_child_size :
+ child_info->nat.width;
+ child_info->alloc.height = allocation->height;
+ child_info->alloc.x = current_pad;
+ child_info->alloc.y = 0;
+ child_info->visible = child_info->alloc.x < allocation->width;
+
+ current_pad += child_info->alloc.width;
+ }
+ else {
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = box_homogeneous ?
+ max_child_size :
+ child_info->nat.height;
+ child_info->alloc.x = 0;
+ child_info->alloc.y = current_pad;
+ child_info->visible = child_info->alloc.y < allocation->height;
+
+ current_pad += child_info->alloc.height;
+ }
+ }
+}
+
+static void
+hdy_stackable_box_size_allocate_unfolded (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GtkAllocation remaining_alloc;
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info, *visible_child;
+ gint homogeneous_size = 0, min_size, extra_size;
+ gint per_child_extra, n_extra_widgets;
+ gint n_visible_children, n_expand_children;
+ gint start_pad = 0, end_pad = 0;
+ gboolean box_homogeneous;
+ HdyStackableBoxTransitionType mode_transition_type;
+ GtkTextDirection direction;
+ gboolean under;
+
+ directed_children = get_directed_children (self);
+ visible_child = self->visible_child;
+
+ box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) ||
+ (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL);
+
+ n_visible_children = n_expand_children = 0;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ child_info->visible = child_info->widget != NULL && gtk_widget_get_visible (child_info->widget);
+
+ if (child_info->visible) {
+ n_visible_children++;
+ if (gtk_widget_compute_expand (child_info->widget, orientation))
+ n_expand_children++;
+ }
+ else {
+ child_info->min.width = child_info->min.height = 0;
+ child_info->nat.width = child_info->nat.height = 0;
+ }
+ }
+
+ /* Compute repartition of extra space. */
+
+ if (box_homogeneous) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ homogeneous_size = n_visible_children > 0 ? allocation->width / n_visible_children : 0;
+ n_expand_children = n_visible_children > 0 ? allocation->width % n_visible_children : 0;
+ min_size = allocation->width - n_expand_children;
+ }
+ else {
+ homogeneous_size = n_visible_children > 0 ? allocation->height / n_visible_children : 0;
+ n_expand_children = n_visible_children > 0 ? allocation->height % n_visible_children : 0;
+ min_size = allocation->height - n_expand_children;
+ }
+ }
+ else {
+ min_size = 0;
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ min_size += child_info->nat.width;
+ }
+ }
+ else {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ min_size += child_info->nat.height;
+ }
+ }
+ }
+
+ remaining_alloc.x = 0;
+ remaining_alloc.y = 0;
+ remaining_alloc.width = allocation->width;
+ remaining_alloc.height = allocation->height;
+
+ extra_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ remaining_alloc.width - min_size :
+ remaining_alloc.height - min_size;
+
+ per_child_extra = 0, n_extra_widgets = 0;
+ if (n_expand_children > 0) {
+ per_child_extra = extra_size / n_expand_children;
+ n_extra_widgets = extra_size % n_expand_children;
+ }
+
+ /* Compute children allocation */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->visible)
+ continue;
+
+ child_info->alloc.x = remaining_alloc.x;
+ child_info->alloc.y = remaining_alloc.y;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (box_homogeneous) {
+ child_info->alloc.width = homogeneous_size;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.width++;
+ n_extra_widgets--;
+ }
+ }
+ else {
+ child_info->alloc.width = child_info->nat.width;
+ if (gtk_widget_compute_expand (child_info->widget, orientation)) {
+ child_info->alloc.width += per_child_extra;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.width++;
+ n_extra_widgets--;
+ }
+ }
+ }
+ child_info->alloc.height = remaining_alloc.height;
+
+ remaining_alloc.x += child_info->alloc.width;
+ remaining_alloc.width -= child_info->alloc.width;
+ }
+ else {
+ if (box_homogeneous) {
+ child_info->alloc.height = homogeneous_size;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.height++;
+ n_extra_widgets--;
+ }
+ }
+ else {
+ child_info->alloc.height = child_info->nat.height;
+ if (gtk_widget_compute_expand (child_info->widget, orientation)) {
+ child_info->alloc.height += per_child_extra;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.height++;
+ n_extra_widgets--;
+ }
+ }
+ }
+ child_info->alloc.width = remaining_alloc.width;
+
+ remaining_alloc.y += child_info->alloc.height;
+ remaining_alloc.height -= child_info->alloc.height;
+ }
+ }
+
+ /* Apply animations. */
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ start_pad = (gint) ((visible_child->alloc.x) * (1.0 - self->mode_transition.current_pos));
+ end_pad = (gint) ((allocation->width - (visible_child->alloc.x + visible_child->alloc.width)) * (1.0 - self->mode_transition.current_pos));
+ }
+ else {
+ start_pad = (gint) ((visible_child->alloc.y) * (1.0 - self->mode_transition.current_pos));
+ end_pad = (gint) ((allocation->height - (visible_child->alloc.y + visible_child->alloc.height)) * (1.0 - self->mode_transition.current_pos));
+ }
+
+ mode_transition_type = self->transition_type;
+ direction = gtk_widget_get_direction (GTK_WIDGET (self->container));
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL);
+ else
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (!child_info->visible)
+ continue;
+
+ if (under)
+ continue;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ child_info->alloc.x -= start_pad;
+ else
+ child_info->alloc.y -= start_pad;
+ }
+
+ self->mode_transition.start_progress = under ? self->mode_transition.current_pos : 1;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL);
+ else
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ for (children = g_list_last (directed_children); children; children = children->prev) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (!child_info->visible)
+ continue;
+
+ if (under)
+ continue;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ child_info->alloc.x += end_pad;
+ else
+ child_info->alloc.y += end_pad;
+ }
+
+ self->mode_transition.end_progress = under ? self->mode_transition.current_pos : 1;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ visible_child->alloc.x -= start_pad;
+ visible_child->alloc.width += start_pad + end_pad;
+ }
+ else {
+ visible_child->alloc.y -= start_pad;
+ visible_child->alloc.height += start_pad + end_pad;
+ }
+}
+
+static HdyStackableBoxChildInfo *
+get_top_overlap_child (HdyStackableBox *self)
+{
+ gboolean is_rtl, start;
+
+ if (!self->last_visible_child)
+ return self->visible_child;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ start = (self->child_transition.active_direction == GTK_PAN_DIRECTION_LEFT && !is_rtl) ||
+ (self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT && is_rtl) ||
+ self->child_transition.active_direction == GTK_PAN_DIRECTION_UP;
+
+ switch (self->transition_type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ // Nothing overlaps in this case
+ return NULL;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return start ? self->visible_child : self->last_visible_child;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return start ? self->last_visible_child : self->visible_child;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+restack_windows (HdyStackableBox *self)
+{
+ HdyStackableBoxChildInfo *child_info, *overlap_child;
+ GList *l;
+
+ overlap_child = get_top_overlap_child (self);
+
+ switch (self->transition_type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ // Nothing overlaps in this case
+ return;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ for (l = g_list_last (self->children); l; l = l->prev) {
+ child_info = l->data;
+
+ if (child_info->window)
+ gdk_window_raise (child_info->window);
+
+ if (child_info == overlap_child)
+ break;
+ }
+
+ break;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ for (l = self->children; l; l = l->next) {
+ child_info = l->data;
+
+ if (child_info->window)
+ gdk_window_raise (child_info->window);
+
+ if (child_info == overlap_child)
+ break;
+ }
+
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+void
+hdy_stackable_box_size_allocate (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info;
+ gboolean folded;
+
+ directed_children = get_directed_children (self);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (gtk_widget_get_realized (widget)) {
+ gdk_window_move_resize (self->view_window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+ }
+
+ /* Prepare children information. */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ gtk_widget_get_preferred_size (child_info->widget, &child_info->min, &child_info->nat);
+ child_info->alloc.x = child_info->alloc.y = child_info->alloc.width = child_info->alloc.height = 0;
+ child_info->visible = FALSE;
+ }
+
+ /* Check whether the children should be stacked or not. */
+ if (self->can_unfold) {
+ gint nat_box_size = 0, nat_max_size = 0, visible_children = 0;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ /* FIXME Check the child is visible. */
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->nat.width <= 0)
+ continue;
+
+ nat_box_size += child_info->nat.width;
+ nat_max_size = MAX (nat_max_size, child_info->nat.width);
+ visible_children++;
+ }
+ if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL])
+ nat_box_size = nat_max_size * visible_children;
+ folded = visible_children > 1 && allocation->width < nat_box_size;
+ }
+ else {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ /* FIXME Check the child is visible. */
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->nat.height <= 0)
+ continue;
+
+ nat_box_size += child_info->nat.height;
+ nat_max_size = MAX (nat_max_size, child_info->nat.height);
+ visible_children++;
+ }
+ if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL])
+ nat_box_size = nat_max_size * visible_children;
+ folded = visible_children > 1 && allocation->height < nat_box_size;
+ }
+ } else {
+ folded = TRUE;
+ }
+
+ hdy_stackable_box_set_folded (self, folded);
+
+ /* Allocate size to the children. */
+ if (folded)
+ hdy_stackable_box_size_allocate_folded (self, allocation);
+ else
+ hdy_stackable_box_size_allocate_unfolded (self, allocation);
+
+ /* Apply visibility and allocation. */
+ for (children = directed_children; children; children = children->next) {
+ GtkAllocation alloc;
+
+ child_info = children->data;
+
+ gtk_widget_set_child_visible (child_info->widget, child_info->visible);
+
+ if (child_info->window &&
+ child_info->visible != gdk_window_is_visible (child_info->window)) {
+ if (child_info->visible)
+ gdk_window_show (child_info->window);
+ else
+ gdk_window_hide (child_info->window);
+ }
+
+ if (!child_info->visible)
+ continue;
+
+ if (child_info->window)
+ gdk_window_move_resize (child_info->window,
+ child_info->alloc.x,
+ child_info->alloc.y,
+ child_info->alloc.width,
+ child_info->alloc.height);
+
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = child_info->alloc.width;
+ alloc.height = child_info->alloc.height;
+ gtk_widget_size_allocate (child_info->widget, &alloc);
+
+ if (gtk_widget_get_realized (widget))
+ gtk_widget_show (child_info->widget);
+ }
+
+ restack_windows (self);
+}
+
+gboolean
+hdy_stackable_box_draw (HdyStackableBox *self,
+ cairo_t *cr)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *stacked_children, *l;
+ HdyStackableBoxChildInfo *child_info, *overlap_child;
+ gboolean is_transition;
+ gboolean is_vertical;
+ gboolean is_rtl;
+ gboolean is_over;
+ GtkAllocation shadow_rect;
+ gdouble shadow_progress, mode_progress;
+ GtkPanDirection shadow_direction;
+
+ overlap_child = get_top_overlap_child (self);
+
+ is_transition = self->child_transition.is_gesture_active ||
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER ||
+ gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER;
+
+ if (!is_transition ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE ||
+ !overlap_child) {
+ for (l = self->children; l; l = l->next) {
+ child_info = l->data;
+
+ if (!gtk_cairo_should_draw_window (cr, child_info->window))
+ continue;
+
+ gtk_container_propagate_draw (self->container,
+ child_info->widget,
+ cr);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ stacked_children = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ?
+ self->children_reversed : self->children;
+
+ is_vertical = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)) == GTK_ORIENTATION_VERTICAL;
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+ is_over = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+
+ cairo_save (cr);
+
+ shadow_rect.x = 0;
+ shadow_rect.y = 0;
+ shadow_rect.width = gtk_widget_get_allocated_width (widget);
+ shadow_rect.height = gtk_widget_get_allocated_height (widget);
+
+ if (is_vertical) {
+ if (!is_over) {
+ shadow_rect.y = overlap_child->alloc.y + overlap_child->alloc.height;
+ shadow_rect.height -= shadow_rect.y;
+ shadow_direction = GTK_PAN_DIRECTION_UP;
+ mode_progress = self->mode_transition.end_progress;
+ } else {
+ shadow_rect.height = overlap_child->alloc.y;
+ shadow_direction = GTK_PAN_DIRECTION_DOWN;
+ mode_progress = self->mode_transition.start_progress;
+ }
+ } else {
+ if (is_over == is_rtl) {
+ shadow_rect.x = overlap_child->alloc.x + overlap_child->alloc.width;
+ shadow_rect.width -= shadow_rect.x;
+ shadow_direction = GTK_PAN_DIRECTION_LEFT;
+ mode_progress = self->mode_transition.end_progress;
+ } else {
+ shadow_rect.width = overlap_child->alloc.x;
+ shadow_direction = GTK_PAN_DIRECTION_RIGHT;
+ mode_progress = self->mode_transition.start_progress;
+ }
+ }
+
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER) {
+ shadow_progress = mode_progress;
+ } else {
+ GtkPanDirection direction = self->child_transition.active_direction;
+ GtkPanDirection left_or_right = is_rtl ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT;
+ gint width = gtk_widget_get_allocated_width (widget);
+ gint height = gtk_widget_get_allocated_height (widget);
+
+ if (direction == GTK_PAN_DIRECTION_UP || direction == left_or_right)
+ shadow_progress = self->child_transition.progress;
+ else
+ shadow_progress = 1 - self->child_transition.progress;
+
+ if (is_over)
+ shadow_progress = 1 - shadow_progress;
+
+ /* Normalize the shadow rect size so that we can cache the shadow */
+ if (shadow_direction == GTK_PAN_DIRECTION_RIGHT)
+ shadow_rect.x -= (width - shadow_rect.width);
+ else if (shadow_direction == GTK_PAN_DIRECTION_DOWN)
+ shadow_rect.y -= (height - shadow_rect.height);
+
+ shadow_rect.width = width;
+ shadow_rect.height = height;
+ }
+
+ cairo_rectangle (cr, shadow_rect.x, shadow_rect.y, shadow_rect.width, shadow_rect.height);
+ cairo_clip (cr);
+
+ for (l = stacked_children; l; l = l->next) {
+ child_info = l->data;
+
+ if (!gtk_cairo_should_draw_window (cr, child_info->window))
+ continue;
+
+ if (child_info == overlap_child)
+ cairo_restore (cr);
+
+ gtk_container_propagate_draw (self->container,
+ child_info->widget,
+ cr);
+ }
+
+ cairo_save (cr);
+ cairo_translate (cr, shadow_rect.x, shadow_rect.y);
+ hdy_shadow_helper_draw_shadow (self->shadow_helper, cr,
+ shadow_rect.width, shadow_rect.height,
+ shadow_progress, shadow_direction);
+ cairo_restore (cr);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+update_tracker_orientation (HdyStackableBox *self)
+{
+ gboolean reverse;
+
+ reverse = (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL);
+
+ g_object_set (self->tracker,
+ "orientation", self->orientation,
+ "reversed", reverse,
+ NULL);
+}
+
+void
+hdy_stackable_box_direction_changed (HdyStackableBox *self,
+ GtkTextDirection previous_direction)
+{
+ update_tracker_orientation (self);
+}
+
+static void
+hdy_stackable_box_child_visibility_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ GtkWidget *widget = GTK_WIDGET (obj);
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ if (self->visible_child == NULL && gtk_widget_get_visible (widget))
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+ else if (self->visible_child == child_info && !gtk_widget_get_visible (widget))
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+
+ if (child_info == self->last_visible_child) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded);
+ self->last_visible_child = NULL;
+ }
+}
+
+static void
+register_window (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+
+ attributes.x = child->alloc.x;
+ attributes.y = child->alloc.y;
+ attributes.width = child->alloc.width;
+ attributes.height = child->alloc.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ attributes.event_mask = gtk_widget_get_events (widget) |
+ gtk_widget_get_events (child->widget);
+
+ child->window = gdk_window_new (self->view_window, &attributes, attributes_mask);
+ gtk_widget_register_window (widget, child->window);
+
+ gtk_widget_set_parent_window (child->widget, child->window);
+
+ gdk_window_show (child->window);
+}
+
+static void
+unregister_window (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (!child->window)
+ return;
+
+ gtk_widget_unregister_window (widget, child->window);
+ gdk_window_destroy (child->window);
+ child->window = NULL;
+}
+
+void
+hdy_stackable_box_add (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ g_return_if_fail (gtk_widget_get_parent (widget) == NULL);
+
+ child_info = g_new0 (HdyStackableBoxChildInfo, 1);
+ child_info->widget = widget;
+ child_info->navigatable = TRUE;
+
+ self->children = g_list_append (self->children, child_info);
+ self->children_reversed = g_list_prepend (self->children_reversed, child_info);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (self->container)))
+ register_window (self, child_info);
+
+ gtk_widget_set_child_visible (widget, FALSE);
+ gtk_widget_set_parent (widget, GTK_WIDGET (self->container));
+
+ g_signal_connect (widget, "notify::visible",
+ G_CALLBACK (hdy_stackable_box_child_visibility_notify_cb), self);
+
+ if (hdy_stackable_box_get_visible_child (self) == NULL &&
+ gtk_widget_get_visible (widget)) {
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, FALSE);
+ }
+
+ if (!self->folded ||
+ (self->folded && (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] ||
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] ||
+ self->visible_child == child_info)))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+}
+
+void
+hdy_stackable_box_remove (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ g_autoptr (HdyStackableBoxChildInfo) child_info = find_child_info_for_widget (self, widget);
+ gboolean contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ self->children = g_list_remove (self->children, child_info);
+ self->children_reversed = g_list_remove (self->children_reversed, child_info);
+
+ g_signal_handlers_disconnect_by_func (widget,
+ hdy_stackable_box_child_visibility_notify_cb,
+ self);
+
+ if (hdy_stackable_box_get_visible_child (self) == widget)
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+
+ if (child_info == self->last_visible_child)
+ self->last_visible_child = NULL;
+
+ if (gtk_widget_get_visible (widget))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+
+ unregister_window (self, child_info);
+
+ gtk_widget_unparent (widget);
+}
+
+void
+hdy_stackable_box_forall (HdyStackableBox *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ /* This shallow copy is needed when the callback changes the list while we are
+ * looping through it, for example by calling hdy_stackable_box_remove() on all
+ * children when destroying the HdyStackableBox_private_offset.
+ */
+ g_autoptr (GList) children_copy = g_list_copy (self->children);
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = children_copy; children; children = children->next) {
+ child_info = children->data;
+
+ (* callback) (child_info->widget, callback_data);
+ }
+
+ g_list_free (self->children_reversed);
+ self->children_reversed = g_list_copy (self->children);
+ self->children_reversed = g_list_reverse (self->children_reversed);
+}
+
+static void
+hdy_stackable_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ switch (prop_id) {
+ case PROP_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_folded (self));
+ break;
+ case PROP_HHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_stackable_box_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_stackable_box_get_transition_type (self));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_stackable_box_get_mode_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_stackable_box_get_child_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_stackable_box_get_child_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_stackable_box_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_stackable_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS_FOLDED:
+ hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_stackable_box_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_stackable_box_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_stackable_box_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ hdy_stackable_box_set_mode_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ hdy_stackable_box_set_child_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_stackable_box_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_stackable_box_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_stackable_box_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_stackable_box_finalize (GObject *object)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ self->visible_child = NULL;
+
+ if (self->shadow_helper)
+ g_clear_object (&self->shadow_helper);
+
+ hdy_stackable_box_unschedule_child_ticks (self);
+
+ G_OBJECT_CLASS (hdy_stackable_box_parent_class)->finalize (object);
+}
+
+void
+hdy_stackable_box_realize (HdyStackableBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+ GList *children;
+
+ gtk_widget_set_realized (widget, TRUE);
+ gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget)));
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ self->view_window = gdk_window_new (gtk_widget_get_window (widget),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->view_window);
+
+ for (children = self->children; children != NULL; children = children->next)
+ register_window (self, children->data);
+}
+
+void
+hdy_stackable_box_unrealize (HdyStackableBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *children;
+
+ for (children = self->children; children != NULL; children = children->next)
+ unregister_window (self, children->data);
+
+ gtk_widget_unregister_window (widget, self->view_window);
+ gdk_window_destroy (self->view_window);
+ self->view_window = NULL;
+
+ GTK_WIDGET_CLASS (self->klass)->unrealize (widget);
+}
+
+void
+hdy_stackable_box_map (HdyStackableBox *self)
+{
+ GTK_WIDGET_CLASS (self->klass)->map (GTK_WIDGET (self->container));
+
+ gdk_window_show (self->view_window);
+}
+
+void
+hdy_stackable_box_unmap (HdyStackableBox *self)
+{
+ gdk_window_hide (self->view_window);
+
+ GTK_WIDGET_CLASS (self->klass)->unmap (GTK_WIDGET (self->container));
+}
+
+HdySwipeTracker *
+hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self)
+{
+ return self->tracker;
+}
+
+gdouble
+hdy_stackable_box_get_distance (HdyStackableBox *self)
+{
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+ return gtk_widget_get_allocated_width (GTK_WIDGET (self->container));
+ else
+ return gtk_widget_get_allocated_height (GTK_WIDGET (self->container));
+}
+
+static gboolean
+can_swipe_in_direction (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ switch (direction) {
+ case HDY_NAVIGATION_DIRECTION_BACK:
+ return self->child_transition.can_swipe_back;
+ case HDY_NAVIGATION_DIRECTION_FORWARD:
+ return self->child_transition.can_swipe_forward;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+gdouble *
+hdy_stackable_box_get_snap_points (HdyStackableBox *self,
+ gint *n_snap_points)
+{
+ gint n;
+ gdouble *points, lower, upper;
+
+ if (self->child_transition.tick_id > 0 ||
+ self->child_transition.is_gesture_active) {
+ gint current_direction;
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ switch (self->child_transition.active_direction) {
+ case GTK_PAN_DIRECTION_UP:
+ current_direction = 1;
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ current_direction = -1;
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ current_direction = is_rtl ? -1 : 1;
+ break;
+ case GTK_PAN_DIRECTION_RIGHT:
+ current_direction = is_rtl ? 1 : -1;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ lower = MIN (0, current_direction);
+ upper = MAX (0, current_direction);
+ } else {
+ HdyStackableBoxChildInfo *child = NULL;
+
+ if ((can_swipe_in_direction (self, self->child_transition.swipe_direction) ||
+ !self->child_transition.is_direct_swipe) && self->folded)
+ child = find_swipeable_child (self, self->child_transition.swipe_direction);
+
+ lower = MIN (0, child ? self->child_transition.swipe_direction : 0);
+ upper = MAX (0, child ? self->child_transition.swipe_direction : 0);
+ }
+
+ n = (lower != upper) ? 2 : 1;
+
+ points = g_new0 (gdouble, n);
+ points[0] = lower;
+ points[n - 1] = upper;
+
+ if (n_snap_points)
+ *n_snap_points = n;
+
+ return points;
+}
+
+gdouble
+hdy_stackable_box_get_progress (HdyStackableBox *self)
+{
+ gboolean new_first = FALSE;
+ GList *children;
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ for (children = self->children; children; children = children->next) {
+ if (self->last_visible_child == children->data) {
+ new_first = TRUE;
+
+ break;
+ }
+ if (self->visible_child == children->data)
+ break;
+ }
+
+ return self->child_transition.progress * (new_first ? 1 : -1);
+}
+
+gdouble
+hdy_stackable_box_get_cancel_progress (HdyStackableBox *self)
+{
+ return 0;
+}
+
+void
+hdy_stackable_box_get_swipe_area (HdyStackableBox *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ gint width = gtk_widget_get_allocated_width (GTK_WIDGET (self->container));
+ gint height = gtk_widget_get_allocated_height (GTK_WIDGET (self->container));
+ gdouble progress = 0;
+
+ rect->x = 0;
+ rect->y = 0;
+ rect->width = width;
+ rect->height = height;
+
+ if (!is_drag)
+ return;
+
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE)
+ return;
+
+ if (self->child_transition.is_gesture_active ||
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER)
+ progress = self->child_transition.progress;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) {
+ rect->width = MAX (progress * width, HDY_SWIPE_BORDER);
+ rect->x = is_rtl ? 0 : width - rect->width;
+ } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) {
+ rect->width = MAX (progress * width, HDY_SWIPE_BORDER);
+ rect->x = is_rtl ? width - rect->width : 0;
+ }
+ } else {
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) {
+ rect->height = MAX (progress * height, HDY_SWIPE_BORDER);
+ rect->y = height - rect->height;
+ } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) {
+ rect->height = MAX (progress * height, HDY_SWIPE_BORDER);
+ rect->y = 0;
+ }
+ }
+}
+
+void
+hdy_stackable_box_switch_child (HdyStackableBox *self,
+ guint index,
+ gint64 duration)
+{
+ HdyStackableBoxChildInfo *child_info = NULL;
+ GList *children;
+ guint i = 0;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->navigatable)
+ continue;
+
+ if (i == index)
+ break;
+
+ i++;
+ }
+
+ if (child_info == NULL) {
+ g_critical ("Couldn't find eligible child with index %u", index);
+ return;
+ }
+
+ set_visible_child_info (self, child_info, self->transition_type,
+ duration, FALSE);
+}
+
+static void
+begin_swipe_cb (HdySwipeTracker *tracker,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdyStackableBox *self)
+{
+ self->child_transition.is_direct_swipe = direct;
+ self->child_transition.swipe_direction = direction;
+
+ if (self->child_transition.tick_id > 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self->container),
+ self->child_transition.tick_id);
+ self->child_transition.tick_id = 0;
+ self->child_transition.is_gesture_active = TRUE;
+ self->child_transition.is_cancelled = FALSE;
+ } else {
+ HdyStackableBoxChildInfo *child;
+
+ if ((can_swipe_in_direction (self, direction) || !direct) && self->folded)
+ child = find_swipeable_child (self, direction);
+ else
+ child = NULL;
+
+ if (child) {
+ self->child_transition.is_gesture_active = TRUE;
+ set_visible_child_info (self, child, self->transition_type,
+ self->child_transition.duration, FALSE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+ }
+}
+
+static void
+update_swipe_cb (HdySwipeTracker *tracker,
+ gdouble progress,
+ HdyStackableBox *self)
+{
+ self->child_transition.progress = ABS (progress);
+ hdy_stackable_box_child_progress_updated (self);
+}
+
+static void
+end_swipe_cb (HdySwipeTracker *tracker,
+ gint64 duration,
+ gdouble to,
+ HdyStackableBox *self)
+{
+ if (!self->child_transition.is_gesture_active)
+ return;
+
+ self->child_transition.start_progress = self->child_transition.progress;
+ self->child_transition.end_progress = ABS (to);
+ self->child_transition.is_cancelled = (to == 0);
+ self->child_transition.first_frame_skipped = TRUE;
+
+ hdy_stackable_box_schedule_child_ticks (self);
+ if (hdy_get_enable_animations (GTK_WIDGET (self->container)) && duration != 0) {
+ gtk_progress_tracker_start (&self->child_transition.tracker,
+ duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ self->child_transition.progress = self->child_transition.end_progress;
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ }
+
+ self->child_transition.is_gesture_active = FALSE;
+ hdy_stackable_box_child_progress_updated (self);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self->container));
+}
+
+GtkOrientation
+hdy_stackable_box_get_orientation (HdyStackableBox *self)
+{
+ return self->orientation;
+}
+
+void
+hdy_stackable_box_set_orientation (HdyStackableBox *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ update_tracker_orientation (self);
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+const gchar *
+hdy_stackable_box_get_child_name (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_val_if_fail (child_info != NULL, NULL);
+
+ return child_info->name;
+}
+
+void
+hdy_stackable_box_set_child_name (HdyStackableBox *self,
+ GtkWidget *widget,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+ HdyStackableBoxChildInfo *child_info2;
+ GList *children;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_if_fail (child_info != NULL);
+
+ for (children = self->children; children; children = children->next) {
+ child_info2 = children->data;
+
+ if (child_info == child_info2)
+ continue;
+ if (g_strcmp0 (child_info2->name, name) == 0) {
+ g_warning ("Duplicate child name in HdyStackableBox: %s", name);
+
+ break;
+ }
+ }
+
+ g_free (child_info->name);
+ child_info->name = g_strdup (name);
+
+ if (self->visible_child == child_info)
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_VISIBLE_CHILD_NAME]);
+}
+
+gboolean
+hdy_stackable_box_get_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_val_if_fail (child_info != NULL, FALSE);
+
+ return child_info->navigatable;
+}
+
+void
+hdy_stackable_box_set_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget,
+ gboolean navigatable)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_if_fail (child_info != NULL);
+
+ child_info->navigatable = navigatable;
+
+ if (!child_info->navigatable &&
+ hdy_stackable_box_get_visible_child (self) == widget)
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+static void
+hdy_stackable_box_class_init (HdyStackableBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = hdy_stackable_box_get_property;
+ object_class->set_property = hdy_stackable_box_set_property;
+ object_class->finalize = hdy_stackable_box_finalize;
+
+ /**
+ * HdyStackableBox:folded:
+ *
+ * %TRUE if the widget is folded.
+ *
+ * The #HdyStackableBox will be folded if the size allocated to it is smaller
+ * than the sum of the natural size of its children, it will be unfolded
+ * otherwise.
+ */
+ props[PROP_FOLDED] =
+ g_param_spec_boolean ("folded",
+ _("Folded"),
+ _("Whether the widget is folded"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:hhomogeneous_folded:
+ *
+ * %TRUE if the widget allocates the same width for all children when folded.
+ */
+ props[PROP_HHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("hhomogeneous-folded",
+ _("Horizontally homogeneous folded"),
+ _("Horizontally homogeneous sizing when the widget is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:vhomogeneous_folded:
+ *
+ * %TRUE if the widget allocates the same height for all children when folded.
+ */
+ props[PROP_VHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("vhomogeneous-folded",
+ _("Vertically homogeneous folded"),
+ _("Vertically homogeneous sizing when the widget is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:hhomogeneous_unfolded:
+ *
+ * %TRUE if the widget allocates the same width for all children when unfolded.
+ */
+ props[PROP_HHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("hhomogeneous-unfolded",
+ _("Box horizontally homogeneous"),
+ _("Horizontally homogeneous sizing when the widget is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:vhomogeneous_unfolded:
+ *
+ * %TRUE if the widget allocates the same height for all children when unfolded.
+ */
+ props[PROP_VHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("vhomogeneous-unfolded",
+ _("Box vertically homogeneous"),
+ _("Vertically homogeneous sizing when the widget is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible when the widget is folded"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible when the children are stacked"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:transition-type:
+ *
+ * The type of animation that will be used for transitions between modes and
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about
+ * to become current.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between modes and children"),
+ HDY_TYPE_STACKABLE_BOX_TRANSITION_TYPE, HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_MODE_TRANSITION_DURATION] =
+ g_param_spec_uint ("mode-transition-duration",
+ _("Mode transition duration"),
+ _("The mode transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 250,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_DURATION] =
+ g_param_spec_uint ("child-transition-duration",
+ _("Child transition duration"),
+ _("The child transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("child-transition-running",
+ _("Child transition running"),
+ _("Whether or not the child transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:can-swipe-back:
+ *
+ * Whether or not the widget allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:can-swipe-forward:
+ *
+ * Whether or not the widget allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_ORIENTATION] =
+ g_param_spec_enum ("orientation",
+ _("Orientation"),
+ _("Orientation"),
+ GTK_TYPE_ORIENTATION,
+ GTK_ORIENTATION_HORIZONTAL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+HdyStackableBox *
+hdy_stackable_box_new (GtkContainer *container,
+ GtkContainerClass *klass,
+ gboolean can_unfold)
+{
+ GtkWidget *widget;
+ HdyStackableBox *self;
+
+ g_return_val_if_fail (GTK_IS_CONTAINER (container), NULL);
+ g_return_val_if_fail (GTK_IS_ORIENTABLE (container), NULL);
+ g_return_val_if_fail (GTK_IS_CONTAINER_CLASS (klass), NULL);
+
+ widget = GTK_WIDGET (container);
+ self = g_object_new (HDY_TYPE_STACKABLE_BOX, NULL);
+
+ self->container = container;
+ self->klass = klass;
+ self->can_unfold = can_unfold;
+
+ self->children = NULL;
+ self->children_reversed = NULL;
+ self->visible_child = NULL;
+ self->folded = FALSE;
+ self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] = FALSE;
+ self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] = FALSE;
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] = TRUE;
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] = TRUE;
+ self->transition_type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ self->mode_transition.duration = 250;
+ self->child_transition.duration = 200;
+ self->mode_transition.current_pos = 1.0;
+ self->mode_transition.target_pos = 1.0;
+
+ self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self->container));
+
+ g_object_set (self->tracker, "orientation", self->orientation, "enabled", FALSE, NULL);
+
+ g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0);
+
+ self->shadow_helper = hdy_shadow_helper_new (widget);
+
+ gtk_widget_set_has_window (widget, FALSE);
+ gtk_widget_set_can_focus (widget, FALSE);
+ gtk_widget_set_redraw_on_allocate (widget, FALSE);
+
+ if (can_unfold) {
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gtk_style_context_add_class (context, "unfolded");
+ }
+
+ return self;
+}
+
+static void
+hdy_stackable_box_init (HdyStackableBox *self)
+{
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-group.c b/subprojects/libhandy/src/hdy-swipe-group.c
new file mode 100644
index 0000000..2779a31
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-group.c
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-swipe-group.h"
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker-private.h"
+
+#define BUILDABLE_TAG_OBJECT "object"
+#define BUILDABLE_TAG_SWIPEABLE "swipeable"
+#define BUILDABLE_TAG_SWIPEABLES "swipeables"
+#define BUILDABLE_TAG_TEMPLATE "template"
+
+/**
+ * SECTION:hdy-swipe-group
+ * @short_description: An object for syncing swipeable widgets.
+ * @title: HdySwipeGroup
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable
+ *
+ * The #HdySwipeGroup object can be used to sync multiple swipeable widgets
+ * that implement the #HdySwipeable interface, such as #HdyCarousel, so that
+ * animating one of them also animates all the other widgets in the group.
+ *
+ * This can be useful for syncing widgets between a window's titlebar and
+ * content area.
+ *
+ * # #HdySwipeGroup as #GtkBuildable
+ *
+ * #HdySwipeGroup can be created in an UI definition. The list of swipeable
+ * widgets is specified with a &lt;swipeables&gt; element containing multiple
+ * &lt;swipeable&gt; elements with their ”name” attribute specifying the id of
+ * the widgets.
+ *
+ * |[
+ * <object class="HdySwipeGroup">
+ * <swipeables>
+ * <swipeable name="carousel1"/>
+ * <swipeable name="carousel2"/>
+ * </swipeables>
+ * </object>
+ * ]|
+ *
+ * Since: 0.0.12
+ */
+
+struct _HdySwipeGroup
+{
+ GObject parent_instance;
+
+ GSList *swipeables;
+ HdySwipeable *current;
+ gboolean block;
+};
+
+static void hdy_swipe_group_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdySwipeGroup, hdy_swipe_group, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_swipe_group_buildable_init))
+
+static gboolean
+contains (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ GSList *swipeables;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data == swipeable)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+swipeable_destroyed (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+
+ self->swipeables = g_slist_remove (self->swipeables, swipeable);
+
+ g_object_unref (self);
+}
+
+/**
+ * hdy_swipe_group_new:
+ *
+ * Create a new #HdySwipeGroup object.
+ *
+ * Returns: The newly created #HdySwipeGroup object
+ *
+ * Since: 0.0.12
+ */
+HdySwipeGroup *
+hdy_swipe_group_new (void)
+{
+ return g_object_new (HDY_TYPE_SWIPE_GROUP, NULL);
+}
+
+static void
+child_switched_cb (HdySwipeGroup *self,
+ guint index,
+ gint64 duration,
+ HdySwipeable *swipeable)
+{
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ if (self->current != NULL && self->current != swipeable)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipeable_switch_child (swipeables->data, index, duration);
+
+ self->block = FALSE;
+}
+
+static void
+begin_swipe_cb (HdySwipeGroup *self,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (self->current != NULL && self->current != swipeable)
+ return;
+
+ self->current = swipeable;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_begin_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ direction, FALSE);
+
+ self->block = FALSE;
+}
+
+static void
+update_swipe_cb (HdySwipeGroup *self,
+ gdouble progress,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (swipeable != self->current)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_update_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ progress);
+
+ self->block = FALSE;
+}
+
+static void
+end_swipe_cb (HdySwipeGroup *self,
+ gint64 duration,
+ gdouble to,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (swipeable != self->current)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_end_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ duration, to);
+
+ self->current = NULL;
+
+ self->block = FALSE;
+}
+
+/**
+ * hdy_swipe_group_add_swipeable:
+ * @self: a #HdySwipeGroup
+ * @swipeable: the #HdySwipeable to add
+ *
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the swipe group.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_swipe_group_add_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ HdySwipeTracker *tracker;
+
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+ g_return_if_fail (HDY_IS_SWIPEABLE (swipeable));
+
+ tracker = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (tracker));
+
+ g_signal_connect_swapped (swipeable, "child-switched", G_CALLBACK (child_switched_cb), self);
+ g_signal_connect_swapped (tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self);
+ g_signal_connect_swapped (tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self);
+ g_signal_connect_swapped (tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self);
+
+ self->swipeables = g_slist_prepend (self->swipeables, swipeable);
+
+ g_object_ref (self);
+
+ g_signal_connect_swapped (swipeable, "destroy", G_CALLBACK (swipeable_destroyed), self);
+}
+
+
+/**
+ * hdy_swipe_group_remove_swipeable:
+ * @self: a #HdySwipeGroup
+ * @swipeable: the #HdySwipeable to remove
+ *
+ * Removes a widget from a #HdySwipeGroup.
+ *
+ * Since: 0.0.12
+ **/
+void
+hdy_swipe_group_remove_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ HdySwipeTracker *tracker;
+
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+ g_return_if_fail (HDY_IS_SWIPEABLE (swipeable));
+ g_return_if_fail (contains (self, swipeable));
+
+ tracker = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ self->swipeables = g_slist_remove (self->swipeables, swipeable);
+
+ g_signal_handlers_disconnect_by_data (swipeable, self);
+ g_signal_handlers_disconnect_by_data (tracker, self);
+
+ g_object_unref (self);
+}
+
+
+/**
+ * hdy_swipe_group_get_swipeables:
+ * @self: a #HdySwipeGroup
+ *
+ * Returns the list of swipeables associated with @self.
+ *
+ * Returns: (element-type HdySwipeable) (transfer none): a #GSList of
+ * swipeables. The list is owned by libhandy and should not be modified.
+ *
+ * Since: 0.0.12
+ **/
+GSList *
+hdy_swipe_group_get_swipeables (HdySwipeGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_GROUP (self), NULL);
+
+ return self->swipeables;
+}
+
+typedef struct {
+ gchar *name;
+ gint line;
+ gint col;
+} ItemData;
+
+static void
+item_data_free (gpointer data)
+{
+ ItemData *item_data = data;
+
+ g_free (item_data->name);
+ g_free (item_data);
+}
+
+typedef struct {
+ GObject *object;
+ GtkBuilder *builder;
+ GSList *items;
+} GSListSubParserData;
+
+static void
+hdy_swipe_group_dispose (GObject *object)
+{
+ HdySwipeGroup *self = (HdySwipeGroup *)object;
+
+ g_slist_free_full (self->swipeables, (GDestroyNotify) g_object_unref);
+ self->swipeables = NULL;
+
+ G_OBJECT_CLASS (hdy_swipe_group_parent_class)->dispose (object);
+}
+
+/*< private >
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @parent_name: the name of the expected parent element
+ * @error: return location for an error
+ *
+ * Checks that the parent element of the currently handled
+ * start tag is @parent_name and set @error if it isn't.
+ *
+ * This is intended to be called in start_element vfuncs to
+ * ensure that element nesting is as intended.
+ *
+ * Returns: %TRUE if @parent_name is the parent element
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static gboolean
+_gtk_builder_check_parent (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *parent_name,
+ GError **error)
+{
+ const GSList *stack;
+ gint line, col;
+ const gchar *parent;
+ const gchar *element;
+
+ stack = g_markup_parse_context_get_element_stack (context);
+
+ element = (const gchar *)stack->data;
+ parent = stack->next ? (const gchar *)stack->next->data : "";
+
+ if (g_str_equal (parent_name, parent) ||
+ (g_str_equal (parent_name, BUILDABLE_TAG_OBJECT) &&
+ g_str_equal (parent, BUILDABLE_TAG_TEMPLATE)))
+ return TRUE;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_INVALID_TAG,
+ ".:%d:%d Can't use <%s> here",
+ line, col, element);
+
+ return FALSE;
+}
+
+/*< private >
+ * _gtk_builder_prefix_error:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @error: an error
+ *
+ * Calls g_prefix_error() to prepend a filename:line:column marker
+ * to the given error. The filename is taken from @builder, and
+ * the line and column are obtained by calling
+ * g_markup_parse_context_get_position().
+ *
+ * This is intended to be called on errors returned by
+ * g_markup_collect_attributes() in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_prefix_error (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_prefix_error (error, ".:%d:%d ", line, col);
+}
+
+/*< private >
+ * _gtk_builder_error_unhandled_tag:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @object: name of the object that is being handled
+ * @element_name: name of the element whose start tag is being handled
+ * @error: return location for the error
+ *
+ * Sets @error to a suitable error indicating that an @element_name
+ * tag is not expected in the custom markup for @object.
+ *
+ * This is intended to be called in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *object,
+ const gchar *element_name,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_UNHANDLED_TAG,
+ ".:%d:%d Unsupported tag for %s: <%s>",
+ line, col,
+ object, element_name);
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+swipe_group_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **names,
+ const gchar **values,
+ gpointer user_data,
+ GError **error)
+{
+ GSListSubParserData *data = (GSListSubParserData*)user_data;
+
+ if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLE) == 0)
+ {
+ const gchar *name;
+ ItemData *item_data;
+
+ if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_SWIPEABLES, error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_STRING, "name", &name,
+ G_MARKUP_COLLECT_INVALID))
+ {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ item_data = g_new (ItemData, 1);
+ item_data->name = g_strdup (name);
+ g_markup_parse_context_get_position (context, &item_data->line, &item_data->col);
+ data->items = g_slist_prepend (data->items, item_data);
+ }
+ else if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLES) == 0)
+ {
+ if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_OBJECT, error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_INVALID, NULL, NULL,
+ G_MARKUP_COLLECT_INVALID))
+ _gtk_builder_prefix_error (data->builder, context, error);
+ }
+ else
+ {
+ _gtk_builder_error_unhandled_tag (data->builder, context,
+ "HdySwipeGroup", element_name,
+ error);
+ }
+}
+
+
+/* This has been copied and modified from gtksizegroup.c. */
+static const GMarkupParser swipe_group_parser =
+ {
+ swipe_group_start_element
+ };
+
+/* This has been copied and modified from gtksizegroup.c. */
+static gboolean
+hdy_swipe_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *parser_data)
+{
+ GSListSubParserData *data;
+
+ if (child)
+ return FALSE;
+
+ if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) == 0)
+ {
+ data = g_slice_new0 (GSListSubParserData);
+ data->items = NULL;
+ data->object = G_OBJECT (buildable);
+ data->builder = builder;
+
+ *parser = swipe_group_parser;
+ *parser_data = data;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+hdy_swipe_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data)
+{
+ GSList *l;
+ GSListSubParserData *data;
+ GObject *object;
+
+ if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) != 0)
+ return;
+
+ data = (GSListSubParserData*)user_data;
+ data->items = g_slist_reverse (data->items);
+
+ for (l = data->items; l; l = l->next)
+ {
+ ItemData *item_data = l->data;
+ object = gtk_builder_get_object (builder, item_data->name);
+ if (!object)
+ continue;
+ hdy_swipe_group_add_swipeable (HDY_SWIPE_GROUP (data->object), HDY_SWIPEABLE (object));
+ }
+ g_slist_free_full (data->items, item_data_free);
+ g_slice_free (GSListSubParserData, data);
+}
+
+static void
+hdy_swipe_group_class_init (HdySwipeGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_swipe_group_dispose;
+}
+
+static void
+hdy_swipe_group_init (HdySwipeGroup *self)
+{
+}
+
+static void
+hdy_swipe_group_buildable_init (GtkBuildableIface *iface)
+{
+ iface->custom_tag_start = hdy_swipe_group_buildable_custom_tag_start;
+ iface->custom_finished = hdy_swipe_group_buildable_custom_finished;
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-group.h b/subprojects/libhandy/src/hdy-swipe-group.h
new file mode 100644
index 0000000..791962e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-group.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib-object.h>
+#include "hdy-swipeable.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPE_GROUP (hdy_swipe_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySwipeGroup, hdy_swipe_group, HDY, SWIPE_GROUP, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeGroup *hdy_swipe_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_group_add_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable);
+HDY_AVAILABLE_IN_ALL
+GSList * hdy_swipe_group_get_swipeables (HdySwipeGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_group_remove_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker-private.h b/subprojects/libhandy/src/hdy-swipe-tracker-private.h
new file mode 100644
index 0000000..d4b5541
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-swipe-tracker.h"
+
+G_BEGIN_DECLS
+
+void hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean direct);
+void hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self,
+ gdouble progress);
+void hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self,
+ gint64 duration,
+ gdouble to);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.c b/subprojects/libhandy/src/hdy-swipe-tracker.c
new file mode 100644
index 0000000..0cbf4a4
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker.c
@@ -0,0 +1,1113 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-swipe-tracker-private.h"
+#include "hdy-navigation-direction.h"
+
+#include <math.h>
+
+#define TOUCHPAD_BASE_DISTANCE_H 400
+#define TOUCHPAD_BASE_DISTANCE_V 300
+#define SCROLL_MULTIPLIER 10
+#define MIN_ANIMATION_DURATION 100
+#define MAX_ANIMATION_DURATION 400
+#define VELOCITY_THRESHOLD 0.4
+#define DURATION_MULTIPLIER 3
+#define ANIMATION_BASE_VELOCITY 0.002
+#define DRAG_THRESHOLD_DISTANCE 5
+
+/**
+ * SECTION:hdy-swipe-tracker
+ * @short_description: Swipe tracker used in #HdyCarousel and #HdyLeaflet
+ * @title: HdySwipeTracker
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable
+ *
+ * The HdySwipeTracker object can be used for implementing widgets with swipe
+ * gestures. It supports touch-based swipes, pointer dragging, and touchpad
+ * scrolling.
+ *
+ * The widgets will probably want to expose #HdySwipeTracker:enabled property.
+ * If they expect to use horizontal orientation, #HdySwipeTracker:reversed
+ * property can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+
+typedef enum {
+ HDY_SWIPE_TRACKER_STATE_NONE,
+ HDY_SWIPE_TRACKER_STATE_PENDING,
+ HDY_SWIPE_TRACKER_STATE_SCROLLING,
+ HDY_SWIPE_TRACKER_STATE_FINISHING,
+ HDY_SWIPE_TRACKER_STATE_REJECTED,
+} HdySwipeTrackerState;
+
+struct _HdySwipeTracker
+{
+ GObject parent_instance;
+
+ HdySwipeable *swipeable;
+ gboolean enabled;
+ gboolean reversed;
+ gboolean allow_mouse_drag;
+ GtkOrientation orientation;
+
+ gint start_x;
+ gint start_y;
+
+ guint32 prev_time;
+ gdouble velocity;
+
+ gdouble initial_progress;
+ gdouble progress;
+ gboolean cancelled;
+
+ gdouble prev_offset;
+
+ gboolean is_scrolling;
+
+ HdySwipeTrackerState state;
+ GtkGesture *touch_gesture;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdySwipeTracker, hdy_swipe_tracker, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+ PROP_0,
+ PROP_SWIPEABLE,
+ PROP_ENABLED,
+ PROP_REVERSED,
+ PROP_ALLOW_MOUSE_DRAG,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ALLOW_MOUSE_DRAG + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_BEGIN_SWIPE,
+ SIGNAL_UPDATE_SWIPE,
+ SIGNAL_END_SWIPE,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+reset (HdySwipeTracker *self)
+{
+ self->state = HDY_SWIPE_TRACKER_STATE_NONE;
+
+ self->prev_offset = 0;
+
+ self->initial_progress = 0;
+ self->progress = 0;
+
+ self->start_x = 0;
+ self->start_y = 0;
+
+ self->prev_time = 0;
+ self->velocity = 0;
+
+ self->cancelled = FALSE;
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+}
+
+static void
+get_range (HdySwipeTracker *self,
+ gdouble *first,
+ gdouble *last)
+{
+ g_autofree gdouble *points = NULL;
+ gint n;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ *first = points[0];
+ *last = points[n - 1];
+}
+
+static void
+gesture_prepare (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean is_drag)
+{
+ GdkRectangle rect;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ hdy_swipeable_get_swipe_area (self->swipeable, direction, is_drag, &rect);
+
+ if (self->start_x < rect.x ||
+ self->start_x >= rect.x + rect.width ||
+ self->start_y < rect.y ||
+ self->start_y >= rect.y + rect.height) {
+ self->state = HDY_SWIPE_TRACKER_STATE_REJECTED;
+
+ return;
+ }
+
+ hdy_swipe_tracker_emit_begin_swipe (self, direction, TRUE);
+
+ self->initial_progress = hdy_swipeable_get_progress (self->swipeable);
+ self->progress = self->initial_progress;
+ self->velocity = 0;
+ self->state = HDY_SWIPE_TRACKER_STATE_PENDING;
+}
+
+static void
+gesture_begin (HdySwipeTracker *self)
+{
+ GdkEvent *event;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING)
+ return;
+
+ event = gtk_get_current_event ();
+ self->prev_time = gdk_event_get_time (event);
+ self->state = HDY_SWIPE_TRACKER_STATE_SCROLLING;
+
+ gtk_grab_add (GTK_WIDGET (self->swipeable));
+}
+
+static void
+gesture_update (HdySwipeTracker *self,
+ gdouble delta)
+{
+ GdkEvent *event;
+ guint32 time;
+ gdouble progress;
+ gdouble first_point, last_point;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ event = gtk_get_current_event ();
+ time = gdk_event_get_time (event);
+ if (time != self->prev_time)
+ self->velocity = delta / (time - self->prev_time);
+
+ get_range (self, &first_point, &last_point);
+
+ progress = self->progress + delta;
+ progress = CLAMP (progress, first_point, last_point);
+
+ /* FIXME: this is a hack to prevent swiping more than 1 page at once */
+ progress = CLAMP (progress, self->initial_progress - 1, self->initial_progress + 1);
+
+ self->progress = progress;
+
+ hdy_swipe_tracker_emit_update_swipe (self, progress);
+
+ self->prev_time = time;
+}
+
+static void
+get_closest_snap_points (HdySwipeTracker *self,
+ gdouble *upper,
+ gdouble *lower)
+{
+ gint i, n;
+ gdouble *points;
+
+ *upper = 0;
+ *lower = 0;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ for (i = 0; i < n; i++) {
+ if (points[i] >= self->progress) {
+ *upper = points[i];
+ break;
+ }
+ }
+
+ for (i = n - 1; i >= 0; i--) {
+ if (points[i] <= self->progress) {
+ *lower = points[i];
+ break;
+ }
+ }
+
+ g_free (points);
+}
+
+static gdouble
+get_end_progress (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble upper, lower, middle;
+
+ if (self->cancelled)
+ return hdy_swipeable_get_cancel_progress (self->swipeable);
+
+ get_closest_snap_points (self, &upper, &lower);
+ middle = (upper + lower) / 2;
+
+ if (self->progress > middle)
+ return (self->velocity * distance > -VELOCITY_THRESHOLD ||
+ self->initial_progress > upper) ? upper : lower;
+
+ return (self->velocity * distance < VELOCITY_THRESHOLD ||
+ self->initial_progress < lower) ? lower : upper;
+}
+
+static void
+gesture_end (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble end_progress, velocity;
+ gint64 duration;
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ end_progress = get_end_progress (self, distance);
+
+ velocity = ANIMATION_BASE_VELOCITY;
+ if ((end_progress - self->progress) * self->velocity > 0)
+ velocity = self->velocity;
+
+ duration = ABS ((self->progress - end_progress) / velocity * DURATION_MULTIPLIER);
+ if (self->progress != end_progress)
+ duration = CLAMP (duration, MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION);
+
+ hdy_swipe_tracker_emit_end_swipe (self, duration, end_progress);
+
+ if (self->cancelled)
+ reset (self);
+ else
+ self->state = HDY_SWIPE_TRACKER_STATE_FINISHING;
+}
+
+static void
+gesture_cancel (HdySwipeTracker *self,
+ gdouble distance)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->cancelled = TRUE;
+ gesture_end (self, distance);
+}
+
+static void
+drag_begin_cb (HdySwipeTracker *self,
+ gdouble start_x,
+ gdouble start_y,
+ GtkGestureDrag *gesture)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ self->start_x = start_x;
+ self->start_y = start_y;
+}
+
+static void
+drag_update_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble offset, distance;
+ gboolean is_vertical, is_offset_vertical;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ if (is_vertical)
+ offset = -offset_y / distance;
+ else
+ offset = -offset_x / distance;
+
+ if (self->reversed)
+ offset = -offset;
+
+ is_offset_vertical = (ABS (offset_y) > ABS (offset_x));
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (is_vertical == is_offset_vertical)
+ gesture_prepare (self, offset > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, TRUE);
+ else
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gdouble drag_distance;
+ gdouble first_point, last_point;
+ gboolean is_overshooting;
+
+ get_range (self, &first_point, &last_point);
+
+ drag_distance = sqrt (offset_x * offset_x + offset_y * offset_y);
+ is_overshooting = (offset < 0 && self->progress <= first_point) ||
+ (offset > 0 && self->progress >= last_point);
+
+ if (drag_distance >= DRAG_THRESHOLD_DISTANCE) {
+ if ((is_vertical == is_offset_vertical) && !is_overshooting) {
+ gesture_begin (self);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ } else {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ }
+ }
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_update (self, offset - self->prev_offset);
+ self->prev_offset = offset;
+ }
+}
+
+static void
+drag_end_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ reset (self);
+ return;
+ }
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ gesture_end (self, distance);
+}
+
+static void
+drag_cancel_cb (HdySwipeTracker *self,
+ GdkEventSequence *sequence,
+ GtkGesture *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static gboolean
+handle_scroll_event (HdySwipeTracker *self,
+ GdkEvent *event,
+ gboolean capture)
+{
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ gdouble dx, dy, delta, distance;
+ gboolean is_vertical;
+ gboolean is_delta_vertical;
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ distance = is_vertical ? TOUCHPAD_BASE_DISTANCE_V : TOUCHPAD_BASE_DISTANCE_H;
+
+ if (gdk_event_get_scroll_direction (event, NULL))
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device (event);
+ input_source = gdk_device_get_source (source_device);
+ if (input_source != GDK_SOURCE_TOUCHPAD)
+ return GDK_EVENT_PROPAGATE;
+
+ gdk_event_get_scroll_deltas (event, &dx, &dy);
+ delta = is_vertical ? dy : dx;
+ if (self->reversed)
+ delta = -delta;
+
+ is_delta_vertical = (ABS (dy) > ABS (dx));
+
+ if (self->is_scrolling) {
+ gesture_cancel (self, distance);
+
+ if (gdk_event_is_scroll_stop_event (event))
+ self->is_scrolling = FALSE;
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ if (gdk_event_is_scroll_stop_event (event))
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (gdk_event_is_scroll_stop_event (event))
+ return GDK_EVENT_PROPAGATE;
+
+ if (is_vertical == is_delta_vertical) {
+ if (!capture) {
+ GtkWidget *widget = gtk_get_event_widget (event);
+ gdouble event_x, event_y;
+
+ gdk_event_get_coords (event, &event_x, &event_y);
+ gtk_widget_translate_coordinates (widget, GTK_WIDGET (self->swipeable),
+ event_x, event_y,
+ &self->start_x, &self->start_y);
+
+ gesture_prepare (self, delta > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, FALSE);
+ }
+ } else {
+ self->is_scrolling = TRUE;
+ return GDK_EVENT_PROPAGATE;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gboolean is_overshooting;
+ gdouble first_point, last_point;
+
+ get_range (self, &first_point, &last_point);
+
+ is_overshooting = (delta < 0 && self->progress <= first_point) ||
+ (delta > 0 && self->progress >= last_point);
+
+ if ((is_vertical == is_delta_vertical) && !is_overshooting)
+ gesture_begin (self);
+ else
+ gesture_cancel (self, distance);
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ if (gdk_event_is_scroll_stop_event (event)) {
+ gesture_end (self, distance);
+ } else {
+ gesture_update (self, delta / distance * SCROLL_MULTIPLIER);
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_FINISHING)
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+is_window_handle (GtkWidget *widget)
+{
+ gboolean window_dragging;
+ GtkWidget *parent, *window, *titlebar;
+
+ gtk_widget_style_get (widget, "window-dragging", &window_dragging, NULL);
+
+ if (window_dragging)
+ return TRUE;
+
+ /* Window titlebar area is always draggable, so check if we're inside. */
+ window = gtk_widget_get_toplevel (widget);
+ if (!GTK_IS_WINDOW (window))
+ return FALSE;
+
+ titlebar = gtk_window_get_titlebar (GTK_WINDOW (window));
+ if (!titlebar)
+ return FALSE;
+
+ parent = widget;
+ while (parent && parent != titlebar)
+ parent = gtk_widget_get_parent (parent);
+
+ return parent == titlebar;
+}
+
+static gboolean
+handle_event_cb (HdySwipeTracker *self,
+ GdkEvent *event)
+{
+ GdkEventSequence *sequence;
+ gboolean retval;
+ GtkEventSequenceState state;
+ GtkWidget *widget;
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type == GDK_SCROLL)
+ return handle_scroll_event (self, event, FALSE);
+
+ if (event->type != GDK_BUTTON_PRESS &&
+ event->type != GDK_BUTTON_RELEASE &&
+ event->type != GDK_MOTION_NOTIFY &&
+ event->type != GDK_TOUCH_BEGIN &&
+ event->type != GDK_TOUCH_END &&
+ event->type != GDK_TOUCH_UPDATE &&
+ event->type != GDK_TOUCH_CANCEL)
+ return GDK_EVENT_PROPAGATE;
+
+ widget = gtk_get_event_widget (event);
+ if (is_window_handle (widget))
+ return GDK_EVENT_PROPAGATE;
+
+ sequence = gdk_event_get_event_sequence (event);
+ retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event);
+ state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence);
+
+ if (state == GTK_EVENT_SEQUENCE_DENIED) {
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture));
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ return GDK_EVENT_STOP;
+ } else if (self->state == HDY_SWIPE_TRACKER_STATE_FINISHING) {
+ reset (self);
+ return GDK_EVENT_STOP;
+ }
+ return retval;
+}
+
+static gboolean
+captured_event_cb (HdySwipeable *swipeable,
+ GdkEvent *event)
+{
+ HdySwipeTracker *self = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ g_assert (HDY_IS_SWIPE_TRACKER (self));
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type != GDK_SCROLL)
+ return GDK_EVENT_PROPAGATE;
+
+ return handle_scroll_event (self, event, TRUE);
+}
+
+static void
+hdy_swipe_tracker_constructed (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ g_assert (self->swipeable);
+
+ gtk_widget_add_events (GTK_WIDGET (self->swipeable),
+ GDK_SMOOTH_SCROLL_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_TOUCH_MASK);
+
+ self->touch_gesture = g_object_new (GTK_TYPE_GESTURE_DRAG,
+ "widget", self->swipeable,
+ "propagation-phase", GTK_PHASE_NONE,
+ "touch-only", !self->allow_mouse_drag,
+ NULL);
+
+ g_signal_connect_swapped (self->touch_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-update", G_CALLBACK (drag_update_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-end", G_CALLBACK (drag_end_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "cancel", G_CALLBACK (drag_cancel_cb), self);
+
+ g_signal_connect_object (self->swipeable, "event", G_CALLBACK (handle_event_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->swipeable, "unrealize", G_CALLBACK (reset), self, G_CONNECT_SWAPPED);
+
+ /*
+ * HACK: GTK3 has no other way to get events on capture phase.
+ * This is a reimplementation of _gtk_widget_set_captured_event_handler(),
+ * which is private. In GTK4 it can be replaced with GtkEventControllerLegacy
+ * with capture propagation phase
+ */
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", captured_event_cb);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->constructed (object);
+}
+
+static void
+hdy_swipe_tracker_dispose (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+
+ if (self->touch_gesture)
+ g_signal_handlers_disconnect_by_data (self->touch_gesture, self);
+
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", NULL);
+
+ g_clear_object (&self->touch_gesture);
+ g_clear_object (&self->swipeable);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->dispose (object);
+}
+
+static void
+hdy_swipe_tracker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ g_value_set_object (value, hdy_swipe_tracker_get_swipeable (self));
+ break;
+
+ case PROP_ENABLED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_enabled (self));
+ break;
+
+ case PROP_REVERSED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_reversed (self));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_allow_mouse_drag (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ self->swipeable = HDY_SWIPEABLE (g_object_ref (g_value_get_object (value)));
+ break;
+
+ case PROP_ENABLED:
+ hdy_swipe_tracker_set_enabled (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_REVERSED:
+ hdy_swipe_tracker_set_reversed (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ hdy_swipe_tracker_set_allow_mouse_drag (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = g_value_get_enum (value);
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_class_init (HdySwipeTrackerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = hdy_swipe_tracker_constructed;
+ object_class->dispose = hdy_swipe_tracker_dispose;
+ object_class->get_property = hdy_swipe_tracker_get_property;
+ object_class->set_property = hdy_swipe_tracker_set_property;
+
+ /**
+ * HdySwipeTracker:swipeable:
+ *
+ * The widget the swipe tracker is attached to. Must not be %NULL.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SWIPEABLE] =
+ g_param_spec_object ("swipeable",
+ _("Swipeable"),
+ _("The swipeable the swipe tracker is attached to"),
+ HDY_TYPE_SWIPEABLE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ /**
+ * HdySwipeTracker:enabled:
+ *
+ * Whether the swipe tracker is enabled. When it's not enabled, no events
+ * will be processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ENABLED] =
+ g_param_spec_boolean ("enabled",
+ _("Enabled"),
+ _("Whether the swipe tracker processes events"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:reversed:
+ *
+ * Whether to reverse the swipe direction. If the swipe tracker is horizontal,
+ * it can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVERSED] =
+ g_param_spec_boolean ("reversed",
+ _("Reversed"),
+ _("Whether swipe direction is reversed"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:allow-mouse-drag:
+ *
+ * Whether to allow dragging with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ALLOW_MOUSE_DRAG] =
+ g_param_spec_boolean ("allow-mouse-drag",
+ _("Allow mouse drag"),
+ _("Whether to allow dragging with mouse pointer"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdySwipeTracker::begin-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @direction: The direction of the swipe
+ * @direct: %TRUE if the swipe is directly triggered by a gesture,
+ * %FALSE if it's triggered via a #HdySwipeGroup
+ *
+ * This signal is emitted when a possible swipe is detected.
+ *
+ * The @direction value can be used to restrict the swipe to a certain
+ * direction.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_BEGIN_SWIPE] =
+ g_signal_new ("begin-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ HDY_TYPE_NAVIGATION_DIRECTION, G_TYPE_BOOLEAN);
+
+ /**
+ * HdySwipeTracker::update-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @progress: The current animation progress value
+ *
+ * This signal is emitted every time the progress value changes.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_UPDATE_SWIPE] =
+ g_signal_new ("update-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_DOUBLE);
+
+ /**
+ * HdySwipeTracker::end-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @duration: Snap-back animation duration in milliseconds
+ * @to: The progress value to animate to
+ *
+ * This signal is emitted as soon as the gesture has stopped.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_END_SWIPE] =
+ g_signal_new ("end-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_INT64, G_TYPE_DOUBLE);
+}
+
+static void
+hdy_swipe_tracker_init (HdySwipeTracker *self)
+{
+ reset (self);
+ self->orientation = GTK_ORIENTATION_HORIZONTAL;
+ self->enabled = TRUE;
+}
+
+/**
+ * hdy_swipe_tracker_new:
+ * @swipeable: a #GtkWidget to add the tracker on
+ *
+ * Create a new #HdySwipeTracker object on @widget.
+ *
+ * Returns: the newly created #HdySwipeTracker object
+ *
+ * Since: 1.0
+ */
+HdySwipeTracker *
+hdy_swipe_tracker_new (HdySwipeable *swipeable)
+{
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (swipeable), NULL);
+
+ return g_object_new (HDY_TYPE_SWIPE_TRACKER,
+ "swipeable", swipeable,
+ NULL);
+}
+
+/**
+ * hdy_swipe_tracker_get_swipeable:
+ * @self: a #HdySwipeTracker
+ *
+ * Get @self's swipeable widget.
+ *
+ * Returns: (transfer none): the swipeable widget
+ *
+ * Since: 1.0
+ */
+HdySwipeable *
+hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), NULL);
+
+ return self->swipeable;
+}
+
+/**
+ * hdy_swipe_tracker_get_enabled:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is enabled. When it's not enabled, no events will be
+ * processed. Generally widgets will want to expose this via a property.
+ *
+ * Returns: %TRUE if @self is enabled
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_enabled (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->enabled;
+}
+
+/**
+ * hdy_swipe_tracker_set_enabled:
+ * @self: a #HdySwipeTracker
+ * @enabled: whether to enable to swipe tracker
+ *
+ * Set whether @self is enabled. When it's not enabled, no events will be
+ * processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_enabled (HdySwipeTracker *self,
+ gboolean enabled)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ enabled = !!enabled;
+
+ if (self->enabled == enabled)
+ return;
+
+ self->enabled = enabled;
+
+ if (!enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ reset (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_reversed:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is reversing the swipe direction.
+ *
+ * Returns: %TRUE is the direction is reversed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_reversed (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->reversed;
+}
+
+/**
+ * hdy_swipe_tracker_set_reversed:
+ * @self: a #HdySwipeTracker
+ * @reversed: whether to reverse the swipe direction
+ *
+ * Set whether to reverse the swipe direction. If @self is horizontal,
+ * can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_reversed (HdySwipeTracker *self,
+ gboolean reversed)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ reversed = !!reversed;
+
+ if (self->reversed == reversed)
+ return;
+
+ self->reversed = reversed;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVERSED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self can be dragged with mouse pointer.
+ *
+ * Returns: %TRUE is mouse dragging is allowed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->allow_mouse_drag;
+}
+
+/**
+ * hdy_swipe_tracker_set_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ * @allow_mouse_drag: whether to allow mouse dragging
+ *
+ * Set whether @self can be dragged with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self,
+ gboolean allow_mouse_drag)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ allow_mouse_drag = !!allow_mouse_drag;
+
+ if (self->allow_mouse_drag == allow_mouse_drag)
+ return;
+
+ self->allow_mouse_drag = allow_mouse_drag;
+
+ if (self->touch_gesture)
+ g_object_set (self->touch_gesture, "touch-only", !allow_mouse_drag, NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * hdy_swipe_tracker_shift_position:
+ * @self: a #HdySwipeTracker
+ * @delta: the position delta
+ *
+ * Move the current progress value by @delta. This can be used to adjust the
+ * current position if snap points move during the gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_shift_position (HdySwipeTracker *self,
+ gdouble delta)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->progress += delta;
+ self->initial_progress += delta;
+}
+
+void
+hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean direct)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_BEGIN_SWIPE], 0, direction, direct);
+}
+
+void
+hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self,
+ gdouble progress)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_UPDATE_SWIPE], 0, progress);
+}
+
+void
+hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self,
+ gint64 duration,
+ gdouble to)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_END_SWIPE], 0, duration, to);
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.h b/subprojects/libhandy/src/hdy-swipe-tracker.h
new file mode 100644
index 0000000..20fe751
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-swipeable.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPE_TRACKER (hdy_swipe_tracker_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySwipeTracker, hdy_swipe_tracker, HDY, SWIPE_TRACKER, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeTracker *hdy_swipe_tracker_new (HdySwipeable *swipeable);
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeable *hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_enabled (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_enabled (HdySwipeTracker *self,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_reversed (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_reversed (HdySwipeTracker *self,
+ gboolean reversed);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self,
+ gboolean allow_mouse_drag);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_shift_position (HdySwipeTracker *self,
+ gdouble delta);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipeable.c b/subprojects/libhandy/src/hdy-swipeable.c
new file mode 100644
index 0000000..6be1713
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipeable.c
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-swipeable
+ * @short_description: An interface for swipeable widgets.
+ * @title: HdySwipeable
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeGroup
+ *
+ * The #HdySwipeable interface is implemented by all swipeable widgets. They
+ * can be synced using #HdySwipeGroup.
+ *
+ * See #HdySwipeTracker for details about implementing it.
+ *
+ * Since: 0.0.12
+ */
+
+G_DEFINE_INTERFACE (HdySwipeable, hdy_swipeable, GTK_TYPE_WIDGET)
+
+enum {
+ SIGNAL_CHILD_SWITCHED,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+hdy_swipeable_default_init (HdySwipeableInterface *iface)
+{
+ /**
+ * HdySwipeable::child-switched:
+ * @self: The #HdySwipeable instance
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * This signal should be emitted when the widget's visible child is changed.
+ *
+ * @duration can be 0 if the child is switched without animation.
+ *
+ * This is used by #HdySwipeGroup, applications should not connect to it.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_CHILD_SWITCHED] =
+ g_signal_new ("child-switched",
+ G_TYPE_FROM_INTERFACE (iface),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_UINT, G_TYPE_INT64);
+}
+
+/**
+ * hdy_swipeable_switch_child:
+ * @self: a #HdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * See HdySwipeable::child-switched.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_switch_child (HdySwipeable *self,
+ guint index,
+ gint64 duration)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_if_fail (iface->switch_child != NULL);
+
+ iface->switch_child (self, index, duration);
+}
+
+/**
+ * hdy_swipeable_emit_child_switched:
+ * @self: a #HdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * Emits HdySwipeable::child-switched signal. This should be called when the
+ * widget switches visible child widget.
+ *
+ * @duration can be 0 if the child is switched without animation.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_emit_child_switched (HdySwipeable *self,
+ guint index,
+ gint64 duration)
+{
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+
+ g_signal_emit (self, signals[SIGNAL_CHILD_SWITCHED], 0, index, duration);
+}
+
+/**
+ * hdy_swipeable_get_swipe_tracker:
+ * @self: a #HdySwipeable
+ *
+ * Gets the #HdySwipeTracker used by this swipeable widget.
+ *
+ * Returns: (transfer none): the swipe tracker
+ *
+ * Since: 1.0
+ */
+HdySwipeTracker *
+hdy_swipeable_get_swipe_tracker (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_swipe_tracker != NULL, NULL);
+
+ return iface->get_swipe_tracker (self);
+}
+
+/**
+ * hdy_swipeable_get_distance:
+ * @self: a #HdySwipeable
+ *
+ * Gets the swipe distance of @self. This corresponds to how many pixels
+ * 1 unit represents.
+ *
+ * Returns: the swipe distance in pixels
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_distance (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_distance != NULL, 0);
+
+ return iface->get_distance (self);
+}
+
+/**
+ * hdy_swipeable_get_snap_points: (virtual get_snap_points)
+ * @self: a #HdySwipeable
+ * @n_snap_points: (out): location to return the number of the snap points
+ *
+ * Gets the snap points of @self. Each snap point represents a progress value
+ * that is considered acceptable to end the swipe on.
+ *
+ * Returns: (array length=n_snap_points) (transfer full): the snap points of
+ * @self. The array must be freed with g_free().
+ *
+ * Since: 1.0
+ */
+gdouble *
+hdy_swipeable_get_snap_points (HdySwipeable *self,
+ gint *n_snap_points)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_snap_points != NULL, NULL);
+
+ return iface->get_snap_points (self, n_snap_points);
+}
+
+/**
+ * hdy_swipeable_get_progress:
+ * @self: a #HdySwipeable
+ *
+ * Gets the current progress of @self
+ *
+ * Returns: the current progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_progress (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_progress != NULL, 0);
+
+ return iface->get_progress (self);
+}
+
+/**
+ * hdy_swipeable_get_cancel_progress:
+ * @self: a #HdySwipeable
+ *
+ * Gets the progress @self will snap back to after the gesture is canceled.
+ *
+ * Returns: the cancel progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_cancel_progress (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_cancel_progress != NULL, 0);
+
+ return iface->get_cancel_progress (self);
+}
+
+/**
+ * hdy_swipeable_get_swipe_area:
+ * @self: a #HdySwipeable
+ * @navigation_direction: the direction of the swipe
+ * @is_drag: whether the swipe is caused by a dragging gesture
+ * @rect: (out): a pointer to a #GdkRectangle to store the swipe area
+ *
+ * Gets the area @self can start a swipe from for the given direction and
+ * gesture type.
+ * This can be used to restrict swipes to only be possible from a certain area,
+ * for example, to only allow edge swipes, or to have a draggable element and
+ * ignore swipes elsewhere.
+ *
+ * Swipe area is only considered for direct swipes (as in, not initiated by
+ * #HdySwipeGroup).
+ *
+ * If not implemented, the default implementation returns the allocation of
+ * @self, allowing swipes from anywhere.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_get_swipe_area (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+ g_return_if_fail (rect != NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+
+ if (iface->get_swipe_area) {
+ iface->get_swipe_area (self, navigation_direction, is_drag, rect);
+ return;
+ }
+
+ rect->x = 0;
+ rect->y = 0;
+ rect->width = gtk_widget_get_allocated_width (GTK_WIDGET (self));
+ rect->height = gtk_widget_get_allocated_height (GTK_WIDGET (self));
+}
diff --git a/subprojects/libhandy/src/hdy-swipeable.h b/subprojects/libhandy/src/hdy-swipeable.h
new file mode 100644
index 0000000..9cb6cde
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipeable.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-types.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPEABLE (hdy_swipeable_get_type ())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_INTERFACE (HdySwipeable, hdy_swipeable, HDY, SWIPEABLE, GtkWidget)
+
+/**
+ * HdySwipeableInterface:
+ * @parent: The parent interface.
+ * @switch_child: Switches visible child.
+ * @get_swipe_tracker: Gets the swipe tracker.
+ * @get_distance: Gets the swipe distance.
+ * @get_snap_points: Gets the snap points
+ * @get_progress: Gets the current progress.
+ * @get_cancel_progress: Gets the cancel progress.
+ * @get_swipe_area: Gets the swipeable rectangle.
+ *
+ * An interface for swipeable widgets.
+ *
+ * Since: 1.0
+ **/
+struct _HdySwipeableInterface
+{
+ GTypeInterface parent;
+
+ void (*switch_child) (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+ HdySwipeTracker * (*get_swipe_tracker) (HdySwipeable *self);
+ gdouble (*get_distance) (HdySwipeable *self);
+ gdouble * (*get_snap_points) (HdySwipeable *self,
+ gint *n_snap_points);
+ gdouble (*get_progress) (HdySwipeable *self);
+ gdouble (*get_cancel_progress) (HdySwipeable *self);
+ void (*get_swipe_area) (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_switch_child (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_emit_child_switched (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeTracker *hdy_swipeable_get_swipe_tracker (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_distance (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble *hdy_swipeable_get_snap_points (HdySwipeable *self,
+ gint *n_snap_points);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_progress (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_cancel_progress (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_get_swipe_area (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-title-bar.c b/subprojects/libhandy/src/hdy-title-bar.c
new file mode 100644
index 0000000..fd5371a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-title-bar.c
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-title-bar.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-title-bar
+ * @short_description: A simple title bar container.
+ * @Title: HdyTitleBar
+ *
+ * HdyTitleBar is meant to be used as the top-level widget of your window's
+ * title bar. It will be drawn with the same style as a GtkHeaderBar but it
+ * won't force a widget layout on you: you can put whatever widget you want in
+ * it, including a GtkHeaderBar.
+ *
+ * HdyTitleBar becomes really useful when you want to animate header bars, like
+ * an adaptive application using #HdyLeaflet would do.
+ *
+ * # CSS nodes
+ *
+ * #HdyTitleBar has a single CSS node with name headerbar.
+ */
+
+enum {
+ PROP_0,
+ PROP_SELECTION_MODE,
+ LAST_PROP,
+};
+
+struct _HdyTitleBar
+{
+ GtkBin parent_instance;
+
+ gboolean selection_mode;
+};
+
+G_DEFINE_TYPE (HdyTitleBar, hdy_title_bar, GTK_TYPE_BIN)
+
+static GParamSpec *props[LAST_PROP];
+
+/**
+ * hdy_title_bar_set_selection_mode:
+ * @self: a #HdyTitleBar
+ * @selection_mode: %TRUE to enable the selection mode
+ *
+ * Sets whether @self is in selection mode.
+ */
+void
+hdy_title_bar_set_selection_mode (HdyTitleBar *self,
+ gboolean selection_mode)
+{
+ GtkStyleContext *context;
+
+ g_return_if_fail (HDY_IS_TITLE_BAR (self));
+
+ selection_mode = !!selection_mode;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ if (self->selection_mode == selection_mode)
+ return;
+
+ self->selection_mode = selection_mode;
+
+ if (selection_mode)
+ gtk_style_context_add_class (context, "selection-mode");
+ else
+ gtk_style_context_remove_class (context, "selection-mode");
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]);
+}
+
+/**
+ * hdy_title_bar_get_selection_mode:
+ * @self: a #HdyTitleBar
+ *
+ * Returns whether whether @self is in selection mode.
+ *
+ * Returns: %TRUE if the title bar is in selection mode
+ */
+gboolean
+hdy_title_bar_get_selection_mode (HdyTitleBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TITLE_BAR (self), FALSE);
+
+ return self->selection_mode;
+}
+
+static void
+style_updated_cb (HdyTitleBar *self)
+{
+ GtkStyleContext *context;
+ gboolean selection_mode;
+
+ g_assert (HDY_IS_TITLE_BAR (self));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ selection_mode = gtk_style_context_has_class (context, "selection-mode");
+
+ if (self->selection_mode == selection_mode)
+ return;
+
+ self->selection_mode = selection_mode;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]);
+}
+
+static void
+hdy_title_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTitleBar *self = HDY_TITLE_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SELECTION_MODE:
+ g_value_set_boolean (value, hdy_title_bar_get_selection_mode (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_title_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTitleBar *self = HDY_TITLE_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SELECTION_MODE:
+ hdy_title_bar_set_selection_mode (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static gboolean
+hdy_title_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ /* GtkWidget draws nothing by default so we have to render the background
+ * explicitly for HdyTitleBar to render the typical titlebar background.
+ */
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+
+ return GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->draw (widget, cr);
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_title_bar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ GtkWidget *child;
+ gint parent_min, parent_nat;
+ gint css_width, css_height, css_min;
+
+ child = gtk_bin_get_child (GTK_BIN (widget));
+
+ gtk_style_context_get (gtk_widget_get_style_context (widget),
+ gtk_widget_get_state_flags (widget),
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ css_min = css_width;
+ else
+ css_min = css_height;
+
+ if (child)
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ if (for_size != 1)
+ gtk_widget_get_preferred_width_for_height (child,
+ MAX (for_size, css_height),
+ &parent_min, &parent_nat);
+ else
+ gtk_widget_get_preferred_width (child, &parent_min, &parent_nat);
+ else
+ if (for_size != 1)
+ gtk_widget_get_preferred_height_for_width (child,
+ MAX (for_size, css_width),
+ &parent_min, &parent_nat);
+ else
+ gtk_widget_get_preferred_height (child, &parent_min, &parent_nat);
+ else {
+ parent_min = 0;
+ parent_nat = 0;
+ }
+
+ if (minimum)
+ *minimum = MAX (parent_min, css_min);
+
+ if (natural)
+ *natural = MAX (parent_nat, css_min);
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_title_bar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkAllocation clip;
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->size_allocate (widget, allocation);
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_title_bar_class_init (HdyTitleBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_title_bar_get_property;
+ object_class->set_property = hdy_title_bar_set_property;
+
+ widget_class->draw = hdy_title_bar_draw;
+ widget_class->get_preferred_width = hdy_title_bar_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_title_bar_get_preferred_width_for_height;
+ widget_class->get_preferred_height = hdy_title_bar_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_title_bar_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_title_bar_size_allocate;
+
+ /**
+ * HdyTitleBar:selection_mode:
+ *
+ * %TRUE if the title bar is in selection mode.
+ */
+ props[PROP_SELECTION_MODE] =
+ g_param_spec_boolean ("selection-mode",
+ _("Selection mode"),
+ _("Whether or not the title bar is in selection mode"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_TITLE_BAR);
+ /* Adwaita states it expects a headerbar to be the top-level titlebar widget,
+ * so style-wise HdyTitleBar pretends to be one as its role is to be the
+ * top-level titlebar widget.
+ */
+ gtk_widget_class_set_css_name (widget_class, "headerbar");
+ gtk_container_class_handle_border_width (container_class);
+}
+
+static void
+hdy_title_bar_init (HdyTitleBar *self)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ /* Ensure the widget has the titlebar style class. */
+ gtk_style_context_add_class (context, "titlebar");
+
+ g_signal_connect (self, "style-updated", G_CALLBACK (style_updated_cb), NULL);
+}
+
+/**
+ * hdy_title_bar_new:
+ *
+ * Creates a new #HdyTitleBar.
+ *
+ * Returns: a new #HdyTitleBar
+ */
+GtkWidget *
+hdy_title_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_TITLE_BAR, NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-title-bar.h b/subprojects/libhandy/src/hdy-title-bar.h
new file mode 100644
index 0000000..275f4ea
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-title-bar.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TITLE_BAR (hdy_title_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTitleBar, hdy_title_bar, HDY, TITLE_BAR, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_title_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_title_bar_get_selection_mode (HdyTitleBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_title_bar_set_selection_mode (HdyTitleBar *self,
+ gboolean selection_mode);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-types.h b/subprojects/libhandy/src/hdy-types.h
new file mode 100644
index 0000000..56a3763
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-types.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+G_BEGIN_DECLS
+
+typedef struct _HdySwipeTracker HdySwipeTracker;
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-value-object.c b/subprojects/libhandy/src/hdy-value-object.c
new file mode 100644
index 0000000..485d4b3
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-value-object.c
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+#include <gobject/gvaluecollector.h>
+#include "hdy-value-object.h"
+
+/**
+ * SECTION:hdy-value-object
+ * @short_description: An object representing a #GValue.
+ * @Title: HdyValueObject
+ *
+ * The #HdyValueObject object represents a #GValue, allowing it to be
+ * used with #GListModel.
+ *
+ * Since: 0.0.8
+ */
+
+struct _HdyValueObject
+{
+ GObject parent_instance;
+
+ GValue value;
+};
+
+G_DEFINE_TYPE (HdyValueObject, hdy_value_object, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_VALUE,
+ N_PROPS
+};
+
+static GParamSpec *props [N_PROPS];
+
+/**
+ * hdy_value_object_new:
+ * @value: the #GValue to store
+ *
+ * Create a new #HdyValueObject.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject *
+hdy_value_object_new (const GValue *value)
+{
+ return g_object_new (HDY_TYPE_VALUE_OBJECT,
+ "value", value,
+ NULL);
+}
+
+/**
+ * hdy_value_object_new_collect: (skip)
+ * @type: the #GType of the value
+ * @...: the value to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method which uses
+ * the G_VALUE_COLLECT() macro internally.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_collect (GType type, ...)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+ g_autofree gchar *error = NULL;
+ va_list var_args;
+
+ va_start (var_args, type);
+
+ G_VALUE_COLLECT_INIT (&value, type, var_args, 0, &error);
+
+ va_end (var_args);
+
+ if (error)
+ g_critical ("%s: %s", G_STRFUNC, error);
+
+ return g_object_new (HDY_TYPE_VALUE_OBJECT,
+ "value", &value,
+ NULL);
+}
+
+/**
+ * hdy_value_object_new_string: (skip)
+ * @string: (transfer none): the string to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method to create a
+ * #HdyValueObject that stores a string.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_string (const gchar *string)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+
+ g_value_init (&value, G_TYPE_STRING);
+ g_value_set_string (&value, string);
+ return hdy_value_object_new (&value);
+}
+
+/**
+ * hdy_value_object_new_take_string: (skip)
+ * @string: (transfer full): the string to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method to create a
+ * #HdyValueObject that stores a string taking ownership of it.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_take_string (gchar *string)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+
+ g_value_init (&value, G_TYPE_STRING);
+ g_value_take_string (&value, string);
+ return hdy_value_object_new (&value);
+}
+
+static void
+hdy_value_object_finalize (GObject *object)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+
+ g_value_unset (&self->value);
+
+ G_OBJECT_CLASS (hdy_value_object_parent_class)->finalize (object);
+}
+
+static void
+hdy_value_object_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+
+ switch (prop_id)
+ {
+ case PROP_VALUE:
+ g_value_set_boxed (value, &self->value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_value_object_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+ GValue *real_value;
+
+ switch (prop_id)
+ {
+ case PROP_VALUE:
+ /* construct only */
+ real_value = g_value_get_boxed (value);
+ g_value_init (&self->value, real_value->g_type);
+ g_value_copy (real_value, &self->value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_value_object_class_init (HdyValueObjectClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_value_object_finalize;
+ object_class->get_property = hdy_value_object_get_property;
+ object_class->set_property = hdy_value_object_set_property;
+
+ props[PROP_VALUE] =
+ g_param_spec_boxed ("value", C_("HdyValueObjectClass", "Value"),
+ C_("HdyValueObjectClass", "The contained value"),
+ G_TYPE_VALUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class,
+ N_PROPS,
+ props);
+}
+
+static void
+hdy_value_object_init (HdyValueObject *self)
+{
+}
+
+/**
+ * hdy_value_object_get_value:
+ * @value: the #HdyValueObject
+ *
+ * Return the contained value.
+ *
+ * Returns: (transfer none): the contained #GValue
+ * Since: 0.0.8
+ */
+const GValue*
+hdy_value_object_get_value (HdyValueObject *value)
+{
+ return &value->value;
+}
+
+/**
+ * hdy_value_object_copy_value:
+ * @value: the #HdyValueObject
+ * @dest: #GValue with correct type to copy into
+ *
+ * Copy data from the contained #GValue into @dest.
+ *
+ * Since: 0.0.8
+ */
+void
+hdy_value_object_copy_value (HdyValueObject *value,
+ GValue *dest)
+{
+ g_value_copy (&value->value, dest);
+}
+
+/**
+ * hdy_value_object_get_string:
+ * @value: the #HdyValueObject
+ *
+ * Returns the contained string if the value is of type #G_TYPE_STRING.
+ *
+ * Returns: (transfer none): the contained string
+ * Since: 0.0.8
+ */
+const gchar*
+hdy_value_object_get_string (HdyValueObject *value)
+{
+ return g_value_get_string (&value->value);
+}
+
+/**
+ * hdy_value_object_dup_string:
+ * @value: the #HdyValueObject
+ *
+ * Returns a copy of the contained string if the value is of type
+ * #G_TYPE_STRING.
+ *
+ * Returns: (transfer full): a copy of the contained string
+ * Since: 0.0.8
+ */
+gchar*
+hdy_value_object_dup_string (HdyValueObject *value)
+{
+ return g_value_dup_string (&value->value);
+}
+
diff --git a/subprojects/libhandy/src/hdy-value-object.h b/subprojects/libhandy/src/hdy-value-object.h
new file mode 100644
index 0000000..b44f106
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-value-object.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VALUE_OBJECT (hdy_value_object_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyValueObject, hdy_value_object, HDY, VALUE_OBJECT, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new (const GValue *value);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_collect (GType type,
+ ...);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_string (const gchar *string);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_take_string (gchar *string);
+
+HDY_AVAILABLE_IN_ALL
+const GValue* hdy_value_object_get_value (HdyValueObject *value);
+HDY_AVAILABLE_IN_ALL
+void hdy_value_object_copy_value (HdyValueObject *value,
+ GValue *dest);
+HDY_AVAILABLE_IN_ALL
+const gchar* hdy_value_object_get_string (HdyValueObject *value);
+HDY_AVAILABLE_IN_ALL
+gchar* hdy_value_object_dup_string (HdyValueObject *value);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-version.h.in b/subprojects/libhandy/src/hdy-version.h.in
new file mode 100644
index 0000000..0cb923f
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-version.h.in
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+/**
+ * SECTION:hdy-version
+ * @short_description: Handy version checking.
+ *
+ * Handy provides macros to check the version of the library at compile-time.
+ */
+
+/**
+ * HDY_MAJOR_VERSION:
+ *
+ * Hdy major version component (e.g. 1 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MAJOR_VERSION (@HDY_MAJOR_VERSION@)
+
+/**
+ * HDY_MINOR_VERSION:
+ *
+ * Hdy minor version component (e.g. 2 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MINOR_VERSION (@HDY_MINOR_VERSION@)
+
+/**
+ * HDY_MICRO_VERSION:
+ *
+ * Hdy micro version component (e.g. 3 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MICRO_VERSION (@HDY_MICRO_VERSION@)
+
+/**
+ * HDY_VERSION
+ *
+ * Hdy version.
+ */
+#define HDY_VERSION (@HDY_VERSION@)
+
+/**
+ * HDY_VERSION_S:
+ *
+ * Handy version, encoded as a string, useful for printing and
+ * concatenation.
+ */
+#define HDY_VERSION_S "@HDY_VERSION@"
+
+#define HDY_ENCODE_VERSION(major,minor,micro) \
+ ((major) << 24 | (minor) << 16 | (micro) << 8)
+
+/**
+ * HDY_VERSION_HEX:
+ *
+ * Handy version, encoded as an hexadecimal number, useful for
+ * integer comparisons.
+ */
+#define HDY_VERSION_HEX \
+ (HDY_ENCODE_VERSION (HDY_MAJOR_VERSION, HDY_MINOR_VERSION, HDY_MICRO_VERSION))
+
+/**
+ * HDY_CHECK_VERSION:
+ * @major: required major version
+ * @minor: required minor version
+ * @micro: required micro version
+ *
+ * Compile-time version checking. Evaluates to %TRUE if the version
+ * of handy is greater than the required one.
+ */
+#define HDY_CHECK_VERSION(major,minor,micro) \
+ (HDY_MAJOR_VERSION > (major) || \
+ (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION > (minor)) || \
+ (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION == (minor) && \
+ HDY_MICRO_VERSION >= (micro)))
+
+#ifndef _HDY_EXTERN
+#define _HDY_EXTERN extern
+#endif
+
+#define HDY_AVAILABLE_IN_ALL _HDY_EXTERN
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.c b/subprojects/libhandy/src/hdy-view-switcher-bar.c
new file mode 100644
index 0000000..111d3e6
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.c
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-enums.h"
+#include "hdy-view-switcher-bar.h"
+
+/**
+ * SECTION:hdy-view-switcher-bar
+ * @short_description: A view switcher action bar.
+ * @title: HdyViewSwitcherBar
+ * @See_also: #HdyViewSwitcher, #HdyViewSwitcherTitle
+ *
+ * An action bar letting you switch between multiple views offered by a
+ * #GtkStack, via an #HdyViewSwitcher. It is designed to be put at the bottom of
+ * a window and to be revealed only on really narrow windows e.g. on mobile
+ * phones. It can't be revealed if there are less than two pages.
+ *
+ * You can conveniently bind the #HdyViewSwitcherBar:reveal property to
+ * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher
+ * bar when the title label is displayed in place of the view switcher.
+ *
+ * An example of the UI definition for a common use case:
+ * |[
+ * <object class="GtkWindow"/>
+ * <child type="titlebar">
+ * <object class="HdyHeaderBar">
+ * <property name="centering-policy">strict</property>
+ * <child type="title">
+ * <object class="HdyViewSwitcherTitle"
+ * id="view_switcher_title">
+ * <property name="stack">stack</property>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * <child>
+ * <object class="GtkBox">
+ * <child>
+ * <object class="GtkStack" id="stack"/>
+ * </child>
+ * <child>
+ * <object class="HdyViewSwitcherBar">
+ * <property name="stack">stack</property>
+ * <property name="reveal"
+ * bind-source="view_switcher_title"
+ * bind-property="title-visible"
+ * bind-flags="sync-create"/>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcherBar has a single CSS node with name viewswitcherbar.
+ *
+ * Since: 0.0.10
+ */
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_STACK,
+ PROP_REVEAL,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcherBar
+{
+ GtkBin parent_instance;
+
+ GtkActionBar *action_bar;
+ GtkRevealer *revealer;
+ HdyViewSwitcher *view_switcher;
+
+ HdyViewSwitcherPolicy policy;
+ gboolean reveal;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, GTK_TYPE_BIN)
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+update_bar_revealed (HdyViewSwitcherBar *self) {
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+ gint count = 0;
+
+ if (self->reveal && stack)
+ gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count);
+
+ gtk_revealer_set_reveal_child (self->revealer, count > 1);
+}
+
+static void
+hdy_view_switcher_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_bar_get_policy (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_bar_get_stack (self));
+ break;
+ case PROP_REVEAL:
+ g_value_set_boolean (value, hdy_view_switcher_bar_get_reveal (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_bar_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_bar_set_stack (self, g_value_get_object (value));
+ break;
+ case PROP_REVEAL:
+ hdy_view_switcher_bar_set_reveal (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_bar_class_init (HdyViewSwitcherBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_bar_get_property;
+ object_class->set_property = hdy_view_switcher_bar_set_property;
+
+ /**
+ * HdyViewSwitcherBar:policy:
+ *
+ * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine
+ * which mode to use.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_NARROW,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherBar:stack:
+ *
+ * The #GtkStack the #HdyViewSwitcher controls.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherBar:reveal:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_REVEAL] =
+ g_param_spec_boolean ("reveal",
+ _("Reveal"),
+ _("Whether the view switcher is revealed"),
+ FALSE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitcherbar");
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-bar.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, action_bar);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, view_switcher);
+}
+
+static void
+hdy_view_switcher_bar_init (HdyViewSwitcherBar *self)
+{
+ /* This must be initialized before the template so the embedded view switcher
+ * can pick up the correct default value.
+ */
+ self->policy = HDY_VIEW_SWITCHER_POLICY_NARROW;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->revealer = GTK_REVEALER (gtk_bin_get_child (GTK_BIN (self->action_bar)));
+ update_bar_revealed (self);
+ gtk_revealer_set_transition_type (self->revealer, GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
+}
+
+/**
+ * hdy_view_switcher_bar_new:
+ *
+ * Creates a new #HdyViewSwitcherBar widget.
+ *
+ * Returns: a new #HdyViewSwitcherBar
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_BAR, NULL);
+}
+
+/**
+ * hdy_view_switcher_bar_get_policy:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 0.0.10
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ return self->policy;
+}
+
+/**
+ * hdy_view_switcher_bar_set_policy:
+ * @self: a #HdyViewSwitcherBar
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+
+ if (self->policy == policy)
+ return;
+
+ self->policy = policy;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_bar_get_stack:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 0.0.10
+ */
+GtkStack *
+hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), NULL);
+
+ return hdy_view_switcher_get_stack (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_bar_set_stack:
+ * @self: a #HdyViewSwitcherBar
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self,
+ GtkStack *stack)
+{
+ GtkStack *previous_stack;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ previous_stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (previous_stack == stack)
+ return;
+
+ if (previous_stack)
+ g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_bar_revealed), self);
+
+ hdy_view_switcher_set_stack (self->view_switcher, stack);
+
+ if (stack) {
+ g_signal_connect_swapped (stack, "add", G_CALLBACK (update_bar_revealed), self);
+ g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_bar_revealed), self);
+ }
+
+ update_bar_revealed (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
+
+/**
+ * hdy_view_switcher_bar_get_reveal:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Gets whether @self should be revealed or not.
+ *
+ * Returns: %TRUE if @self is revealed, %FALSE if not.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), FALSE);
+
+ return self->reveal;
+}
+
+/**
+ * hdy_view_switcher_bar_set_reveal:
+ * @self: a #HdyViewSwitcherBar
+ * @reveal: %TRUE to reveal @self
+ *
+ * Sets whether @self should be revealed or not.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self,
+ gboolean reveal)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+
+ reveal = !!reveal;
+
+ if (self->reveal == reveal)
+ return;
+
+ self->reveal = reveal;
+ update_bar_revealed (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL]);
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.h b/subprojects/libhandy/src/hdy-view-switcher-bar.h
new file mode 100644
index 0000000..be2db35
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+#include "hdy-view-switcher.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, HDY, VIEW_SWITCHER_BAR, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_view_switcher_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self,
+ GtkStack *stack);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self,
+ gboolean reveal);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.ui b/subprojects/libhandy/src/hdy-view-switcher-bar.ui
new file mode 100644
index 0000000..a2b1266
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherBar" parent="GtkBin">
+ <child>
+ <object class="GtkActionBar" id="action_bar">
+ <property name="visible">True</property>
+ <child type="center">
+ <object class="HdyViewSwitcher" id="view_switcher">
+ <property name="margin-start">10</property>
+ <property name="margin-end">10</property>
+ <property name="narrow-ellipsize">end</property>
+ <property name="policy" bind-source="HdyViewSwitcherBar" bind-property="policy" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button-private.h b/subprojects/libhandy/src/hdy-view-switcher-button-private.h
new file mode 100644
index 0000000..5f85c52
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button-private.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherButton, hdy_view_switcher_button, HDY, VIEW_SWITCHER_BUTTON, GtkRadioButton)
+
+GtkWidget *hdy_view_switcher_button_new (void);
+
+const gchar *hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self,
+ const gchar *icon_name);
+
+GtkIconSize hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self,
+ GtkIconSize icon_size);
+
+gboolean hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self,
+ gboolean needs_attention);
+
+const gchar *hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self,
+ const gchar *label);
+
+void hdy_view_switcher_button_set_narrow_ellipsize (HdyViewSwitcherButton *self,
+ PangoEllipsizeMode mode);
+
+void hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self,
+ gint *h_min_width,
+ gint *h_nat_width,
+ gint *v_min_width,
+ gint *v_nat_width);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.c b/subprojects/libhandy/src/hdy-view-switcher-button.c
new file mode 100644
index 0000000..20b57b0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button.c
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-view-switcher-button-private.h"
+
+/**
+ * PRIVATE:hdy-view-switcher-button
+ * @short_description: Button used in #HdyViewSwitcher.
+ * @title: HdyViewSwitcherButton
+ * @See_also: #HdyViewSwitcher
+ * @stability: Private
+ *
+ * #HdyViewSwitcherButton represents an application's view. It is designed to be
+ * used exclusively internally by #HdyViewSwitcher.
+ *
+ * Since: 0.0.10
+ */
+
+enum {
+ PROP_0,
+ PROP_ICON_SIZE,
+ PROP_ICON_NAME,
+ PROP_NEEDS_ATTENTION,
+
+ /* Overridden properties */
+ PROP_LABEL,
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_NEEDS_ATTENTION + 1,
+};
+
+struct _HdyViewSwitcherButton
+{
+ GtkRadioButton parent_instance;
+
+ GtkBox *horizontal_box;
+ GtkImage *horizontal_image;
+ GtkLabel *horizontal_label_active;
+ GtkLabel *horizontal_label_inactive;
+ GtkStack *horizontal_label_stack;
+ GtkStack *stack;
+ GtkBox *vertical_box;
+ GtkImage *vertical_image;
+ GtkLabel *vertical_label_active;
+ GtkLabel *vertical_label_inactive;
+ GtkStack *vertical_label_stack;
+
+ gchar *icon_name;
+ GtkIconSize icon_size;
+ gchar *label;
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdyViewSwitcherButton, hdy_view_switcher_button, GTK_TYPE_RADIO_BUTTON,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static void
+on_active_changed (HdyViewSwitcherButton *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self))) {
+ gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_active));
+ gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_active));
+ } else {
+ gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_inactive));
+ gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_inactive));
+ }
+}
+
+static GtkOrientation
+get_orientation (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ORIENTATION_HORIZONTAL);
+
+ return self->orientation;
+}
+
+static void
+set_orientation (HdyViewSwitcherButton *self,
+ GtkOrientation orientation)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+
+ gtk_stack_set_visible_child (self->stack,
+ GTK_WIDGET (self->orientation == GTK_ORIENTATION_VERTICAL ?
+ self->vertical_box :
+ self->horizontal_box));
+}
+
+static void
+hdy_view_switcher_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_view_switcher_button_get_icon_name (self));
+ break;
+ case PROP_ICON_SIZE:
+ g_value_set_int (value, hdy_view_switcher_button_get_icon_size (self));
+ break;
+ case PROP_NEEDS_ATTENTION:
+ g_value_set_boolean (value, hdy_view_switcher_button_get_needs_attention (self));
+ break;
+ case PROP_LABEL:
+ g_value_set_string (value, hdy_view_switcher_button_get_label (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_view_switcher_button_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_ICON_SIZE:
+ hdy_view_switcher_button_set_icon_size (self, g_value_get_int (value));
+ break;
+ case PROP_NEEDS_ATTENTION:
+ hdy_view_switcher_button_set_needs_attention (self, g_value_get_boolean (value));
+ break;
+ case PROP_LABEL:
+ hdy_view_switcher_button_set_label (self, g_value_get_string (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_button_finalize (GObject *object)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ g_free (self->icon_name);
+ g_free (self->label);
+
+ G_OBJECT_CLASS (hdy_view_switcher_button_parent_class)->finalize (object);
+}
+
+static void
+hdy_view_switcher_button_class_init (HdyViewSwitcherButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_button_get_property;
+ object_class->set_property = hdy_view_switcher_button_set_property;
+ object_class->finalize = hdy_view_switcher_button_finalize;
+
+ g_object_class_override_property (object_class,
+ PROP_LABEL,
+ "label");
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyViewSwitcherButton:icon-name:
+ *
+ * The icon name representing the view, or %NULL for no icon.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon Name"),
+ _("Icon name for image"),
+ "text-x-generic-symbolic",
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ /**
+ * HdyViewSwitcherButton:icon-size:
+ *
+ * The icon size.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_SIZE] =
+ g_param_spec_int ("icon-size",
+ _("Icon Size"),
+ _("Symbolic size to use for named icon"),
+ 0, G_MAXINT, GTK_ICON_SIZE_BUTTON,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ /**
+ * HdyViewSwitcherButton:needs-attention:
+ *
+ * Sets a flag specifying whether the view requires the user attention. This
+ * is used by the HdyViewSwitcher to change the appearance of the
+ * corresponding button when a view needs attention and it is not the current
+ * one.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_NEEDS_ATTENTION] =
+ g_param_spec_boolean ("needs-attention",
+ _("Needs attention"),
+ _("Hint the view needs attention"),
+ FALSE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /* We probably should set the class's CSS name to "viewswitcherbutton"
+ * here, but it doesn't work because GtkCheckButton hardcodes it to "button"
+ * on instantiation, and the functions required to override it are private.
+ * In the meantime, we can use the "viewswitcher > button" CSS selector as
+ * a fairly safe fallback.
+ */
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-button.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_image);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_active);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_inactive);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_image);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_active);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_inactive);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_stack);
+ gtk_widget_class_bind_template_callback (widget_class, on_active_changed);
+}
+
+static void
+hdy_view_switcher_button_init (HdyViewSwitcherButton *self)
+{
+ self->icon_size = GTK_ICON_SIZE_BUTTON;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_stack_set_visible_child (GTK_STACK (self->stack), GTK_WIDGET (self->horizontal_box));
+
+ gtk_widget_set_focus_on_click (GTK_WIDGET (self), FALSE);
+ /* Make the button look like a regular button and not a radio button. */
+ gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE);
+
+ on_active_changed (self);
+}
+
+/**
+ * hdy_view_switcher_button_new:
+ *
+ * Creates a new #HdyViewSwitcherButton widget.
+ *
+ * Returns: a new #HdyViewSwitcherButton
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_button_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_BUTTON, NULL);
+}
+
+/**
+ * hdy_view_switcher_button_get_icon_name:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the icon name representing the view, or %NULL is no icon is set.
+ *
+ * Returns: (transfer none) (nullable): the icon name, or %NULL
+ *
+ * Since: 0.0.10
+ **/
+const gchar *
+hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL);
+
+ return self->icon_name;
+}
+
+/**
+ * hdy_view_switcher_button_set_icon_name:
+ * @self: a #HdyViewSwitcherButton
+ * @icon_name: (nullable): an icon name or %NULL
+ *
+ * Sets the icon name representing the view, or %NULL to disable the icon.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self,
+ const gchar *icon_name)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (!g_strcmp0 (self->icon_name, icon_name))
+ return;
+
+ g_free (self->icon_name);
+ self->icon_name = g_strdup (icon_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_view_switcher_button_get_icon_size:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the icon size used by @self.
+ *
+ * Returns: the icon size used by @self
+ *
+ * Since: 0.0.10
+ **/
+GtkIconSize
+hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ICON_SIZE_INVALID);
+
+ return self->icon_size;
+}
+
+/**
+ * hdy_view_switcher_button_set_icon_size:
+ * @self: a #HdyViewSwitcherButton
+ * @icon_size: the new icon size
+ *
+ * Sets the icon size used by @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self,
+ GtkIconSize icon_size)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (self->icon_size == icon_size)
+ return;
+
+ self->icon_size = icon_size;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_SIZE]);
+}
+
+/**
+ * hdy_view_switcher_button_get_needs_attention:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets whether the view represented by @self requires the user attention.
+ *
+ * Returns: %TRUE if the view represented by @self requires the user attention, %FALSE otherwise
+ *
+ * Since: 0.0.10
+ **/
+gboolean
+hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self)
+{
+ GtkStyleContext *context;
+
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), FALSE);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ return gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+}
+
+/**
+ * hdy_view_switcher_button_set_needs_attention:
+ * @self: a #HdyViewSwitcherButton
+ * @needs_attention: the new icon size
+ *
+ * Sets whether the view represented by @self requires the user attention.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self,
+ gboolean needs_attention)
+{
+ GtkStyleContext *context;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ needs_attention = !!needs_attention;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ if (gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION) == needs_attention)
+ return;
+
+ if (needs_attention)
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+ else
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION]);
+}
+
+/**
+ * hdy_view_switcher_button_get_label:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the label representing the view.
+ *
+ * Returns: (transfer none) (nullable): the label, or %NULL
+ *
+ * Since: 0.0.10
+ **/
+const gchar *
+hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL);
+
+ return self->label;
+}
+
+/**
+ * hdy_view_switcher_button_set_label:
+ * @self: a #HdyViewSwitcherButton
+ * @label: (nullable): a label or %NULL
+ *
+ * Sets the label representing the view.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self,
+ const gchar *label)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (!g_strcmp0 (self->label, label))
+ return;
+
+ g_free (self->label);
+ self->label = g_strdup (label);
+
+ g_object_notify (G_OBJECT (self), "label");
+}
+
+/**
+ * hdy_view_switcher_button_set_narrow_ellipsize:
+ * @self: a #HdyViewSwitcherButton
+ * @mode: a #PangoEllipsizeMode
+ *
+ * Set the mode used to ellipsize the text in narrow mode if there is not
+ * enough space to render the entire string.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_narrow_ellipsize (HdyViewSwitcherButton *self,
+ PangoEllipsizeMode mode)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+ g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END);
+
+ gtk_label_set_ellipsize (self->vertical_label_active, mode);
+ gtk_label_set_ellipsize (self->vertical_label_inactive, mode);
+}
+
+/**
+ * hdy_view_switcher_button_get_size:
+ * @self: a #HdyViewSwitcherButton
+ * @h_min_width: (out) (nullable): the minimum width when horizontal
+ * @h_nat_width: (out) (nullable): the natural width when horizontal
+ * @v_min_width: (out) (nullable): the minimum width when vertical
+ * @v_nat_width: (out) (nullable): the natural width when vertical
+ *
+ * Measure the size requests in both horizontal and vertical modes.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self,
+ gint *h_min_width,
+ gint *h_nat_width,
+ gint *v_min_width,
+ gint *v_nat_width)
+{
+ GtkStyleContext *context;
+ GtkStateFlags state;
+ GtkBorder border;
+
+ /* gtk_widget_get_preferred_width() doesn't accept both its out parameters to
+ * be NULL, so we must have guards.
+ */
+ if (h_min_width != NULL || h_nat_width != NULL)
+ gtk_widget_get_preferred_width (GTK_WIDGET (self->horizontal_box), h_min_width, h_nat_width);
+ if (v_min_width != NULL || v_nat_width != NULL)
+ gtk_widget_get_preferred_width (GTK_WIDGET (self->vertical_box), v_min_width, v_nat_width);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ state = gtk_style_context_get_state (context);
+ gtk_style_context_get_border (context, state, &border);
+ if (h_min_width != NULL)
+ *h_min_width += border.left + border.right;
+ if (h_nat_width != NULL)
+ *h_nat_width += border.left + border.right;
+ if (v_min_width != NULL)
+ *v_min_width += border.left + border.right;
+ if (v_nat_width != NULL)
+ *v_nat_width += border.left + border.right;
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.ui b/subprojects/libhandy/src/hdy-view-switcher-button.ui
new file mode 100644
index 0000000..018b6cc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherButton" parent="GtkRadioButton">
+ <signal name="notify::active" handler="on_active_changed" after="yes"/>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="hhomogeneous">False</property>
+ <property name="transition-type">crossfade</property>
+ <property name="vhomogeneous">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="horizontal_box">
+ <property name="halign">center</property>
+ <property name="orientation">horizontal</property>
+ <property name="spacing">8</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="wide"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="horizontal_image">
+ <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" />
+ <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="horizontal_label_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="horizontal_label_inactive">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">inactive</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="horizontal_label_active">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ <style>
+ <class name="active"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">active</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="vertical_box">
+ <property name="halign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="narrow"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="vertical_image">
+ <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" />
+ <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="vertical_label_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="vertical_label_inactive">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">inactive</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="vertical_label_active">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ <style>
+ <class name="active"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">active</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.c b/subprojects/libhandy/src/hdy-view-switcher-title.c
new file mode 100644
index 0000000..39bdafa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.c
@@ -0,0 +1,600 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-view-switcher-title.h"
+#include "hdy-squeezer.h"
+
+/**
+ * SECTION:hdy-view-switcher-title
+ * @short_description: A view switcher title.
+ * @title: HdyViewSwitcherTitle
+ * @See_also: #HdyHeaderBar, #HdyViewSwitcher, #HdyViewSwitcherBar
+ *
+ * A widget letting you switch between multiple views offered by a #GtkStack,
+ * via an #HdyViewSwitcher. It is designed to be used as the title widget of a
+ * #HdyHeaderBar, and will display the window's title when the window is too
+ * narrow to fit the view switcher e.g. on mobile phones, or if there are less
+ * than two views.
+ *
+ * You can conveniently bind the #HdyViewSwitcherBar:reveal property to
+ * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher
+ * bar when the title label is displayed in place of the view switcher.
+ *
+ * An example of the UI definition for a common use case:
+ * |[
+ * <object class="GtkWindow"/>
+ * <child type="titlebar">
+ * <object class="HdyHeaderBar">
+ * <property name="centering-policy">strict</property>
+ * <child type="title">
+ * <object class="HdyViewSwitcherTitle"
+ * id="view_switcher_title">
+ * <property name="stack">stack</property>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * <child>
+ * <object class="GtkBox">
+ * <child>
+ * <object class="GtkStack" id="stack"/>
+ * </child>
+ * <child>
+ * <object class="HdyViewSwitcherBar">
+ * <property name="stack">stack</property>
+ * <property name="reveal"
+ * bind-source="view_switcher_title"
+ * bind-property="title-visible"
+ * bind-flags="sync-create"/>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcherTitle has a single CSS node with name viewswitchertitle.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_STACK,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_VIEW_SWITCHER_ENABLED,
+ PROP_TITLE_VISIBLE,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcherTitle
+{
+ GtkBin parent_instance;
+
+ HdySqueezer *squeezer;
+ GtkLabel *subtitle_label;
+ GtkBox *title_box;
+ GtkLabel *title_label;
+ HdyViewSwitcher *view_switcher;
+
+ gboolean view_switcher_enabled;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, GTK_TYPE_BIN)
+
+static void
+update_subtitle_label (HdyViewSwitcherTitle *self)
+{
+ const gchar *subtitle = gtk_label_get_label (self->subtitle_label);
+
+ gtk_widget_set_visible (GTK_WIDGET (self->subtitle_label), subtitle && subtitle[0]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+update_view_switcher_visible (HdyViewSwitcherTitle *self)
+{
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+ gint count = 0;
+
+ if (self->view_switcher_enabled && stack)
+ gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count);
+
+ hdy_squeezer_set_child_enabled (self->squeezer, GTK_WIDGET (self->view_switcher), count > 1);
+}
+
+static void
+notify_squeezer_visible_child_cb (GObject *self)
+{
+ g_object_notify_by_pspec (self, props[PROP_TITLE_VISIBLE]);
+}
+
+static void
+hdy_view_switcher_title_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_title_get_policy (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_title_get_stack (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_view_switcher_title_get_title (self));
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_view_switcher_title_get_subtitle (self));
+ break;
+ case PROP_VIEW_SWITCHER_ENABLED:
+ g_value_set_boolean (value, hdy_view_switcher_title_get_view_switcher_enabled (self));
+ break;
+ case PROP_TITLE_VISIBLE:
+ g_value_set_boolean (value, hdy_view_switcher_title_get_title_visible (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_title_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_title_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_title_set_stack (self, g_value_get_object (value));
+ break;
+ case PROP_TITLE:
+ hdy_view_switcher_title_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_view_switcher_title_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_VIEW_SWITCHER_ENABLED:
+ hdy_view_switcher_title_set_view_switcher_enabled (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_title_dispose (GObject *object) {
+ HdyViewSwitcherTitle *self = (HdyViewSwitcherTitle *)object;
+
+ if (self->view_switcher) {
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (stack)
+ g_signal_handlers_disconnect_by_func (stack, G_CALLBACK (update_view_switcher_visible), self);
+ }
+
+ G_OBJECT_CLASS (hdy_view_switcher_title_parent_class)->dispose (object);
+}
+
+static void
+hdy_view_switcher_title_class_init (HdyViewSwitcherTitleClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_view_switcher_title_dispose;
+ object_class->get_property = hdy_view_switcher_title_get_property;
+ object_class->set_property = hdy_view_switcher_title_set_property;
+
+ /**
+ * HdyViewSwitcherTitle:policy:
+ *
+ * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine
+ * which mode to use.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:stack:
+ *
+ * The #GtkStack the #HdyViewSwitcher controls.
+ *
+ * Since: 1.0
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:title:
+ *
+ * The title of the #HdyViewSwitcher.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title to display"),
+ NULL,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:subtitle:
+ *
+ * The subtitle of the #HdyViewSwitcher.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle to display"),
+ NULL,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:view-switcher-enabled:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VIEW_SWITCHER_ENABLED] =
+ g_param_spec_boolean ("view-switcher-enabled",
+ _("View switcher enabled"),
+ _("Whether the view switcher is enabled"),
+ TRUE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:title-visible:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TITLE_VISIBLE] =
+ g_param_spec_boolean ("title-visible",
+ _("Title visible"),
+ _("Whether the title label is visible"),
+ TRUE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitchertitle");
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-title.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, squeezer);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, subtitle_label);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_label);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, view_switcher);
+ gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb);
+}
+
+static void
+hdy_view_switcher_title_init (HdyViewSwitcherTitle *self)
+{
+ /* This must be initialized before the template so the embedded view switcher
+ * can pick up the correct default value.
+ */
+ self->view_switcher_enabled = TRUE;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ update_subtitle_label (self);
+ update_view_switcher_visible (self);
+}
+
+/**
+ * hdy_view_switcher_title_new:
+ *
+ * Creates a new #HdyViewSwitcherTitle widget.
+ *
+ * Returns: a new #HdyViewSwitcherTitle
+ *
+ * Since: 1.0
+ */
+HdyViewSwitcherTitle *
+hdy_view_switcher_title_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_TITLE, NULL);
+}
+
+/**
+ * hdy_view_switcher_title_get_policy:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 1.0
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ return hdy_view_switcher_get_policy (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_title_set_policy:
+ * @self: a #HdyViewSwitcherTitle
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (hdy_view_switcher_get_policy (self->view_switcher) == policy)
+ return;
+
+ hdy_view_switcher_set_policy (self->view_switcher, policy);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_title_get_stack:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+GtkStack *
+hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return hdy_view_switcher_get_stack (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_title_set_stack:
+ * @self: a #HdyViewSwitcherTitle
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self,
+ GtkStack *stack)
+{
+ GtkStack *previous_stack;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ previous_stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (previous_stack == stack)
+ return;
+
+ if (previous_stack)
+ g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_view_switcher_visible), self);
+
+ hdy_view_switcher_set_stack (self->view_switcher, stack);
+
+ if (stack) {
+ g_signal_connect_swapped (stack, "add", G_CALLBACK (update_view_switcher_visible), self);
+ g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_view_switcher_visible), self);
+ }
+
+ update_view_switcher_visible (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
+
+/**
+ * hdy_view_switcher_title_get_title:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the title of @self. See hdy_view_switcher_title_set_title().
+ *
+ * Returns: (transfer none) (nullable): the title of @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return gtk_label_get_label (self->title_label);
+}
+
+/**
+ * hdy_view_switcher_title_set_title:
+ * @self: a #HdyViewSwitcherTitle
+ * @title: (nullable): a title, or %NULL
+ *
+ * Sets the title of @self. The title should give a user additional details. A
+ * good title should not include the application name.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self,
+ const gchar *title)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (g_strcmp0 (gtk_label_get_label (self->title_label), title) == 0)
+ return;
+
+ gtk_label_set_label (self->title_label, title);
+ gtk_widget_set_visible (GTK_WIDGET (self->title_label), title && title[0]);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_view_switcher_title_get_subtitle:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the subtitle of @self. See hdy_view_switcher_title_set_subtitle().
+ *
+ * Returns: (transfer none) (nullable): the subtitle of @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return gtk_label_get_label (self->subtitle_label);
+}
+
+/**
+ * hdy_view_switcher_title_set_subtitle:
+ * @self: a #HdyViewSwitcherTitle
+ * @subtitle: (nullable): a subtitle, or %NULL
+ *
+ * Sets the subtitle of @self. The subtitle should give a user additional
+ * details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self,
+ const gchar *subtitle)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (g_strcmp0 (gtk_label_get_label (self->subtitle_label), subtitle) == 0)
+ return;
+
+ gtk_label_set_label (self->subtitle_label, subtitle);
+ update_subtitle_label (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_view_switcher_title_get_view_switcher_enabled:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets whether @self's view switcher is enabled.
+ *
+ * See hdy_view_switcher_title_set_view_switcher_enabled().
+ *
+ * Returns: %TRUE if the view switcher is enabled, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
+
+ return self->view_switcher_enabled;
+}
+
+/**
+ * hdy_view_switcher_title_set_view_switcher_enabled:
+ * @self: a #HdyViewSwitcherTitle
+ * @enabled: %TRUE to enable the view switcher, %FALSE to disable it
+ *
+ * Make @self enable or disable its view switcher. If it is disabled, the title
+ * will be displayed instead. This allows to programmatically and prematurely
+ * hide the view switcher of @self even if it fits in the available space.
+ *
+ * This can be used e.g. to ensure the view switcher is hidden below a certain
+ * window width, or any other constraint you find suitable.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self,
+ gboolean enabled)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ enabled = !!enabled;
+
+ if (self->view_switcher_enabled == enabled)
+ return;
+
+ self->view_switcher_enabled = enabled;
+ update_view_switcher_visible (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]);
+}
+
+/**
+ * hdy_view_switcher_title_get_title_visible:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Get whether the title label of @self is visible.
+ *
+ * Returns: %TRUE if the title label of @self is visible, %FALSE if not.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
+
+ return hdy_squeezer_get_visible_child (self->squeezer) == (GtkWidget *) self->title_box;
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.h b/subprojects/libhandy/src/hdy-view-switcher-title.h
new file mode 100644
index 0000000..2540396
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+#include "hdy-view-switcher.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_TITLE (hdy_view_switcher_title_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, HDY, VIEW_SWITCHER_TITLE, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherTitle *hdy_view_switcher_title_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self,
+ GtkStack *stack);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.ui b/subprojects/libhandy/src/hdy-view-switcher-title.ui
new file mode 100644
index 0000000..1c706bc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherTitle" parent="GtkBin">
+ <child>
+ <object class="HdySqueezer" id="squeezer">
+ <property name="transition-type">crossfade</property>
+ <property name="visible">True</property>
+ <property name="no-show-all">True</property>
+ <signal name="notify::visible-child" handler="notify_squeezer_visible_child_cb" swapped="yes"/>
+ <child>
+ <object class="HdyViewSwitcher" id="view_switcher">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="title_box">
+ <property name="orientation">vertical</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="title_label">
+ <property name="ellipsize">end</property>
+ <property name="halign">center</property>
+ <property name="wrap">False</property>
+ <property name="single-line-mode">True</property>
+ <property name="visible">True</property>
+ <property name="width-chars">5</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle_label">
+ <property name="ellipsize">end</property>
+ <property name="halign">center</property>
+ <property name="wrap">False</property>
+ <property name="single-line-mode">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="subtitle"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher.c b/subprojects/libhandy/src/hdy-view-switcher.c
new file mode 100644
index 0000000..26c5bcf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher.c
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * Based on gtkstackswitcher.c, Copyright (c) 2013 Red Hat, Inc.
+ * https://gitlab.gnome.org/GNOME/gtk/blob/a0129f556b1fd655215165739d0277d7f7a2c1a8/gtk/gtkstackswitcher.c
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-css-private.h"
+#include "hdy-enums.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-button-private.h"
+
+/**
+ * SECTION:hdy-view-switcher
+ * @short_description: An adaptive view switcher.
+ * @title: HdyViewSwitcher
+ *
+ * An adaptive view switcher, designed to switch between multiple views in a
+ * similar fashion than a #GtkStackSwitcher.
+ *
+ * Depending on the available width, the view switcher can adapt from a wide
+ * mode showing the view's icon and title side by side, to a narrow mode showing
+ * the view's icon and title one on top of the other, in a more compact way.
+ * This can be controlled via the policy property.
+ *
+ * To look good in a header bar, an #HdyViewSwitcher requires to fill its full
+ * height. Contrary to #GtkHeaderBar, #HdyHeaderBar doesn't force a vertical
+ * alignment on its title widget, so we recommend it over #GtkHeaderBar.
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcher has a single CSS node with name viewswitcher.
+ *
+ * Since: 0.0.10
+ */
+
+/**
+ * HdyViewSwitcherPolicy:
+ * @HDY_VIEW_SWITCHER_POLICY_AUTO: Automatically adapt to the best fitting mode
+ * @HDY_VIEW_SWITCHER_POLICY_NARROW: Force the narrow mode
+ * @HDY_VIEW_SWITCHER_POLICY_WIDE: Force the wide mode
+ */
+
+#define MIN_NAT_BUTTON_WIDTH 100
+#define TIMEOUT_EXPAND 500
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_NARROW_ELLIPSIZE,
+ PROP_STACK,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcher
+{
+ GtkBin parent_instance;
+
+ GtkWidget *box;
+ GHashTable *buttons;
+ gboolean in_child_changed;
+ GtkWidget *switch_button;
+ guint switch_timer;
+
+ HdyViewSwitcherPolicy policy;
+ PangoEllipsizeMode narrow_ellipsize;
+ GtkStack *stack;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcher, hdy_view_switcher, GTK_TYPE_BIN)
+
+static void
+set_visible_stack_child_for_button (HdyViewSwitcher *self,
+ HdyViewSwitcherButton *button)
+{
+ if (self->in_child_changed)
+ return;
+
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (g_object_get_data (G_OBJECT (button), "stack-child")));
+}
+
+static void
+update_button (HdyViewSwitcher *self,
+ GtkWidget *widget,
+ HdyViewSwitcherButton *button)
+{
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *icon_name = NULL;
+ gboolean needs_attention;
+
+ gtk_container_child_get (GTK_CONTAINER (self->stack), widget,
+ "title", &title,
+ "icon-name", &icon_name,
+ "needs-attention", &needs_attention,
+ NULL);
+
+ g_object_set (G_OBJECT (button),
+ "icon-name", icon_name,
+ "icon-size", GTK_ICON_SIZE_BUTTON,
+ "label", title,
+ "needs-attention", needs_attention,
+ NULL);
+
+ gtk_widget_set_visible (GTK_WIDGET (button),
+ gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL));
+}
+
+static void
+on_stack_child_updated (GtkWidget *widget,
+ GParamSpec *pspec,
+ HdyViewSwitcher *self)
+{
+ update_button (self, widget, g_hash_table_lookup (self->buttons, widget));
+}
+
+static void
+on_position_updated (GtkWidget *widget,
+ GParamSpec *pspec,
+ HdyViewSwitcher *self)
+{
+ GtkWidget *button = g_hash_table_lookup (self->buttons, widget);
+ gint position;
+
+ gtk_container_child_get (GTK_CONTAINER (self->stack), widget,
+ "position", &position,
+ NULL);
+ gtk_box_reorder_child (GTK_BOX (self->box), button, position);
+}
+
+static void
+remove_switch_timer (HdyViewSwitcher *self)
+{
+ if (!self->switch_timer)
+ return;
+
+ g_source_remove (self->switch_timer);
+ self->switch_timer = 0;
+}
+
+static gboolean
+hdy_view_switcher_switch_timeout (gpointer data)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (data);
+ GtkWidget *button = self->switch_button;
+
+ self->switch_timer = 0;
+ self->switch_button = NULL;
+
+ if (button)
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+hdy_view_switcher_drag_motion (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+ GtkAllocation allocation;
+ GtkWidget *button;
+ GHashTableIter iter;
+ gpointer value;
+ gboolean retval = FALSE;
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ x += allocation.x;
+ y += allocation.y;
+
+ button = NULL;
+ g_hash_table_iter_init (&iter, self->buttons);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ gtk_widget_get_allocation (GTK_WIDGET (value), &allocation);
+ if (x >= allocation.x && x <= allocation.x + allocation.width &&
+ y >= allocation.y && y <= allocation.y + allocation.height) {
+ button = GTK_WIDGET (value);
+ retval = TRUE;
+
+ break;
+ }
+ }
+
+ if (button != self->switch_button)
+ remove_switch_timer (self);
+
+ self->switch_button = button;
+
+ if (button && !self->switch_timer) {
+ self->switch_timer = gdk_threads_add_timeout (TIMEOUT_EXPAND,
+ hdy_view_switcher_switch_timeout,
+ self);
+ g_source_set_name_by_id (self->switch_timer, "[gtk+] hdy_view_switcher_switch_timeout");
+ }
+
+ return retval;
+}
+
+static void
+hdy_view_switcher_drag_leave (GtkWidget *widget,
+ GdkDragContext *context,
+ guint time)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+
+ remove_switch_timer (self);
+}
+
+static void
+add_button_for_stack_child (HdyViewSwitcher *self,
+ GtkWidget *stack_child)
+{
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ HdyViewSwitcherButton *button = HDY_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_new ());
+
+ g_object_set_data (G_OBJECT (button), "stack-child", stack_child);
+ hdy_view_switcher_button_set_narrow_ellipsize (button, self->narrow_ellipsize);
+
+ update_button (self, stack_child, button);
+
+ if (children != NULL)
+ gtk_radio_button_join_group (GTK_RADIO_BUTTON (button), GTK_RADIO_BUTTON (children->data));
+
+ gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (button));
+
+ g_signal_connect_swapped (button, "clicked", G_CALLBACK (set_visible_stack_child_for_button), self);
+ g_signal_connect (stack_child, "notify::visible", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::title", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::icon-name", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::needs-attention", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::position", G_CALLBACK (on_position_updated), self);
+
+ g_hash_table_insert (self->buttons, stack_child, button);
+}
+
+static void
+add_button_for_stack_child_cb (GtkWidget *stack_child,
+ HdyViewSwitcher *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (GTK_IS_WIDGET (stack_child));
+
+ add_button_for_stack_child (self, stack_child);
+}
+
+static void
+remove_button_for_stack_child (HdyViewSwitcher *self,
+ GtkWidget *stack_child)
+{
+ g_signal_handlers_disconnect_by_func (stack_child, on_stack_child_updated, self);
+ g_signal_handlers_disconnect_by_func (stack_child, on_position_updated, self);
+ gtk_container_remove (GTK_CONTAINER (self->box), g_hash_table_lookup (self->buttons, stack_child));
+ g_hash_table_remove (self->buttons, stack_child);
+}
+
+static void
+remove_button_for_stack_child_cb (GtkWidget *stack_child,
+ HdyViewSwitcher *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (GTK_IS_WIDGET (stack_child));
+
+ remove_button_for_stack_child (self, stack_child);
+}
+
+static void
+update_active_button_for_visible_stack_child (HdyViewSwitcher *self)
+{
+ GtkWidget *visible_stack_child = gtk_stack_get_visible_child (self->stack);
+ GtkWidget *button = g_hash_table_lookup (self->buttons, visible_stack_child);
+
+ if (button == NULL)
+ return;
+
+ self->in_child_changed = TRUE;
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
+ self->in_child_changed = FALSE;
+}
+
+static void
+disconnect_stack_signals (HdyViewSwitcher *self)
+{
+ g_signal_handlers_disconnect_by_func (self->stack, add_button_for_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, remove_button_for_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, update_active_button_for_visible_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, disconnect_stack_signals, self);
+}
+
+static void
+connect_stack_signals (HdyViewSwitcher *self)
+{
+ g_signal_connect_object (self->stack, "add",
+ G_CALLBACK (add_button_for_stack_child), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "remove",
+ G_CALLBACK (remove_button_for_stack_child), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "notify::visible-child",
+ G_CALLBACK (update_active_button_for_visible_stack_child), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "destroy",
+ G_CALLBACK (disconnect_stack_signals), self,
+ G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_view_switcher_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_get_policy (self));
+ break;
+ case PROP_NARROW_ELLIPSIZE:
+ g_value_set_enum (value, hdy_view_switcher_get_narrow_ellipsize (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_get_stack (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_NARROW_ELLIPSIZE:
+ hdy_view_switcher_set_narrow_ellipsize (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_set_stack (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_dispose (GObject *object)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ remove_switch_timer (self);
+ hdy_view_switcher_set_stack (self, NULL);
+
+ G_OBJECT_CLASS (hdy_view_switcher_parent_class)->dispose (object);
+}
+
+static void
+hdy_view_switcher_finalize (GObject *object)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ g_hash_table_destroy (self->buttons);
+
+ G_OBJECT_CLASS (hdy_view_switcher_parent_class)->finalize (object);
+}
+
+static void
+hdy_view_switcher_get_preferred_width (GtkWidget *widget,
+ gint *min,
+ gint *nat)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ gint max_h_min = 0, max_h_nat = 0, max_v_min = 0, max_v_nat = 0;
+ gint n_children = 0;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l)) {
+ gint h_min = 0, h_nat = 0, v_min = 0, v_nat = 0;
+
+ if (!gtk_widget_get_visible (l->data))
+ continue;
+
+ hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, &h_nat, &v_min, &v_nat);
+ max_h_min = MAX (h_min, max_h_min);
+ max_h_nat = MAX (h_nat, max_h_nat);
+ max_v_min = MAX (v_min, max_v_min);
+ max_v_nat = MAX (v_nat, max_v_nat);
+
+ n_children++;
+ }
+
+ /* Make the buttons ask at least a minimum arbitrary size for their natural
+ * width. This prevents them from looking terribly narrow in a very wide bar.
+ */
+ max_h_nat = MAX (max_h_nat, MIN_NAT_BUTTON_WIDTH);
+ max_v_nat = MAX (max_v_nat, MIN_NAT_BUTTON_WIDTH);
+
+ switch (self->policy) {
+ case HDY_VIEW_SWITCHER_POLICY_NARROW:
+ *min = max_v_min * n_children;
+ *nat = max_v_nat * n_children;
+ break;
+ case HDY_VIEW_SWITCHER_POLICY_WIDE:
+ *min = max_h_min * n_children;
+ *nat = max_h_nat * n_children;
+ break;
+ case HDY_VIEW_SWITCHER_POLICY_AUTO:
+ default:
+ *min = max_v_min * n_children;
+ *nat = max_h_nat * n_children;
+ break;
+ }
+
+ hdy_css_measure (widget, GTK_ORIENTATION_HORIZONTAL, min, nat);
+}
+
+static gint
+is_narrow (HdyViewSwitcher *self,
+ gint width)
+{
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ gint max_h_min = 0;
+ gint n_children = 0;
+
+ if (self->policy == HDY_VIEW_SWITCHER_POLICY_NARROW)
+ return TRUE;
+
+ if (self->policy == HDY_VIEW_SWITCHER_POLICY_WIDE)
+ return FALSE;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l)) {
+ gint h_min = 0;
+
+ hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, NULL, NULL, NULL);
+ max_h_min = MAX (max_h_min, h_min);
+
+ n_children++;
+ }
+
+ return (max_h_min * n_children) > width;
+}
+
+static void
+hdy_view_switcher_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ GtkOrientation orientation;
+
+ hdy_css_size_allocate (widget, allocation);
+
+ orientation = is_narrow (HDY_VIEW_SWITCHER (widget), allocation->width) ?
+ GTK_ORIENTATION_VERTICAL :
+ GTK_ORIENTATION_HORIZONTAL;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l))
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (l->data), orientation);
+
+ GTK_WIDGET_CLASS (hdy_view_switcher_parent_class)->size_allocate (widget, allocation);
+}
+
+static void
+hdy_view_switcher_class_init (HdyViewSwitcherClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_get_property;
+ object_class->set_property = hdy_view_switcher_set_property;
+ object_class->dispose = hdy_view_switcher_dispose;
+ object_class->finalize = hdy_view_switcher_finalize;
+
+ widget_class->size_allocate = hdy_view_switcher_size_allocate;
+ widget_class->get_preferred_width = hdy_view_switcher_get_preferred_width;
+ widget_class->drag_motion = hdy_view_switcher_drag_motion;
+ widget_class->drag_leave = hdy_view_switcher_drag_leave;
+
+ /**
+ * HdyViewSwitcher:policy:
+ *
+ * The #HdyViewSwitcherPolicy the view switcher should use to determine which
+ * mode to use.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcher:narrow-ellipsize:
+ *
+ * The preferred place to ellipsize the string, if the narrow mode label does
+ * not have enough room to display the entire string, specified as a
+ * #PangoEllipsizeMode.
+ *
+ * Note that setting this property to a value other than %PANGO_ELLIPSIZE_NONE
+ * has the side-effect that the label requests only enough space to display
+ * the ellipsis.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_NARROW_ELLIPSIZE] =
+ g_param_spec_enum ("narrow-ellipsize",
+ _("Narrow ellipsize"),
+ _("The preferred place to ellipsize the string, if the narrow mode label does not have enough room to display the entire string"),
+ PANGO_TYPE_ELLIPSIZE_MODE,
+ PANGO_ELLIPSIZE_NONE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyViewSwitcher:stack:
+ *
+ * The #GtkStack the view switcher controls.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitcher");
+}
+
+static void
+hdy_view_switcher_init (HdyViewSwitcher *self)
+{
+ self->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show (self->box);
+ gtk_box_set_homogeneous (GTK_BOX (self->box), TRUE);
+ gtk_container_add (GTK_CONTAINER (self), self->box);
+
+ self->buttons = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+ gtk_widget_set_valign (GTK_WIDGET (self), GTK_ALIGN_FILL);
+
+ gtk_drag_dest_set (GTK_WIDGET (self), 0, NULL, 0, 0);
+ gtk_drag_dest_set_track_motion (GTK_WIDGET (self), TRUE);
+}
+
+/**
+ * hdy_view_switcher_new:
+ *
+ * Creates a new #HdyViewSwitcher widget.
+ *
+ * Returns: a new #HdyViewSwitcher
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER, NULL);
+}
+
+/**
+ * hdy_view_switcher_get_policy:
+ * @self: a #HdyViewSwitcher
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 0.0.10
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_get_policy (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), HDY_VIEW_SWITCHER_POLICY_AUTO);
+
+ return self->policy;
+}
+
+/**
+ * hdy_view_switcher_set_policy:
+ * @self: a #HdyViewSwitcher
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_set_policy (HdyViewSwitcher *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+
+ if (self->policy == policy)
+ return;
+
+ self->policy = policy;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_get_narrow_ellipsize:
+ * @self: a #HdyViewSwitcher
+ *
+ * Get the ellipsizing position of the narrow mode label. See
+ * hdy_view_switcher_set_narrow_ellipsize().
+ *
+ * Returns: #PangoEllipsizeMode
+ *
+ * Since: 0.0.10
+ **/
+PangoEllipsizeMode
+hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), PANGO_ELLIPSIZE_NONE);
+
+ return self->narrow_ellipsize;
+}
+
+/**
+ * hdy_view_switcher_set_narrow_ellipsize:
+ * @self: a #HdyViewSwitcher
+ * @mode: a #PangoEllipsizeMode
+ *
+ * Set the mode used to ellipsize the text in narrow mode if there is not
+ * enough space to render the entire string.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self,
+ PangoEllipsizeMode mode)
+{
+ GHashTableIter iter;
+ gpointer button;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END);
+
+ if ((PangoEllipsizeMode) self->narrow_ellipsize == mode)
+ return;
+
+ self->narrow_ellipsize = mode;
+
+ g_hash_table_iter_init (&iter, self->buttons);
+ while (g_hash_table_iter_next (&iter, NULL, &button))
+ hdy_view_switcher_button_set_narrow_ellipsize (HDY_VIEW_SWITCHER_BUTTON (button), mode);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NARROW_ELLIPSIZE]);
+}
+
+/**
+ * hdy_view_switcher_get_stack:
+ * @self: a #HdyViewSwitcher
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * See: hdy_view_switcher_set_stack()
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 0.0.10
+ */
+GtkStack *
+hdy_view_switcher_get_stack (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), NULL);
+
+ return self->stack;
+}
+
+/**
+ * hdy_view_switcher_set_stack:
+ * @self: a #HdyViewSwitcher
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_set_stack (HdyViewSwitcher *self,
+ GtkStack *stack)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ if (self->stack == stack)
+ return;
+
+ if (self->stack) {
+ disconnect_stack_signals (self);
+ gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) remove_button_for_stack_child_cb, self);
+ }
+
+ g_set_object (&self->stack, stack);
+
+ if (self->stack) {
+ gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) add_button_for_stack_child_cb, self);
+ update_active_button_for_visible_stack_child (self);
+ connect_stack_signals (self);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher.h b/subprojects/libhandy/src/hdy-view-switcher.h
new file mode 100644
index 0000000..3ec02f6
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER (hdy_view_switcher_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcher, hdy_view_switcher, HDY, VIEW_SWITCHER, GtkBin)
+
+typedef enum {
+ HDY_VIEW_SWITCHER_POLICY_AUTO,
+ HDY_VIEW_SWITCHER_POLICY_NARROW,
+ HDY_VIEW_SWITCHER_POLICY_WIDE,
+} HdyViewSwitcherPolicy;
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_view_switcher_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_get_policy (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_policy (HdyViewSwitcher *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+PangoEllipsizeMode hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self,
+ PangoEllipsizeMode mode);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_get_stack (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_stack (HdyViewSwitcher *self,
+ GtkStack *stack);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-handle-controller-private.h b/subprojects/libhandy/src/hdy-window-handle-controller-private.h
new file mode 100644
index 0000000..0a19251
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle-controller-private.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_HANDLE_CONTROLLER (hdy_window_handle_controller_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyWindowHandleController, hdy_window_handle_controller, HDY, WINDOW_HANDLE_CONTROLLER, GObject)
+
+HdyWindowHandleController *hdy_window_handle_controller_new (GtkWidget *widget);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-handle-controller.c b/subprojects/libhandy/src/hdy-window-handle-controller.c
new file mode 100644
index 0000000..d668745
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle-controller.c
@@ -0,0 +1,515 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
+ * file for a list of people on the GTK+ Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/* Most of the file is based on bits of code from GtkWindow */
+
+#include "config.h"
+
+#include "gtk-window-private.h"
+#include "hdy-window-handle-controller-private.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * PRIVATE:hdy-window-handle-controller
+ * @short_description: An oblect that makes widgets behave like titlebars.
+ * @Title: HdyWindowHandleController
+ * @See_also: #HdyHeaderBar, #HdyWindowHandle
+ * @stability: Private
+ *
+ * When HdyWindowHandleController is added to the widget, dragging that widget
+ * will move the window, and right click, double click and middle click will be
+ * handled as if that widget was a titlebar. Currently it's used to implement
+ * these properties in #HdyWindowHandle and #HdyHeaderBar
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowHandleController
+{
+ GObject parent;
+
+ GtkWidget *widget;
+ GtkGesture *multipress_gesture;
+ GtkWidget *fallback_menu;
+ gboolean keep_above;
+};
+
+G_DEFINE_TYPE (HdyWindowHandleController, hdy_window_handle_controller, G_TYPE_OBJECT);
+
+static GtkWindow *
+get_window (HdyWindowHandleController *self)
+{
+ GtkWidget *toplevel = gtk_widget_get_toplevel (self->widget);
+
+ if (GTK_IS_WINDOW (toplevel))
+ return GTK_WINDOW (toplevel);
+
+ return NULL;
+}
+
+static void
+popup_menu_detach (GtkWidget *widget,
+ GtkMenu *menu)
+{
+ HdyWindowHandleController *self;
+
+ self = g_object_steal_data (G_OBJECT (menu), "hdywindowhandlecontroller");
+
+ self->fallback_menu = NULL;
+}
+
+static void
+restore_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+ GdkWindowState state;
+
+ if (!window)
+ return;
+
+ if (gtk_window_is_maximized (window)) {
+ gtk_window_unmaximize (window);
+ return;
+ }
+
+ state = hdy_gtk_window_get_state (window);
+
+ if (state & GDK_WINDOW_STATE_ICONIFIED)
+ gtk_window_deiconify (window);
+}
+
+static void
+move_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_begin_move_drag (window,
+ 0, /* 0 means "use keyboard" */
+ 0, 0,
+ GDK_CURRENT_TIME);
+}
+
+static void
+resize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_begin_resize_drag (window,
+ 0,
+ 0, /* 0 means "use keyboard" */
+ 0, 0,
+ GDK_CURRENT_TIME);
+}
+
+static void
+minimize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ /* Turns out, we can't iconify a maximized window */
+ if (gtk_window_is_maximized (window))
+ gtk_window_unmaximize (window);
+
+ gtk_window_iconify (window);
+}
+
+static void
+maximize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+ GdkWindowState state;
+
+ if (!window)
+ return;
+
+ state = hdy_gtk_window_get_state (window);
+
+ if (state & GDK_WINDOW_STATE_ICONIFIED)
+ gtk_window_deiconify (window);
+
+ gtk_window_maximize (window);
+}
+
+static void
+ontop_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ /*
+ * FIXME: It will go out of sync if something else calls
+ * gtk_window_set_keep_above(), so we need to actually track it.
+ * For some reason this doesn't seem to be reflected in the
+ * window state.
+ */
+ self->keep_above = !self->keep_above;
+ gtk_window_set_keep_above (window, self->keep_above);
+}
+
+static void
+close_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_close (window);
+}
+
+static void
+do_popup (HdyWindowHandleController *self,
+ GdkEventButton *event)
+{
+ GtkWindow *window = get_window (self);
+ GtkWidget *menuitem;
+ GdkWindowState state;
+ gboolean maximized, iconified, resizable;
+ GdkWindowTypeHint type_hint;
+
+ if (!window)
+ return;
+
+ if (gdk_window_show_window_menu (gtk_widget_get_window (GTK_WIDGET (window)),
+ (GdkEvent *) event))
+ return;
+
+ if (self->fallback_menu)
+ gtk_widget_destroy (self->fallback_menu);
+
+ state = hdy_gtk_window_get_state (window);
+
+ iconified = (state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED;
+ maximized = gtk_window_is_maximized (window) && !iconified;
+ resizable = gtk_window_get_resizable (window);
+ type_hint = gtk_window_get_type_hint (window);
+
+ self->fallback_menu = gtk_menu_new ();
+ gtk_style_context_add_class (gtk_widget_get_style_context (self->fallback_menu),
+ GTK_STYLE_CLASS_CONTEXT_MENU);
+
+ /* We can't pass self to popup_menu_detach, so will have to use custom data */
+ g_object_set_data (G_OBJECT (self->fallback_menu),
+ "hdywindowhandlecontroller", self);
+
+ gtk_menu_attach_to_widget (GTK_MENU (self->fallback_menu),
+ self->widget,
+ popup_menu_detach);
+
+ menuitem = gtk_menu_item_new_with_label (_("Restore"));
+ gtk_widget_show (menuitem);
+ /* "Restore" means "Unmaximize" or "Unminimize"
+ * (yes, some WMs allow window menu to be shown for minimized windows).
+ * Not restorable:
+ * - visible windows that are not maximized or minimized
+ * - non-resizable windows that are not minimized
+ * - non-normal windows
+ */
+ if ((gtk_widget_is_visible (GTK_WIDGET (window)) &&
+ !(maximized || iconified)) ||
+ (!iconified && !resizable) ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (restore_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Move"));
+ gtk_widget_show (menuitem);
+ if (maximized || iconified)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (move_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Resize"));
+ gtk_widget_show (menuitem);
+ if (!resizable || maximized || iconified)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (resize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Minimize"));
+ gtk_widget_show (menuitem);
+ if (iconified ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (minimize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Maximize"));
+ gtk_widget_show (menuitem);
+ if (maximized ||
+ !resizable ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (maximize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_separator_menu_item_new ();
+ gtk_widget_show (menuitem);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_check_menu_item_new_with_label (_("Always on Top"));
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), self->keep_above);
+ if (maximized)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ gtk_widget_show (menuitem);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (ontop_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_separator_menu_item_new ();
+ gtk_widget_show (menuitem);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Close"));
+ gtk_widget_show (menuitem);
+ if (!gtk_window_get_deletable (window))
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (close_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+ gtk_menu_popup_at_pointer (GTK_MENU (self->fallback_menu), (GdkEvent *) event);
+}
+
+static gboolean
+titlebar_action (HdyWindowHandleController *self,
+ const GdkEvent *event,
+ guint button)
+{
+ GtkSettings *settings;
+ g_autofree gchar *action = NULL;
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return FALSE;
+
+ settings = gtk_widget_get_settings (GTK_WIDGET (window));
+
+ switch (button) {
+ case GDK_BUTTON_PRIMARY:
+ g_object_get (settings, "gtk-titlebar-double-click", &action, NULL);
+ break;
+
+ case GDK_BUTTON_MIDDLE:
+ g_object_get (settings, "gtk-titlebar-middle-click", &action, NULL);
+ break;
+
+ case GDK_BUTTON_SECONDARY:
+ g_object_get (settings, "gtk-titlebar-right-click", &action, NULL);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (action == NULL)
+ return FALSE;
+
+ if (g_str_equal (action, "none"))
+ return FALSE;
+
+ if (g_str_has_prefix (action, "toggle-maximize")) {
+ /*
+ * gtk header bar won't show the maximize button if the following
+ * properties are not met, apply the same to title bar actions for
+ * consistency.
+ */
+ if (gtk_window_get_resizable (window) &&
+ gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL)
+ hdy_gtk_window_toggle_maximized (window);
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "lower")) {
+ gdk_window_lower (gtk_widget_get_window (GTK_WIDGET (window)));
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "minimize")) {
+ gdk_window_iconify (gtk_widget_get_window (GTK_WIDGET (window)));
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "menu")) {
+ do_popup (self, (GdkEventButton*) event);
+
+ return TRUE;
+ }
+
+ g_warning ("Unsupported titlebar action %s", action);
+
+ return FALSE;
+}
+
+static void
+pressed_cb (GtkGestureMultiPress *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ HdyWindowHandleController *self)
+{
+ GtkWidget *window = gtk_widget_get_toplevel (self->widget);
+ GdkEventSequence *sequence =
+ gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+ const GdkEvent *event =
+ gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
+ guint button =
+ gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+ if (!event)
+ return;
+
+ if (gdk_display_device_is_grabbed (gtk_widget_get_display (window),
+ gtk_gesture_get_device (GTK_GESTURE (gesture))))
+ return;
+
+ switch (button) {
+ case GDK_BUTTON_PRIMARY:
+ gdk_window_raise (gtk_widget_get_window (window));
+
+ if (n_press == 2)
+ titlebar_action (self, event, button);
+
+ if (gtk_widget_has_grab (window))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ break;
+
+ case GDK_BUTTON_SECONDARY:
+ if (titlebar_action (self, event, button))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
+ break;
+
+ case GDK_BUTTON_MIDDLE:
+ if (titlebar_action (self, event, button))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void
+hdy_window_handle_controller_finalize (GObject *object)
+{
+ HdyWindowHandleController *self = (HdyWindowHandleController *)object;
+
+ self->widget = NULL;
+ g_clear_object (&self->multipress_gesture);
+ g_clear_object (&self->fallback_menu);
+
+ G_OBJECT_CLASS (hdy_window_handle_controller_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_handle_controller_class_init (HdyWindowHandleControllerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_window_handle_controller_finalize;
+}
+
+static void
+hdy_window_handle_controller_init (HdyWindowHandleController *self)
+{
+}
+
+/**
+ * hdy_window_handle_controller_new:
+ * @widget: The widget to create a controller for
+ *
+ * Creates a new #HdyWindowHandleController for @widget.
+ *
+ * Returns: (transfer full): a newly created #HdyWindowHandleController
+ *
+ * Since: 1.0
+ */
+HdyWindowHandleController *
+hdy_window_handle_controller_new (GtkWidget *widget)
+{
+ HdyWindowHandleController *self;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ self = g_object_new (HDY_TYPE_WINDOW_HANDLE_CONTROLLER, NULL);
+
+ /* The object is intended to have the same life cycle as the widget,
+ * so we don't ref it. */
+ self->widget = widget;
+ self->multipress_gesture = g_object_new (GTK_TYPE_GESTURE_MULTI_PRESS,
+ "widget", widget,
+ "button", 0,
+ NULL);
+ g_signal_connect_object (self->multipress_gesture,
+ "pressed",
+ G_CALLBACK (pressed_cb),
+ self,
+ 0);
+
+ gtk_widget_add_events (widget,
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_TOUCH_MASK);
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (widget),
+ "windowhandle");
+
+ return self;
+}
diff --git a/subprojects/libhandy/src/hdy-window-handle.c b/subprojects/libhandy/src/hdy-window-handle.c
new file mode 100644
index 0000000..30cc855
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-window-handle.h"
+#include "hdy-window-handle-controller-private.h"
+
+/**
+ * SECTION:hdy-window-handle
+ * @short_description: A bin that acts like a titlebar.
+ * @Title: HdyWindowHandle
+ * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindow
+ *
+ * HdyWindowHandle is a #GtkBin subclass that can be dragged to move its
+ * #GtkWindow, and handles right click, middle click and double click as
+ * expected from a titlebar. This is particularly useful with #HdyWindow or
+ * #HdyApplicationWindow.
+ *
+ * It isn't necessary to use #HdyWindowHandle if you use #HdyHeaderBar.
+ *
+ * It can be safely nested or used in the actual window titlebar.
+ *
+ * # CSS nodes
+ *
+ * #HdyWindowHandle has a single CSS node with name windowhandle.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowHandle
+{
+ GtkEventBox parent_instance;
+
+ HdyWindowHandleController *controller;
+};
+
+G_DEFINE_TYPE (HdyWindowHandle, hdy_window_handle, GTK_TYPE_EVENT_BOX)
+
+static void
+hdy_window_handle_finalize (GObject *object)
+{
+ HdyWindowHandle *self = (HdyWindowHandle *)object;
+
+ g_clear_object (&self->controller);
+
+ G_OBJECT_CLASS (hdy_window_handle_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_handle_class_init (HdyWindowHandleClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_window_handle_finalize;
+
+ gtk_widget_class_set_css_name (widget_class, "windowhandle");
+}
+
+static void
+hdy_window_handle_init (HdyWindowHandle *self)
+{
+ self->controller = hdy_window_handle_controller_new (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_window_handle_new:
+ *
+ * Creates a new #HdyWindowHandle.
+ *
+ * Returns: (transfer full): a newly created #HdyWindowHandle
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_window_handle_new (void)
+{
+ return g_object_new (HDY_TYPE_WINDOW_HANDLE, NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-window-handle.h b/subprojects/libhandy/src/hdy-window-handle.h
new file mode 100644
index 0000000..d7835f9
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_HANDLE (hdy_window_handle_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyWindowHandle, hdy_window_handle, HDY, WINDOW_HANDLE, GtkEventBox)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_window_handle_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-mixin-private.h b/subprojects/libhandy/src/hdy-window-mixin-private.h
new file mode 100644
index 0000000..27ce713
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-mixin-private.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_MIXIN (hdy_window_mixin_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyWindowMixin, hdy_window_mixin, HDY, WINDOW_MIXIN, GObject)
+
+HdyWindowMixin *hdy_window_mixin_new (GtkWindow *window,
+ GtkWindowClass *klass);
+
+void hdy_window_mixin_add (HdyWindowMixin *self,
+ GtkWidget *widget);
+void hdy_window_mixin_remove (HdyWindowMixin *self,
+ GtkWidget *widget);
+void hdy_window_mixin_forall (HdyWindowMixin *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data);
+
+gboolean hdy_window_mixin_draw (HdyWindowMixin *self,
+ cairo_t *cr);
+void hdy_window_mixin_destroy (HdyWindowMixin *self);
+
+void hdy_window_mixin_buildable_add_child (HdyWindowMixin *self,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-mixin.c b/subprojects/libhandy/src/hdy-window-mixin.c
new file mode 100644
index 0000000..d55536c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-mixin.c
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-cairo-private.h"
+#include "hdy-deck.h"
+#include "hdy-nothing-private.h"
+#include "hdy-window-mixin-private.h"
+
+typedef enum {
+ HDY_CORNER_TOP_LEFT,
+ HDY_CORNER_TOP_RIGHT,
+ HDY_CORNER_BOTTOM_LEFT,
+ HDY_CORNER_BOTTOM_RIGHT,
+ HDY_N_CORNERS,
+} HdyCorner;
+
+/**
+ * PRIVATE:hdy-window-mixin
+ * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow
+ * @title: HdyWindowMixin
+ * @See_also: #HdyApplicationWindow, #HdyWindow
+ * @stability: Private
+ *
+ * The HdyWindowMixin object contains the implementation of the HdyWindow and
+ * HdyApplicationWindow classes, providing a way to make a GtkWindow subclass
+ * that has masked window corners on all sides and no titlebar by default,
+ * allowing for more freedom with how to handle the titlebar for applications.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowMixin
+{
+ GObject parent;
+
+ GtkWindow *window;
+ GtkWindowClass *klass;
+
+ GtkWidget *content;
+ GtkWidget *titlebar;
+ cairo_surface_t *masks[HDY_N_CORNERS];
+ gint last_border_radius;
+
+ GtkStyleContext *decoration_context;
+ GtkStyleContext *overlay_context;
+
+ GtkWidget *child;
+};
+
+G_DEFINE_TYPE (HdyWindowMixin, hdy_window_mixin, G_TYPE_OBJECT)
+
+static GtkStyleContext *
+create_child_context (HdyWindowMixin *self)
+{
+ GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ GtkStyleContext *child = gtk_style_context_new ();
+
+ gtk_style_context_set_parent (child, parent);
+ gtk_style_context_set_screen (child, gtk_style_context_get_screen (parent));
+ gtk_style_context_set_frame_clock (child, gtk_style_context_get_frame_clock (parent));
+
+ g_signal_connect_object (child,
+ "changed",
+ G_CALLBACK (gtk_widget_queue_draw),
+ self->window,
+ G_CONNECT_SWAPPED);
+
+ return child;
+}
+
+static void
+update_child_context (HdyWindowMixin *self,
+ GtkStyleContext *context,
+ const gchar *name)
+{
+ g_autoptr (GtkWidgetPath) path = gtk_widget_path_new ();
+ GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ gint position;
+
+ gtk_widget_path_append_for_widget (path, GTK_WIDGET (self->window));
+ position = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET);
+ gtk_widget_path_iter_set_object_name (path, position, name);
+
+ gtk_style_context_set_path (context, path);
+ gtk_style_context_set_state (context, gtk_style_context_get_state (parent));
+}
+
+static void
+style_changed_cb (HdyWindowMixin *self)
+{
+ update_child_context (self, self->decoration_context, "decoration");
+ update_child_context (self, self->overlay_context, "decoration-overlay");
+}
+
+static gboolean
+window_state_event_cb (HdyWindowMixin *self,
+ GdkEvent *event,
+ GtkWidget *widget)
+{
+ style_changed_cb (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+size_allocate_cb (HdyWindowMixin *self,
+ GtkAllocation *alloc)
+{
+ /* We don't want to allow any other titlebar */
+ if (gtk_window_get_titlebar (self->window) != self->titlebar)
+ g_error ("gtk_window_set_titlebar() is not supported for HdyWindow");
+}
+
+static gboolean
+is_fullscreen (HdyWindowMixin *self)
+{
+ GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self->window));
+
+ return !!(gdk_window_get_state (window) & GDK_WINDOW_STATE_FULLSCREEN);
+}
+
+static gboolean
+supports_client_shadow (HdyWindowMixin *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+
+ /*
+ * GtkWindow adds this when it can't draw proper decorations, e.g. on a
+ * non-composited WM on X11. This is documented, so we can rely on this
+ * instead of copying the (pretty extensive) check.
+ */
+ return !gtk_style_context_has_class (context, "solid-csd");
+}
+
+static void
+max_borders (GtkBorder *one,
+ GtkBorder *two)
+{
+ one->top = MAX (one->top, two->top);
+ one->right = MAX (one->right, two->right);
+ one->bottom = MAX (one->bottom, two->bottom);
+ one->left = MAX (one->left, two->left);
+}
+
+static void
+get_shadow_width (HdyWindowMixin *self,
+ GtkStyleContext *context,
+ GtkBorder *shadow_width)
+{
+ GtkStateFlags state;
+ GtkBorder margin = { 0 };
+ GtkAllocation content_alloc, alloc;
+ GtkWidget *titlebar;
+
+ *shadow_width = margin;
+
+ if (!gtk_window_get_decorated (self->window))
+ return;
+
+ if (gtk_window_is_maximized (self->window) ||
+ is_fullscreen (self))
+ return;
+
+ if (!gtk_widget_is_toplevel (GTK_WIDGET (self->window)))
+ return;
+
+ state = gtk_style_context_get_state (context);
+
+ gtk_style_context_get_margin (context, state, &margin);
+
+ gtk_widget_get_allocation (GTK_WIDGET (self->window), &alloc);
+ gtk_widget_get_allocation (self->content, &content_alloc);
+
+ titlebar = gtk_window_get_titlebar (self->window);
+ if (titlebar && gtk_widget_get_visible (titlebar)) {
+ GtkAllocation titlebar_alloc;
+
+ gtk_widget_get_allocation (titlebar, &titlebar_alloc);
+
+ content_alloc.y = titlebar_alloc.y;
+ content_alloc.height += titlebar_alloc.height;
+ }
+
+ /*
+ * Since we can't get shadow extents the normal way,
+ * we have to compare window and content allocation instead.
+ */
+ shadow_width->left = content_alloc.x - alloc.x;
+ shadow_width->right = alloc.width - content_alloc.width - content_alloc.x;
+ shadow_width->top = content_alloc.y - alloc.y;
+ shadow_width->bottom = alloc.height - content_alloc.height - content_alloc.y;
+
+ max_borders (shadow_width, &margin);
+}
+
+static void
+create_masks (HdyWindowMixin *self,
+ cairo_t *cr,
+ gint border_radius)
+{
+ gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self->window));
+ gdouble radius_correction = 0.5 / scale_factor;
+ gdouble r = border_radius - radius_correction;
+ gint i;
+
+ for (i = 0; i < HDY_N_CORNERS; i++)
+ g_clear_pointer (&self->masks[i], cairo_surface_destroy);
+
+ if (r <= 0)
+ return;
+
+ for (i = 0; i < HDY_N_CORNERS; i++) {
+ g_autoptr (cairo_t) mask_cr = NULL;
+
+ self->masks[i] =
+ cairo_surface_create_similar_image (cairo_get_target (cr),
+ CAIRO_FORMAT_A8,
+ border_radius * scale_factor,
+ border_radius * scale_factor);
+
+ mask_cr = cairo_create (self->masks[i]);
+
+ cairo_scale (mask_cr, scale_factor, scale_factor);
+ cairo_set_source_rgb (mask_cr, 0, 0, 0);
+ cairo_arc (mask_cr,
+ (i % 2 == 0) ? r : radius_correction,
+ (i / 2 == 0) ? r : radius_correction,
+ r,
+ 0, G_PI * 2);
+ cairo_fill (mask_cr);
+ }
+}
+
+void
+hdy_window_mixin_add (HdyWindowMixin *self,
+ GtkWidget *widget)
+{
+ if (GTK_IS_POPOVER (widget))
+ GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window),
+ widget);
+ else {
+ g_return_if_fail (self->child == NULL);
+
+ self->child = widget;
+ gtk_container_add (GTK_CONTAINER (self->content), widget);
+ }
+}
+
+void
+hdy_window_mixin_remove (HdyWindowMixin *self,
+ GtkWidget *widget)
+{
+ GtkWidget *titlebar = gtk_window_get_titlebar (self->window);
+
+ if (widget == self->content ||
+ widget == titlebar ||
+ GTK_IS_POPOVER (widget))
+ GTK_CONTAINER_CLASS (self->klass)->remove (GTK_CONTAINER (self->window),
+ widget);
+ else if (widget == self->child) {
+ self->child = NULL;
+ gtk_container_remove (GTK_CONTAINER (self->content), widget);
+ }
+}
+
+void
+hdy_window_mixin_forall (HdyWindowMixin *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (self->klass)->forall (GTK_CONTAINER (self->window),
+ include_internals,
+ callback,
+ callback_data);
+
+ return;
+ }
+
+ if (self->child)
+ (*callback) (self->child, callback_data);
+}
+
+typedef struct {
+ HdyWindowMixin *self;
+ cairo_t *cr;
+} HdyWindowMixinDrawData;
+
+static void
+draw_popover_cb (GtkWidget *child,
+ HdyWindowMixinDrawData *data)
+{
+ HdyWindowMixin *self = data->self;
+ GdkWindow *window;
+ cairo_t *cr = data->cr;
+
+ if (child == self->content ||
+ child == gtk_window_get_titlebar (self->window) ||
+ !gtk_widget_get_visible (child) ||
+ !gtk_widget_get_child_visible (child))
+ return;
+
+ window = gtk_widget_get_window (child);
+
+ if (gtk_widget_get_has_window (child))
+ window = gdk_window_get_parent (window);
+
+ if (!gtk_cairo_should_draw_window (cr, window))
+ return;
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), child, cr);
+}
+
+static inline void
+mask_corner (HdyWindowMixin *self,
+ cairo_t *cr,
+ gint scale_factor,
+ gint corner,
+ gint x,
+ gint y)
+{
+ cairo_save (cr);
+ cairo_scale (cr, 1.0 / scale_factor, 1.0 / scale_factor);
+ cairo_mask_surface (cr,
+ self->masks[corner],
+ x * scale_factor,
+ y * scale_factor);
+ cairo_restore (cr);
+}
+
+gboolean
+hdy_window_mixin_draw (HdyWindowMixin *self,
+ cairo_t *cr)
+{
+ HdyWindowMixinDrawData data;
+ GtkWidget *widget = GTK_WIDGET (self->window);
+ GdkWindow *window = gtk_widget_get_window (widget);
+
+ if (gtk_cairo_should_draw_window (cr, window)) {
+ GtkStyleContext *context;
+ gboolean should_mask_corners;
+ GdkRectangle clip = { 0 };
+ gint width, height, x, y, w, h, r, scale_factor;
+ GtkWidget *titlebar;
+ g_autoptr (cairo_surface_t) surface = NULL;
+ g_autoptr (cairo_t) surface_cr = NULL;
+ GtkBorder shadow;
+
+ /* Use the parent drawing unless we have a reason to use masking */
+ if (!gtk_window_get_decorated (self->window) ||
+ !supports_client_shadow (self) ||
+ is_fullscreen (self))
+ return GTK_WIDGET_CLASS (self->klass)->draw (GTK_WIDGET (self->window), cr);
+
+ context = gtk_widget_get_style_context (widget);
+
+ get_shadow_width (self, self->decoration_context, &shadow);
+
+ width = gtk_widget_get_allocated_width (widget);
+ height = gtk_widget_get_allocated_height (widget);
+
+ x = shadow.left;
+ y = shadow.top;
+ w = width - shadow.left - shadow.right;
+ h = height - shadow.top - shadow.bottom;
+
+ gtk_style_context_get (context,
+ gtk_style_context_get_state (self->decoration_context),
+ GTK_STYLE_PROPERTY_BORDER_RADIUS, &r,
+ NULL);
+
+ r = CLAMP (r, 0, MIN (w / 2, h / 2));
+
+ if (!gdk_cairo_get_clip_rectangle (cr, &clip)) {
+ clip.x = 0;
+ clip.y = 0;
+ clip.width = w;
+ clip.height = h;
+ }
+
+ gtk_render_background (self->decoration_context, cr, x, y, w, h);
+ gtk_render_frame (self->decoration_context, cr, x, y, w, h);
+
+ cairo_save (cr);
+
+ scale_factor = gtk_widget_get_scale_factor (widget);
+
+ if (r * scale_factor != self->last_border_radius) {
+ create_masks (self, cr, r);
+ self->last_border_radius = r * scale_factor;
+ }
+
+ should_mask_corners = !gtk_window_is_maximized (self->window) &&
+ r > 0 &&
+ ((clip.x < x + r && clip.y < y + r) ||
+ (clip.x < x + r && clip.y + clip.height > y + h - r) ||
+ (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) ||
+ (clip.x + clip.width > x + w - r && clip.y < y + r));
+
+
+ if (should_mask_corners) {
+ surface = gdk_window_create_similar_surface (window,
+ CAIRO_CONTENT_COLOR_ALPHA,
+ MAX (clip.width, 1),
+ MAX (clip.height, 1));
+ surface_cr = cairo_create (surface);
+ cairo_surface_set_device_offset (surface, -clip.x * scale_factor, -clip.y * scale_factor);
+ } else {
+ surface_cr = cairo_reference (cr);
+ }
+
+ if (!gtk_widget_get_app_paintable (widget)) {
+ gtk_render_background (context, surface_cr, x, y, w, h);
+ gtk_render_frame (context, surface_cr, x, y, w, h);
+ }
+
+ titlebar = gtk_window_get_titlebar (self->window);
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), self->content, surface_cr);
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), titlebar, surface_cr);
+
+ gtk_render_background (self->overlay_context, surface_cr, x, y, w, h);
+ gtk_render_frame (self->overlay_context, surface_cr, x, y, w, h);
+
+ if (should_mask_corners) {
+ cairo_set_source_surface (cr, surface, 0, 0);
+
+ cairo_rectangle (cr, x + r, y, w - r * 2, r);
+ cairo_rectangle (cr, x + r, y + h - r, w - r * 2, r);
+ cairo_rectangle (cr, x, y + r, w, h - r * 2);
+ cairo_fill (cr);
+
+ if (clip.x < x + r && clip.y < y + r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_TOP_LEFT, x, y);
+
+ if (clip.x + clip.width > x + w - r && clip.y < y + r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_TOP_RIGHT, x + w - r, y);
+
+ if (clip.x < x + r && clip.y + clip.height > y + h - r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_BOTTOM_LEFT, x, y + h - r);
+
+ if (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_BOTTOM_RIGHT, x + w - r, y + h - r);
+
+ cairo_surface_flush (surface);
+ }
+
+ cairo_restore (cr);
+ }
+
+ data.self = self;
+ data.cr = cr;
+ gtk_container_forall (GTK_CONTAINER (self->window),
+ (GtkCallback) draw_popover_cb,
+ &data);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+void
+hdy_window_mixin_destroy (HdyWindowMixin *self)
+{
+ if (self->titlebar) {
+ hdy_window_mixin_remove (self, self->titlebar);
+ self->titlebar = NULL;
+ }
+
+ if (self->content) {
+ hdy_window_mixin_remove (self, self->content);
+ self->content = NULL;
+ self->child = NULL;
+ }
+
+ GTK_WIDGET_CLASS (self->klass)->destroy (GTK_WIDGET (self->window));
+}
+
+void
+hdy_window_mixin_buildable_add_child (HdyWindowMixin *self,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ GtkBuildable *buildable = GTK_BUILDABLE (self->window);
+
+ if (!type)
+ gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type);
+}
+
+static void
+hdy_window_mixin_finalize (GObject *object)
+{
+ HdyWindowMixin *self = (HdyWindowMixin *)object;
+ gint i;
+
+ for (i = 0; i < HDY_N_CORNERS; i++)
+ g_clear_pointer (&self->masks[i], cairo_surface_destroy);
+ g_clear_object (&self->decoration_context);
+ g_clear_object (&self->overlay_context);
+
+ G_OBJECT_CLASS (hdy_window_mixin_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_mixin_class_init (HdyWindowMixinClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_window_mixin_finalize;
+}
+
+static void
+hdy_window_mixin_init (HdyWindowMixin *self)
+{
+}
+
+HdyWindowMixin *
+hdy_window_mixin_new (GtkWindow *window,
+ GtkWindowClass *klass)
+{
+ HdyWindowMixin *self;
+ GtkStyleContext *context;
+
+ g_return_val_if_fail (GTK_IS_WINDOW (window), NULL);
+ g_return_val_if_fail (GTK_IS_WINDOW_CLASS (klass), NULL);
+ g_return_val_if_fail (GTK_IS_BUILDABLE (window), NULL);
+
+ self = g_object_new (HDY_TYPE_WINDOW_MIXIN, NULL);
+
+ self->window = window;
+ self->klass = klass;
+
+ gtk_widget_add_events (GTK_WIDGET (window), GDK_STRUCTURE_MASK);
+
+ g_signal_connect_object (window,
+ "style-updated",
+ G_CALLBACK (style_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (window,
+ "window-state-event",
+ G_CALLBACK (window_state_event_cb),
+ self,
+ G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+ g_signal_connect_object (window,
+ "size-allocate",
+ G_CALLBACK (size_allocate_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ self->decoration_context = create_child_context (self);
+ self->overlay_context = create_child_context (self);
+
+ style_changed_cb (self);
+
+ self->content = hdy_deck_new ();
+ gtk_widget_set_vexpand (self->content, TRUE);
+ gtk_widget_show (self->content);
+ GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window),
+ self->content);
+
+ self->titlebar = hdy_nothing_new ();
+ gtk_widget_set_no_show_all (self->titlebar, TRUE);
+ gtk_window_set_titlebar (self->window, self->titlebar);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ gtk_style_context_add_class (context, "unified");
+
+ return self;
+}
diff --git a/subprojects/libhandy/src/hdy-window.c b/subprojects/libhandy/src/hdy-window.c
new file mode 100644
index 0000000..1a868d7
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window.c
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-window.h"
+#include "hdy-window-mixin-private.h"
+
+/**
+ * SECTION:hdy-window
+ * @short_description: A freeform window.
+ * @title: HdyWindow
+ * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindowHandle
+ *
+ * The HdyWindow widget is a subclass of #GtkWindow which has no titlebar area
+ * and provides rounded corners on all sides, ensuring they can never be
+ * overlapped by the content. This makes it safe to use headerbars in the
+ * content area as follows:
+ *
+ * |[
+ * <object class="HdyWindow"/>
+ * <child>
+ * <object class="GtkBox">
+ * <property name="visible">True</property>
+ * <property name="orientation">vertical</property>
+ * <child>
+ * <object class="HdyHeaderBar">
+ * <property name="visible">True</property>
+ * <property name="show-close-button">True</property>
+ * </object>
+ * </child>
+ * <child>
+ * ...
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * It's recommended to use #HdyHeaderBar with #HdyWindow, as unlike
+ * #GtkHeaderBar it remains draggable inside the window. Otherwise,
+ * #HdyWindowHandle can be used.
+ *
+ * #HdyWindow allows to easily implement titlebar autohiding by putting the
+ * headerbar inside a #GtkRevealer, and to show titlebar above content by
+ * putting it into a #GtkOverlay instead of #GtkBox.
+ *
+ * if the window has a #GtkGLArea, it may bring a slight performance regression
+ * when the window is not fullscreen, tiled or maximized.
+ *
+ * Using gtk_window_get_titlebar() and gtk_window_set_titlebar() is not
+ * supported and will result in a crash.
+ *
+ * # CSS nodes
+ *
+ * #HdyWindow has a main CSS node with the name window and style classes
+ * .background, .csd and .unified.
+ *
+ * The .solid-csd style class on the main node is used for client-side
+ * decorations without invisible borders.
+ *
+ * #HdyWindow also represents window states with the following
+ * style classes on the main node: .tiled, .maximized, .fullscreen.
+ *
+ * It contains the subnodes decoration for window shadow and/or border,
+ * decoration-overlay for the sheen on top of the window, widget.titlebar, and
+ * deck, which contains the child inside the window.
+ *
+ * Since: 1.0
+ */
+
+typedef struct
+{
+ HdyWindowMixin *mixin;
+} HdyWindowPrivate;
+
+static void hdy_window_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyWindow, hdy_window, GTK_TYPE_WINDOW,
+ G_ADD_PRIVATE (HdyWindow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_window_buildable_init))
+
+#define HDY_GET_WINDOW_MIXIN(obj) (((HdyWindowPrivate *) hdy_window_get_instance_private (HDY_WINDOW (obj)))->mixin)
+
+static void
+hdy_window_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_window_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container),
+ include_internals,
+ callback,
+ callback_data);
+}
+
+static gboolean
+hdy_window_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr);
+}
+
+static void
+hdy_window_destroy (GtkWidget *widget)
+{
+ hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget));
+}
+
+static void
+hdy_window_finalize (GObject *object)
+{
+ HdyWindow *self = (HdyWindow *)object;
+ HdyWindowPrivate *priv = hdy_window_get_instance_private (self);
+
+ g_clear_object (&priv->mixin);
+
+ G_OBJECT_CLASS (hdy_window_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_class_init (HdyWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_window_finalize;
+ widget_class->draw = hdy_window_draw;
+ widget_class->destroy = hdy_window_destroy;
+ container_class->add = hdy_window_add;
+ container_class->remove = hdy_window_remove;
+ container_class->forall = hdy_window_forall;
+}
+
+static void
+hdy_window_init (HdyWindow *self)
+{
+ HdyWindowPrivate *priv = hdy_window_get_instance_private (self);
+
+ priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self),
+ GTK_WINDOW_CLASS (hdy_window_parent_class));
+}
+
+static void
+hdy_window_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable),
+ builder,
+ child,
+ type);
+}
+
+static void
+hdy_window_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_window_buildable_add_child;
+}
+
+/**
+ * hdy_window_new:
+ *
+ * Creates a new #HdyWindow.
+ *
+ * Returns: (transfer full): a newly created #HdyWindow
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_window_new (void)
+{
+ return g_object_new (HDY_TYPE_WINDOW,
+ "type", GTK_WINDOW_TOPLEVEL,
+ NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-window.h b/subprojects/libhandy/src/hdy-window.h
new file mode 100644
index 0000000..51099cf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW (hdy_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyWindow, hdy_window, HDY, WINDOW, GtkWindow)
+
+struct _HdyWindowClass
+{
+ GtkWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/icons/avatar-default-symbolic.svg b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg
new file mode 100644
index 0000000..ec0905d
--- /dev/null
+++ b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <path d="M8 1a3 3 0 100 6 3 3 0 000-6zM6.5 8A4.49 4.49 0 002 12.5V14c0 1 1 1 1 1h10s1 0 1-1v-1.5A4.49 4.49 0 009.5 8z" style="marker:none" color="#bebebe" overflow="visible" fill="#2e3436"/>
+</svg>
diff --git a/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg
new file mode 100644
index 0000000..78ab0be
--- /dev/null
+++ b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <g color="#000" fill="#474747">
+ <path d="M3.707 5.293L2.293 6.707 8 12.414l5.707-5.707-1.414-1.414L8 9.586z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+ <path d="M13 6V5h1v1zM2 6V5h1v1z" style="marker:none" overflow="visible"/>
+ <path d="M2 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1zM12 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1z" style="marker:none" overflow="visible"/>
+ </g>
+</svg>
diff --git a/subprojects/libhandy/src/meson.build b/subprojects/libhandy/src/meson.build
new file mode 100644
index 0000000..11d4100
--- /dev/null
+++ b/subprojects/libhandy/src/meson.build
@@ -0,0 +1,298 @@
+libhandy_header_subdir = package_subdir / package_api_name
+libhandy_header_dir = get_option('includedir') / libhandy_header_subdir
+libhandy_resources = gnome.compile_resources(
+ 'hdy-resources',
+ 'handy.gresources.xml',
+
+ c_name: 'hdy',
+)
+
+hdy_public_enum_headers = [
+ 'hdy-deck.h',
+ 'hdy-header-bar.h',
+ 'hdy-header-group.h',
+ 'hdy-leaflet.h',
+ 'hdy-navigation-direction.h',
+ 'hdy-squeezer.h',
+ 'hdy-view-switcher.h',
+]
+
+hdy_private_enum_headers = [
+ 'hdy-stackable-box-private.h',
+]
+
+version_data = configuration_data()
+version_data.set('HDY_MAJOR_VERSION', handy_version_major)
+version_data.set('HDY_MINOR_VERSION', handy_version_minor)
+version_data.set('HDY_MICRO_VERSION', handy_version_micro)
+version_data.set('HDY_VERSION', meson.project_version())
+
+hdy_version_h = configure_file(
+ input: 'hdy-version.h.in',
+ output: 'hdy-version.h',
+ install_dir: libhandy_header_dir,
+ install: true,
+ configuration: version_data)
+
+libhandy_generated_headers = [
+]
+
+install_headers(['handy.h'],
+ subdir: libhandy_header_subdir)
+
+# Filled out in the subdirs
+libhandy_public_headers = []
+libhandy_public_sources = []
+libhandy_private_sources = []
+
+hdy_public_enums = gnome.mkenums('hdy-enums',
+ h_template: 'hdy-enums.h.in',
+ c_template: 'hdy-enums.c.in',
+ sources: hdy_public_enum_headers,
+ install_header: true,
+ install_dir: libhandy_header_dir,
+)
+
+hdy_private_enums = gnome.mkenums('hdy-enums-private',
+ h_template: 'hdy-enums-private.h.in',
+ c_template: 'hdy-enums-private.c.in',
+ sources: hdy_private_enum_headers,
+ install_header: false,
+)
+
+libhandy_public_sources += [hdy_public_enums[0]]
+libhandy_private_sources += [hdy_private_enums[0]]
+libhandy_generated_headers += [hdy_public_enums[1]]
+
+src_headers = [
+ 'hdy-action-row.h',
+ 'hdy-animation.h',
+ 'hdy-application-window.h',
+ 'hdy-avatar.h',
+ 'hdy-carousel.h',
+ 'hdy-carousel-indicator-dots.h',
+ 'hdy-carousel-indicator-lines.h',
+ 'hdy-clamp.h',
+ 'hdy-combo-row.h',
+ 'hdy-deck.h',
+ 'hdy-deprecation-macros.h',
+ 'hdy-enum-value-object.h',
+ 'hdy-expander-row.h',
+ 'hdy-header-bar.h',
+ 'hdy-header-group.h',
+ 'hdy-keypad.h',
+ 'hdy-leaflet.h',
+ 'hdy-main.h',
+ 'hdy-navigation-direction.h',
+ 'hdy-preferences-group.h',
+ 'hdy-preferences-page.h',
+ 'hdy-preferences-row.h',
+ 'hdy-preferences-window.h',
+ 'hdy-search-bar.h',
+ 'hdy-squeezer.h',
+ 'hdy-swipe-group.h',
+ 'hdy-swipe-tracker.h',
+ 'hdy-swipeable.h',
+ 'hdy-title-bar.h',
+ 'hdy-types.h',
+ 'hdy-value-object.h',
+ 'hdy-view-switcher.h',
+ 'hdy-view-switcher-bar.h',
+ 'hdy-view-switcher-title.h',
+ 'hdy-window.h',
+ 'hdy-window-handle.h',
+]
+
+sed = find_program('sed', required: true)
+gen_public_types = find_program('gen-public-types.sh', required: true)
+
+libhandy_init_public_types = custom_target('hdy-public-types.c',
+ output: 'hdy-public-types.c',
+ input: [src_headers, libhandy_generated_headers],
+ command: [gen_public_types, '@INPUT@'],
+ capture: true,
+)
+
+src_sources = [
+ 'gtkprogresstracker.c',
+ 'gtk-window.c',
+ 'hdy-action-row.c',
+ 'hdy-animation.c',
+ 'hdy-application-window.c',
+ 'hdy-avatar.c',
+ 'hdy-carousel.c',
+ 'hdy-carousel-box.c',
+ 'hdy-carousel-indicator-dots.c',
+ 'hdy-carousel-indicator-lines.c',
+ 'hdy-clamp.c',
+ 'hdy-combo-row.c',
+ 'hdy-css.c',
+ 'hdy-deck.c',
+ 'hdy-enum-value-object.c',
+ 'hdy-expander-row.c',
+ 'hdy-header-bar.c',
+ 'hdy-header-group.c',
+ 'hdy-keypad-button.c',
+ 'hdy-keypad.c',
+ 'hdy-leaflet.c',
+ 'hdy-main.c',
+ 'hdy-navigation-direction.c',
+ 'hdy-nothing.c',
+ 'hdy-preferences-group.c',
+ 'hdy-preferences-page.c',
+ 'hdy-preferences-row.c',
+ 'hdy-preferences-window.c',
+ 'hdy-search-bar.c',
+ 'hdy-shadow-helper.c',
+ 'hdy-squeezer.c',
+ 'hdy-stackable-box.c',
+ 'hdy-swipe-group.c',
+ 'hdy-swipe-tracker.c',
+ 'hdy-swipeable.c',
+ 'hdy-title-bar.c',
+ 'hdy-value-object.c',
+ 'hdy-view-switcher.c',
+ 'hdy-view-switcher-bar.c',
+ 'hdy-view-switcher-button.c',
+ 'hdy-view-switcher-title.c',
+ 'hdy-window.c',
+ 'hdy-window-handle.c',
+ 'hdy-window-handle-controller.c',
+ 'hdy-window-mixin.c',
+]
+
+libhandy_public_headers += files(src_headers)
+libhandy_public_sources += files(src_sources)
+
+install_headers(src_headers, subdir: libhandy_header_subdir)
+
+
+libhandy_sources = [
+ libhandy_generated_headers,
+ libhandy_public_sources,
+ libhandy_private_sources,
+ libhandy_resources,
+ libhandy_init_public_types,
+]
+
+glib_min_version = '>= 2.44'
+
+libhandy_deps = [
+ dependency('glib-2.0', version: glib_min_version),
+ dependency('gio-2.0', version: glib_min_version),
+ dependency('gmodule-2.0', version: glib_min_version),
+ dependency('gtk+-3.0', version: '>= 3.24.1'),
+ cc.find_library('m', required: false),
+ cc.find_library('rt', required: false),
+]
+
+libhandy_c_args = [
+ '-DG_LOG_DOMAIN="Handy"',
+]
+
+config_h = configuration_data()
+config_h.set_quoted('GETTEXT_PACKAGE', 'libhandy')
+config_h.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir'))
+
+# Symbol visibility
+if target_system == 'windows'
+ config_h.set('DLL_EXPORT', true)
+ config_h.set('_HDY_EXTERN', '__declspec(dllexport) extern')
+ if cc.get_id() != 'msvc'
+ libhandy_c_args += ['-fvisibility=hidden']
+ endif
+else
+ config_h.set('_HDY_EXTERN', '__attribute__((visibility("default"))) extern')
+ libhandy_c_args += ['-fvisibility=hidden']
+endif
+
+configure_file(
+ output: 'config.h',
+ configuration: config_h,
+)
+
+libhandy_link_args = []
+libhandy_symbols_file = 'libhandy.syms'
+
+# Check linker flags
+ld_version_script_arg = '-Wl,--version-script,@0@/@1@'.format(meson.source_root(),
+ libhandy_symbols_file)
+if cc.links('int main() { return 0; }', args : ld_version_script_arg, name : 'ld_supports_version_script')
+ libhandy_link_args += [ld_version_script_arg]
+endif
+
+# set default libdir on win32 for libhandy target to keep MinGW compatibility
+if target_system == 'windows'
+ handy_libdir = [true]
+else
+ handy_libdir = libdir
+endif
+
+libhandy = shared_library(
+ 'handy-' + apiversion,
+ libhandy_sources,
+
+ soversion: soversion,
+ c_args: libhandy_c_args,
+ dependencies: libhandy_deps,
+ include_directories: [ root_inc, src_inc ],
+ install: true,
+ link_args: libhandy_link_args,
+ install_dir: handy_libdir,
+)
+
+libhandy_dep = declare_dependency(
+ sources: libhandy_generated_headers,
+ dependencies: libhandy_deps,
+ link_with: libhandy,
+ include_directories: include_directories('.'),
+)
+
+if introspection
+
+ libhandy_gir_extra_args = [
+ '--c-include=handy.h',
+ '--quiet',
+ '-DHANDY_COMPILATION',
+ ]
+
+ libhandy_gir = gnome.generate_gir(libhandy,
+ sources: libhandy_generated_headers + libhandy_public_headers + libhandy_public_sources,
+ nsversion: apiversion,
+ namespace: 'Handy',
+ export_packages: package_api_name,
+ symbol_prefix: 'hdy',
+ identifier_prefix: 'Hdy',
+ link_with: libhandy,
+ includes: ['Gio-2.0', 'Gtk-3.0'],
+ install: true,
+ install_dir_gir: girdir,
+ install_dir_typelib: typelibdir,
+ extra_args: libhandy_gir_extra_args,
+ )
+
+ if get_option('vapi')
+
+ libhandy_vapi = gnome.generate_vapi(package_api_name,
+ sources: libhandy_gir[0],
+ packages: [ 'gio-2.0', 'gtk+-3.0' ],
+ install: true,
+ install_dir: vapidir,
+ metadata_dirs: [ meson.current_source_dir() ],
+ )
+
+ endif
+endif
+
+pkgg = import('pkgconfig')
+
+pkgg.generate(
+ libraries: [libhandy],
+ subdirs: libhandy_header_subdir,
+ version: meson.project_version(),
+ name: 'Handy',
+ filebase: package_api_name,
+ description: 'Handy Mobile widgets',
+ requires: 'gtk+-3.0',
+ install_dir: libdir / 'pkgconfig',
+)
diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.css b/subprojects/libhandy/src/themes/Adwaita-dark.css
new file mode 100644
index 0000000..e553fac
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita-dark.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#353535, 0.5); border-color: alpha(#1b1b1b, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #15539e; }
+
+row.expander image.expander-row-arrow:disabled { color: #919190; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.2); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.05); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#1b1b1b, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#353535)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#1b1b1b, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#1b1b1b, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#353535, 0.7), 0.99) 2px, alpha(#353535, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #1b1b1b; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #1b1b1b; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#1b1b1b, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.scss b/subprojects/libhandy/src/themes/Adwaita-dark.scss
new file mode 100644
index 0000000..918f489
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita-dark.scss
@@ -0,0 +1,5 @@
+$variant: 'dark';
+$high_contrast: false;
+
+@import 'colors';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/Adwaita.css b/subprojects/libhandy/src/themes/Adwaita.css
new file mode 100644
index 0000000..acb7f27
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; }
+
+row.expander image.expander-row-arrow:disabled { color: #929595; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#cdc7c2, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#f6f5f4)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#cdc7c2, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#cdc7c2, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#f6f5f4, 0.7), 0.96) 2px, alpha(#f6f5f4, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #cdc7c2; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #cdc7c2; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#cdc7c2, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/Adwaita.scss b/subprojects/libhandy/src/themes/Adwaita.scss
new file mode 100644
index 0000000..5ded9f6
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/HighContrast.css b/subprojects/libhandy/src/themes/HighContrast.css
new file mode 100644
index 0000000..f1d1eda
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrast.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#fdfdfc, 0.5); border-color: alpha(#877b6e, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #1b6acb; }
+
+row.expander image.expander-row-arrow:disabled { color: #929495; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #877b6e; }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#877b6e, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#fdfdfc)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#877b6e, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#877b6e, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#fdfdfc, 0.7), 0.96) 2px, alpha(#fdfdfc, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #877b6e; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #877b6e; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#877b6e, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/HighContrast.scss b/subprojects/libhandy/src/themes/HighContrast.scss
new file mode 100644
index 0000000..4456428
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrast.scss
@@ -0,0 +1,6 @@
+$variant: 'light';
+$high_contrast: true;
+
+@import 'colors';
+@import 'colors-hc';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.css b/subprojects/libhandy/src/themes/HighContrastInverse.css
new file mode 100644
index 0000000..fd5b01b
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrastInverse.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#303030, 0.5); border-color: alpha(#686868, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #0f3b71; }
+
+row.expander image.expander-row-arrow:disabled { color: #919191; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #686868; }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#686868, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#303030)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#686868, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#686868, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#303030, 0.7), 0.99) 2px, alpha(#303030, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #686868; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #686868; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#686868, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.scss b/subprojects/libhandy/src/themes/HighContrastInverse.scss
new file mode 100644
index 0000000..a49c0e1
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrastInverse.scss
@@ -0,0 +1,6 @@
+$variant: 'dark';
+$high_contrast: true;
+
+@import 'colors';
+@import 'colors-hc';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/_Adwaita-base.scss b/subprojects/libhandy/src/themes/_Adwaita-base.scss
new file mode 100644
index 0000000..cc0b754
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_Adwaita-base.scss
@@ -0,0 +1,336 @@
+// Include base styling.
+@import 'fallback-base';
+@import 'shared-base';
+
+// HdyComboRow
+
+popover.combo {
+ padding: 0px;
+
+ list {
+ border-style: none;
+ background-color: transparent;
+
+ > row {
+ padding: 0px 12px 0px 12px;
+ min-height: 50px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid hdyalpha($borders_color, 0.5)
+ }
+
+ &:first-child {
+ @include rounded-border(top);
+ }
+
+ &:last-child {
+ @include rounded-border(bottom);
+ }
+ }
+ }
+
+ @each $border in top, bottom {
+ overshoot.#{$border} {
+ @include rounded-border($border);
+ }
+ }
+
+ scrollbar.vertical {
+ padding-top: 2px;
+ padding-bottom: 2px;
+
+ &:dir(ltr) {
+ @include rounded-border(right);
+ }
+
+ &:dir(rtl) {
+ @include rounded-border(left);
+ }
+ }
+}
+
+// HdyExpanderRow
+
+row.expander {
+ padding: 0px;
+
+ image.expander-row-arrow {
+ @include margin-start(6px);
+ }
+}
+
+// HdyKeypad
+
+keypad {
+ .digit {
+ font-size: 200%;
+ font-weight: bold;
+ }
+
+ .letters {
+ font-size: 70%;
+ }
+
+ .symbol {
+ font-size: 160%;
+ }
+}
+
+// HdyViewSwitcher
+
+viewswitcher {
+ &, & button {
+ margin: 0;
+ padding: 0;
+ }
+
+ button {
+ border-radius: 0;
+ border-top: 0;
+ border-bottom: 0;
+ box-shadow: none;
+ font-size: 1rem;
+
+ &:not(:checked):not(:hover) {
+ background: transparent;
+ }
+
+ &:not(:only-child):not(:last-child) {
+ border-right-width: 0px;
+ }
+
+ &:not(only-child):first-child:not(:checked):not(:hover),
+ &:not(:checked):not(:hover) + button:not(:checked):not(:hover) {
+ border-left-color: transparent;
+ }
+
+ &:not(only-child):last-child:not(:checked):not(:hover) {
+ border-right-color: transparent;
+ }
+
+ &:not(:checked):hover:not(:backdrop) {
+ background-image: image(lighter($bg_color));
+ }
+
+ &:not(only-child):first-child:not(:checked):hover,
+ &:not(:checked):hover + button:not(:checked):not(:hover),
+ &:not(:checked):not(:hover) + button:not(:checked):hover {
+ border-left-color: shade($borders_color, 1.15);
+ }
+
+ &:not(only-child):last-child:not(:checked):hover {
+ border-right-color: shade($borders_color, 1.15);
+ }
+
+ &:not(:checked):hover:backdrop {
+ background-image: image($bg_color);
+ }
+
+ // View switcher in a header bar
+ headerbar &:not(:checked) {
+ &:hover:not(:backdrop) {
+ // Reimplementation of $button_fill from Adwaita. The colors are made
+ // only 70% visible to avoid the highlight to be too strong.
+ $c: hdyalpha($bg_color, 0.7);
+ $button_fill: if($variant == 'light', linear-gradient(to top, shade($c, 0.96) 2px, $c),
+ linear-gradient(to top, shade($c, 0.99) 2px, $c)) !global;
+ background-image: $button_fill;
+ }
+
+ &:not(only-child):first-child:hover,
+ &:hover + button:not(:checked):not(:hover),
+ &:not(:hover) + button:not(:checked):hover {
+ border-left-color: $borders_color;
+ }
+
+ &:not(only-child):last-child:hover {
+ border-right-color: $borders_color;
+ }
+
+ &:hover:backdrop {
+ background-image: image($bg_color);
+ }
+ }
+
+ // View switcher button
+ > stack > box {
+ &.narrow {
+ font-size: 0.75rem;
+ padding-top: 7px;
+ padding-bottom: 5px;
+
+ image,
+ label {
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+ }
+
+ &.wide {
+ padding: 8px 12px;
+
+ label {
+ &:dir(ltr) {
+ padding-right: 7px;
+ }
+
+ &:dir(rtl) {
+ padding-left: 7px;
+ }
+ }
+ }
+
+ label.active {
+ font-weight: bold;
+ }
+ }
+
+ &.needs-attention {
+ &:active > stack > box label,
+ &:checked > stack > box label {
+ animation: none;
+ background-image: none;
+ }
+
+ > stack > box label {
+ animation: needs_attention 150ms ease-in;
+ background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent));
+ background-size: 6px 6px, 6px 6px;
+ background-repeat: no-repeat;
+ background-position: right 0px, right 1px;
+
+ &:backdrop {
+ background-size: 6px 6px, 0 0;
+ }
+
+ &:dir(rtl) {
+ background-position: left 0px, left 1px;
+ }
+ }
+ }
+ }
+}
+
+// HdyViewSwitcherBar
+
+viewswitcherbar actionbar > revealer > box {
+ padding: 0;
+}
+
+// Content list
+
+list.content {
+ &,
+ list {
+ background-color: transparent;
+ }
+
+ // Nested rows background
+ list.nested > row:not(:active) {
+ &:not(:hover):not(:selected),
+ &:hover:not(.activatable):not(:selected) {
+ background-color: hdymix($bg_color, $base_color, 0.5);
+ }
+
+ &:hover.activatable:not(:selected) {
+ background-color: hdymix($fg_color, $base_color, 0.95);
+ }
+ }
+
+ > row {
+ // Regular rows and expander header rows background
+ &:not(.expander):not(:active):not(:hover):not(:selected),
+ &:not(.expander):not(:active):hover:not(.activatable):not(:selected),
+ &.expander row.header:not(:active):not(:hover):not(:selected),
+ &.expander row.header:not(:active):hover:not(.activatable):not(:selected) {
+ background-color: $base_color;
+ }
+
+ &:not(.expander):not(:active):hover.activatable:not(:selected),
+ &.expander row.header:not(:active):hover.activatable:not(:selected) {
+ background-color: hdymix($fg_color, $base_color, 0.95);
+ }
+
+ &,
+ list > row {
+ border-color: hdyalpha($borders_color, 0.7);
+ border-style: solid;
+ transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ // Top border
+ &:not(:last-child) {
+ border-width: 1px 1px 0px 1px;
+ }
+
+ // Rounded top
+ &:first-child,
+ &.expander:first-child row.header,
+ &.expander:checked,
+ &.expander:checked row.header,
+ &.expander:checked + row,
+ &.expander:checked + row.expander row.header {
+ @include rounded-border(top);
+ }
+
+ // Bottom border
+ &:last-child,
+ &.checked-expander-row-previous-sibling,
+ &.expander:checked {
+ border-width: 1px;
+ }
+
+ // Rounded bottom
+ &:last-child,
+ &.checked-expander-row-previous-sibling,
+ &.expander:checked,
+ &.expander:not(:checked):last-child row.header,
+ &.expander:not(:checked).checked-expander-row-previous-sibling row.header,
+ &.expander.empty:checked row.header,
+ &.expander list.nested > row:last-child {
+ @include rounded-border(bottom);
+ }
+
+ // Add space around expanded rows
+ &.expander:checked:not(:first-child),
+ &.expander:checked + row {
+ margin-top: 6px;
+ }
+ }
+}
+
+// List button
+
+button.list-button:not(:active):not(:checked):not(:hover) {
+ background: none;
+ border: 1px solid hdyalpha($borders_color, 0.5);
+ box-shadow: none;
+}
+
+// Unified window
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) {
+ // Remove the sheen on headerbar...
+ headerbar {
+ box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.7, 0));
+
+ &.selection-mode {
+ box-shadow: none;
+ }
+ }
+
+ // ...and add it on the window itself
+ > decoration-overlay {
+ // Use a white sheen instead of @borders, as it has to be neutral enough
+ // for any content and not just headerbar background
+ box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.34, 0.065));
+ }
+
+ &:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) {
+ &,
+ > decoration,
+ > decoration-overlay {
+ border-radius: 8px;
+ }
+ }
+}
diff --git a/subprojects/libhandy/src/themes/_definitions.scss b/subprojects/libhandy/src/themes/_definitions.scss
new file mode 100644
index 0000000..ed427a4
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_definitions.scss
@@ -0,0 +1,66 @@
+@import 'drawing';
+
+@function hdyalpha($c, $a) {
+ @return unquote("alpha(#{$c}, #{$a})");
+}
+
+@function hdymix($c1, $c2, $r) {
+ @return unquote("mix(#{$c1}, #{$c2}, #{$r})");
+}
+
+$leaflet_dimming: rgba(0, 0, 0, if($variant == 'light', 0.12, 0.24));
+$leaflet_border: rgba(0, 0, 0, if($variant == 'light', 0.05, 0.2));
+$leaflet_outline: rgba(255, 255, 255, if($variant == 'light', 0.2, 0.05));
+
+@if $high_contrast {
+ $leaflet_border: $borders_color;
+ $leaflet_outline: transparent;
+}
+
+@mixin background-shadow($direction) {
+ background-image:
+ linear-gradient($direction,
+ rgba(0, 0, 0, if($variant == 'light', 0.05, 0.1)),
+ rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 40px,
+ rgba(0, 0, 0, 0) 56px),
+ linear-gradient($direction,
+ rgba(0, 0, 0, if($variant == 'light', 0.03, 0.06)),
+ rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 7px,
+ rgba(0, 0, 0, 0) 24px);
+}
+
+// Makes the corners of the given border rounded.
+// $border must be top, bottom, left, or right.
+@mixin rounded-border($border) {
+ // The floors (top, bottom) and walls (left, right) of the corners matching
+ // $border. This is needed to easily form floor-wall pairs regardless of
+ // whether $border is a floor or a wall.
+ $corners: (
+ 'top': (('top'), ('left', 'right')),
+ 'bottom': (('bottom'), ('left', 'right')),
+ 'left': (('top', 'bottom'), ('left')),
+ 'right': (('top', 'bottom'), ('right')),
+ );
+
+ @if not map-get($corners, $border) {
+ @error "Unknown border type: #{$border}";
+ }
+
+ // Loop through the floors and walls of the corners of $border.
+ @each $floor in nth(map-get($corners, $border), 1) {
+ @each $wall in nth(map-get($corners, $border), 2) {
+ border-#{$floor}-#{$wall}-radius: 8px;
+ -gtk-outline-#{$floor}-#{$wall}-radius: 7px;
+ }
+ }
+}
+
+@mixin margin-start($margin) {
+ &:dir(ltr) {
+ margin-left: $margin;
+ }
+
+ &:dir(rtl) {
+ margin-right: $margin;
+ }
+}
diff --git a/subprojects/libhandy/src/themes/_fallback-base.scss b/subprojects/libhandy/src/themes/_fallback-base.scss
new file mode 100644
index 0000000..b821d95
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_fallback-base.scss
@@ -0,0 +1,146 @@
+@import 'definitions';
+
+// HdyActionRow
+
+row {
+ label.subtitle {
+ font-size: smaller;
+ opacity: 0.55;
+ text-shadow: none;
+ }
+
+ > box.header {
+ margin-left: 12px;
+ margin-right: 12px;
+ min-height: 50px;
+
+ > box.title {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+ }
+}
+
+// HdyExpanderRow
+
+row.expander {
+ // Drop transparent background on expander rows to let nested rows handle it,
+ // avoiding double highlights.
+ background-color: transparent;
+
+ list.nested > row {
+ background-color: hdyalpha($bg_color, 0.5);
+ border-color: hdyalpha($borders_color, 0.7);
+ border-style: solid;
+ border-width: 1px 0px 0px 0px;
+ }
+
+ // HdyExpanderRow arrow rotation
+
+ image.expander-row-arrow {
+ transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ &:checked image.expander-row-arrow {
+ -gtk-icon-transform: rotate(0turn);
+ }
+
+ &:not(:checked) image.expander-row-arrow {
+ opacity: 0.55;
+ text-shadow: none;
+
+ &:dir(ltr) {
+ -gtk-icon-transform: rotate(-0.25turn);
+ }
+
+ &:dir(rtl) {
+ -gtk-icon-transform: rotate(0.25turn);
+ }
+ }
+
+ &:checked image.expander-row-arrow:not(:disabled) {
+ color: $selected_bg_color;
+ }
+
+ & image.expander-row-arrow:disabled {
+ color: $insensitive_fg_color;
+ }
+}
+
+// Shadows
+
+deck,
+leaflet {
+ > dimming {
+ background: $leaflet_dimming;
+ }
+
+ > border {
+ min-width: 1px;
+ min-height: 1px;
+ background: $leaflet_border;
+ }
+
+ > shadow {
+ min-width: 56px;
+ min-height: 56px;
+
+ &.left { @include background-shadow(to right); }
+ &.right { @include background-shadow(to left); }
+ &.up { @include background-shadow(to bottom); }
+ &.down { @include background-shadow(to top); }
+ }
+
+ > outline {
+ min-width: 1px;
+ min-height: 1px;
+ background: $leaflet_outline;
+ }
+}
+
+// Avatar
+
+avatar {
+ border-radius: 9999px;
+ -gtk-outline-radius: 9999px;
+ font-weight: bold;
+
+ // The list of colors to generate avatars.
+ // Each avatar color is represented by a font color, a gradient start color and a gradient stop color.
+ // There are 8 different colors for avtars in the list if you change the number of them you
+ // need to update the NUMBER_OF_COLORS in src/hdy-avatar.c.
+ // The 2D list has this form: ((font-color, gradient-top-color, gradient-bottom-color)).
+ $avatarcolorlist: (
+ (#cfe1f5, #83b6ec, #337fdc), // blue
+ (#caeaf2, #7ad9f1, #0f9ac8), // cyan
+ (#cef8d8, #8de6b1, #29ae74), // green
+ (#e6f9d7, #b5e98a, #6ab85b), // lime
+ (#f9f4e1, #f8e359, #d29d09), // yellow
+ (#ffead1, #ffcb62, #d68400), // gold
+ (#ffe5c5, #ffa95a, #ed5b00), // orange
+ (#f8d2ce, #f78773, #e62d42), // raspberry
+ (#fac7de, #e973ab, #e33b6a), // magenta
+ (#e7c2e8, #cb78d4, #9945b5), // purple
+ (#d5d2f5, #9e91e8, #7a59ca), // violet
+ (#f2eade, #e3cf9c, #b08952), // beige
+ (#e5d6ca, #be916d, #785336), // brown
+ (#d8d7d3, #c0bfbc, #6e6d71), // gray
+ );
+
+ @for $i from 1 through length($avatarcolorlist) {
+ &.color#{$i} {
+ $avatarcolor: nth($avatarcolorlist, $i);
+ background-image: linear-gradient(nth($avatarcolor, 2), nth($avatarcolor, 3));
+ color: nth($avatarcolor, 1);
+ }
+ }
+
+ &.contrasted { color: #fff; }
+}
+
+// HdyViewSwitcherTitle
+
+viewswitchertitle viewswitcher {
+ margin-left: 12px;
+ margin-right: 12px;
+}
diff --git a/subprojects/libhandy/src/themes/_shared-base.scss b/subprojects/libhandy/src/themes/_shared-base.scss
new file mode 100644
index 0000000..934eeda
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_shared-base.scss
@@ -0,0 +1,21 @@
+@import 'definitions';
+
+// HdyComboRow
+
+popover.combo list {
+ min-width: 200px;
+}
+
+window.csd.unified:not(.solid-csd) {
+ // Since corners are masked, there's no need for round corners anymore
+ &, headerbar {
+ border-radius: 0;
+ }
+}
+
+.windowhandle {
+ &, & * {
+ // This is the most reliable way to enable window dragging
+ -GtkWidget-window-dragging: true;
+ }
+}
diff --git a/subprojects/libhandy/src/themes/fallback.css b/subprojects/libhandy/src/themes/fallback.css
new file mode 100644
index 0000000..8c1d89b
--- /dev/null
+++ b/subprojects/libhandy/src/themes/fallback.css
@@ -0,0 +1,74 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; }
+
+row.expander image.expander-row-arrow:disabled { color: #929595; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
diff --git a/subprojects/libhandy/src/themes/fallback.scss b/subprojects/libhandy/src/themes/fallback.scss
new file mode 100644
index 0000000..d8a0985
--- /dev/null
+++ b/subprojects/libhandy/src/themes/fallback.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'fallback-base';
diff --git a/subprojects/libhandy/src/themes/parse-sass.sh b/subprojects/libhandy/src/themes/parse-sass.sh
new file mode 100755
index 0000000..4238e88
--- /dev/null
+++ b/subprojects/libhandy/src/themes/parse-sass.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+if [ ! "$(which sassc 2> /dev/null)" ]; then
+ echo sassc needs to be installed to generate the css.
+ exit 1
+fi
+
+if [ ! "$(which git 2> /dev/null)" ]; then
+ echo git needs to be installed to check GTK.
+ exit 1
+fi
+
+SASSC_OPT="-M -t compact"
+
+: ${GTK_SOURCE_PATH:="../../../gtk"}
+: ${GTK_TAG:="3.24.21"}
+
+if [ ! -d "${GTK_SOURCE_PATH}/gtk/theme/Adwaita" ]; then
+ echo GTK sources not found at ${GTK_SOURCE_PATH}.
+ exit 1
+fi
+
+# > /dev/null makes pushd and popd silent.
+pushd ${GTK_SOURCE_PATH} > /dev/null
+GTK_CURRENT_TAG=`git describe --tags`
+popd > /dev/null
+
+if [ "${GTK_CURRENT_TAG}" != "${GTK_TAG}" ]; then
+ echo GTK must be at tag ${GTK_TAG}.
+ exit 1
+fi
+
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ Adwaita.scss Adwaita.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ Adwaita-dark.scss Adwaita-dark.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ fallback.scss fallback.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \
+ HighContrast.scss HighContrast.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \
+ HighContrastInverse.scss HighContrastInverse.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ shared.scss shared.css
diff --git a/subprojects/libhandy/src/themes/shared.css b/subprojects/libhandy/src/themes/shared.css
new file mode 100644
index 0000000..6bfd522
--- /dev/null
+++ b/subprojects/libhandy/src/themes/shared.css
@@ -0,0 +1,6 @@
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
diff --git a/subprojects/libhandy/src/themes/shared.scss b/subprojects/libhandy/src/themes/shared.scss
new file mode 100644
index 0000000..86f64b0
--- /dev/null
+++ b/subprojects/libhandy/src/themes/shared.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'shared-base';