1 /*
2  * Copyright (C) 2013 Red Hat, Inc.
3  * Copyright (C) 2019 Purism SPC
4  *
5  * Author: Alexander Larsson <alexl@redhat.com>
6  * Author: Adrien Plazas <adrien.plazas@puri.sm>
7  *
8  * SPDX-License-Identifier: LGPL-2.1-or-later
9  */
10 
11 /*
12  * Forked from the GTK+ 3.24.2 GtkStack widget initially written by Alexander
13  * Larsson, and heavily modified for libadwaita by Adrien Plazas on behalf of
14  * Purism SPC 2019.
15  */
16 
17 #include "config.h"
18 
19 #include "adw-squeezer.h"
20 
21 #include "gtkprogresstrackerprivate.h"
22 #include "adw-animation-util-private.h"
23 #include "adw-animation-private.h"
24 #include "adw-widget-utils-private.h"
25 
26 /**
27  * AdwSqueezer:
28  *
29  * A best fit container.
30  *
31  * The `AdwSqueezer` widget is a container which only shows the first of its
32  * children that fits in the available size. It is convenient to offer different
33  * widgets to represent the same data with different levels of detail, making
34  * the widget seem to squeeze itself to fit in the available space.
35  *
36  * Transitions between children can be animated as fades. This can be controlled
37  * with [property@Adw.Squeezer:transition-type].
38  *
39  * ## CSS nodes
40  *
41  * `AdwSqueezer` has a single CSS node with name `squeezer`.
42  *
43  * Since: 1.0
44  */
45 
46 /**
47  * AdwSqueezerPage:
48  *
49  * An auxiliary class used by [class@Adw.Squeezer].
50  */
51 
52 /**
53  * AdwSqueezerTransitionType:
54  * @ADW_SQUEEZER_TRANSITION_TYPE_NONE: No transition
55  * @ADW_SQUEEZER_TRANSITION_TYPE_CROSSFADE: A cross-fade
56  *
57  * Describes the possible transitions in a [class@Adw.Squeezer] widget.
58  *
59  * Since: 1.0
60  */
61 
62 struct _AdwSqueezerPage {
63   GObject parent_instance;
64 
65   GtkWidget *widget;
66   GtkWidget *last_focus;
67   gboolean enabled;
68 };
69 
70 G_DEFINE_TYPE (AdwSqueezerPage, adw_squeezer_page, G_TYPE_OBJECT)
71 
72 enum {
73   PAGE_PROP_0,
74   PAGE_PROP_CHILD,
75   PAGE_PROP_ENABLED,
76   LAST_PAGE_PROP
77 };
78 
79 static GParamSpec *page_props[LAST_PAGE_PROP];
80 
81 struct _AdwSqueezer
82 {
83   GtkWidget parent_instance;
84 
85   GList *children;
86 
87   AdwSqueezerPage *visible_child;
88 
89   gboolean homogeneous;
90 
91   gboolean allow_none;
92 
93   AdwSqueezerTransitionType transition_type;
94   guint transition_duration;
95 
96   AdwSqueezerPage *last_visible_child;
97   guint tick_id;
98   GtkProgressTracker tracker;
99   gboolean first_frame_skipped;
100 
101   int last_visible_widget_width;
102   int last_visible_widget_height;
103 
104   AdwSqueezerTransitionType active_transition_type;
105 
106   gboolean interpolate_size;
107 
108   float xalign;
109   float yalign;
110 
111   GtkOrientation orientation;
112 
113   GtkSelectionModel *pages;
114 };
115 
116 enum  {
117   PROP_0,
118   PROP_HOMOGENEOUS,
119   PROP_VISIBLE_CHILD,
120   PROP_ALLOW_NONE,
121   PROP_TRANSITION_DURATION,
122   PROP_TRANSITION_TYPE,
123   PROP_TRANSITION_RUNNING,
124   PROP_INTERPOLATE_SIZE,
125   PROP_XALIGN,
126   PROP_YALIGN,
127   PROP_PAGES,
128 
129   /* Overridden properties */
130   PROP_ORIENTATION,
131 
132   LAST_PROP = PROP_PAGES + 1,
133 };
134 
135 static GParamSpec *props[LAST_PROP];
136 
137 static void adw_squeezer_buildable_init (GtkBuildableIface *iface);
138 
139 G_DEFINE_TYPE_WITH_CODE (AdwSqueezer, adw_squeezer, GTK_TYPE_WIDGET,
140                          G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
141                          G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_squeezer_buildable_init))
142 
143 static GtkBuildableIface *parent_buildable_iface;
144 
145 static void
adw_squeezer_page_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)146 adw_squeezer_page_get_property (GObject    *object,
147                                 guint       property_id,
148                                 GValue     *value,
149                                 GParamSpec *pspec)
150 {
151   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
152 
153   switch (property_id) {
154   case PAGE_PROP_CHILD:
155     g_value_set_object (value, adw_squeezer_page_get_child (self));
156     break;
157   case PAGE_PROP_ENABLED:
158     g_value_set_boolean (value, adw_squeezer_page_get_enabled (self));
159     break;
160   default:
161     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
162     break;
163   }
164 }
165 
166 static void
adw_squeezer_page_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)167 adw_squeezer_page_set_property (GObject      *object,
168                                 guint         property_id,
169                                 const GValue *value,
170                                 GParamSpec   *pspec)
171 {
172   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
173 
174   switch (property_id) {
175   case PAGE_PROP_CHILD:
176     g_set_object (&self->widget, g_value_get_object (value));
177     break;
178   case PAGE_PROP_ENABLED:
179     adw_squeezer_page_set_enabled (self, g_value_get_boolean (value));
180     break;
181   default:
182     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
183     break;
184   }
185 }
186 
187 static void
adw_squeezer_page_finalize(GObject * object)188 adw_squeezer_page_finalize (GObject *object)
189 {
190   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
191 
192   g_clear_object (&self->widget);
193 
194   if (self->last_focus)
195     g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
196                                   (gpointer *) &self->last_focus);
197 
198   G_OBJECT_CLASS (adw_squeezer_page_parent_class)->finalize (object);
199 }
200 
201 static void
adw_squeezer_page_class_init(AdwSqueezerPageClass * klass)202 adw_squeezer_page_class_init (AdwSqueezerPageClass *klass)
203 {
204   GObjectClass *object_class = G_OBJECT_CLASS (klass);
205 
206   object_class->get_property = adw_squeezer_page_get_property;
207   object_class->set_property = adw_squeezer_page_set_property;
208   object_class->finalize = adw_squeezer_page_finalize;
209 
210   /**
211    * AdwSqueezerPage:child: (attributes org.gtk.Property.get=adw_squeezer_page_get_child)
212    *
213    * The child of the page.
214    *
215    * Since: 1.0
216    */
217   page_props[PAGE_PROP_CHILD] =
218     g_param_spec_object ("child",
219                          "Child",
220                          "The child of the page",
221                          GTK_TYPE_WIDGET,
222                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
223 
224   /**
225    * AdwSqueezerPage:enabled: (attributes org.gtk.Property.get=adw_squeezer_page_get_enabled org.gtk.Property.get=adw_squeezer_page_set_enabled)
226    *
227    * Whether the child is enabled.
228    *
229    * If a child is disabled, it will be ignored when looking for the child
230    * fitting the available size best.
231    *
232    * This allows to programmatically and prematurely hide a child even if it
233    * fits in the available space.
234    *
235    * This can be used e.g. to ensure a certain child is hidden below a certain
236    * window width, or any other constraint you find suitable.
237    *
238    * Since: 1.0
239    */
240   page_props[PAGE_PROP_ENABLED] =
241     g_param_spec_boolean ("enabled",
242                           "Enabled",
243                           "Whether the child is enabled",
244                           TRUE,
245                           G_PARAM_READWRITE);
246 
247   g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
248 }
249 
250 static void
adw_squeezer_page_init(AdwSqueezerPage * self)251 adw_squeezer_page_init (AdwSqueezerPage *self)
252 {
253   self->enabled = TRUE;
254 }
255 
256 #define ADW_TYPE_SQUEEZER_PAGES (adw_squeezer_pages_get_type ())
257 
258 G_DECLARE_FINAL_TYPE (AdwSqueezerPages, adw_squeezer_pages, ADW, SQUEEZER_PAGES, GObject)
259 
260 struct _AdwSqueezerPages
261 {
262   GObject parent_instance;
263   AdwSqueezer *squeezer;
264 };
265 
266 static GType
adw_squeezer_pages_get_item_type(GListModel * model)267 adw_squeezer_pages_get_item_type (GListModel *model)
268 {
269   return ADW_TYPE_SQUEEZER_PAGE;
270 }
271 
272 static guint
adw_squeezer_pages_get_n_items(GListModel * model)273 adw_squeezer_pages_get_n_items (GListModel *model)
274 {
275   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
276 
277   return g_list_length (self->squeezer->children);
278 }
279 
280 static gpointer
adw_squeezer_pages_get_item(GListModel * model,guint position)281 adw_squeezer_pages_get_item (GListModel *model,
282                              guint       position)
283 {
284   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
285   AdwSqueezerPage *page;
286 
287   page = g_list_nth_data (self->squeezer->children, position);
288 
289   if (!page)
290     return NULL;
291 
292   return g_object_ref (page);
293 }
294 
295 static void
adw_squeezer_pages_list_model_init(GListModelInterface * iface)296 adw_squeezer_pages_list_model_init (GListModelInterface *iface)
297 {
298   iface->get_item_type = adw_squeezer_pages_get_item_type;
299   iface->get_n_items = adw_squeezer_pages_get_n_items;
300   iface->get_item = adw_squeezer_pages_get_item;
301 }
302 
303 static gboolean
adw_squeezer_pages_is_selected(GtkSelectionModel * model,guint position)304 adw_squeezer_pages_is_selected (GtkSelectionModel *model,
305                                 guint              position)
306 {
307   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
308   AdwSqueezerPage *page;
309 
310   page = g_list_nth_data (self->squeezer->children, position);
311 
312   return page && page == self->squeezer->visible_child;
313 }
314 
315 static void
adw_squeezer_pages_selection_model_init(GtkSelectionModelInterface * iface)316 adw_squeezer_pages_selection_model_init (GtkSelectionModelInterface *iface)
317 {
318   iface->is_selected = adw_squeezer_pages_is_selected;
319 }
320 
G_DEFINE_TYPE_WITH_CODE(AdwSqueezerPages,adw_squeezer_pages,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,adw_squeezer_pages_list_model_init)G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,adw_squeezer_pages_selection_model_init))321 G_DEFINE_TYPE_WITH_CODE (AdwSqueezerPages, adw_squeezer_pages, G_TYPE_OBJECT,
322                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, adw_squeezer_pages_list_model_init)
323                          G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, adw_squeezer_pages_selection_model_init))
324 
325 static void
326 adw_squeezer_pages_init (AdwSqueezerPages *pages)
327 {
328 }
329 
330 static void
adw_squeezer_pages_class_init(AdwSqueezerPagesClass * klass)331 adw_squeezer_pages_class_init (AdwSqueezerPagesClass *klass)
332 {
333 }
334 
335 static AdwSqueezerPages *
adw_squeezer_pages_new(AdwSqueezer * squeezer)336 adw_squeezer_pages_new (AdwSqueezer *squeezer)
337 {
338   AdwSqueezerPages *pages;
339 
340   pages = g_object_new (ADW_TYPE_SQUEEZER_PAGES, NULL);
341   pages->squeezer = squeezer;
342 
343   return pages;
344 }
345 
346 static GtkOrientation
get_orientation(AdwSqueezer * self)347 get_orientation (AdwSqueezer *self)
348 {
349   return self->orientation;
350 }
351 
352 static void
set_orientation(AdwSqueezer * self,GtkOrientation orientation)353 set_orientation (AdwSqueezer    *self,
354                  GtkOrientation  orientation)
355 {
356   if (self->orientation == orientation)
357     return;
358 
359   self->orientation = orientation;
360   gtk_widget_queue_resize (GTK_WIDGET (self));
361   g_object_notify (G_OBJECT (self), "orientation");
362 }
363 
364 static AdwSqueezerPage *
find_page_for_widget(AdwSqueezer * self,GtkWidget * child)365 find_page_for_widget (AdwSqueezer *self,
366                       GtkWidget   *child)
367 {
368   AdwSqueezerPage *page;
369   GList *l;
370 
371   for (l = self->children; l != NULL; l = l->next) {
372     page = l->data;
373     if (page->widget == child)
374       return page;
375   }
376 
377   return NULL;
378 }
379 
380 static void
adw_squeezer_progress_updated(AdwSqueezer * self)381 adw_squeezer_progress_updated (AdwSqueezer *self)
382 {
383   if (!self->homogeneous)
384     gtk_widget_queue_resize (GTK_WIDGET (self));
385   else
386     gtk_widget_queue_draw (GTK_WIDGET (self));
387 
388   if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER &&
389       self->last_visible_child != NULL) {
390     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
391     self->last_visible_child = NULL;
392   }
393 }
394 
395 static gboolean
adw_squeezer_transition_cb(GtkWidget * widget,GdkFrameClock * frame_clock,gpointer user_data)396 adw_squeezer_transition_cb (GtkWidget     *widget,
397                             GdkFrameClock *frame_clock,
398                             gpointer       user_data)
399 {
400   AdwSqueezer *self = ADW_SQUEEZER (widget);
401 
402   if (self->first_frame_skipped) {
403     gtk_progress_tracker_advance_frame (&self->tracker,
404                                         gdk_frame_clock_get_frame_time (frame_clock));
405   } else {
406     self->first_frame_skipped = TRUE;
407   }
408 
409   /* Finish the animation early if the widget isn't mapped anymore. */
410   if (!gtk_widget_get_mapped (widget))
411     gtk_progress_tracker_finish (&self->tracker);
412 
413   adw_squeezer_progress_updated (ADW_SQUEEZER (widget));
414 
415   if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
416     self->tick_id = 0;
417     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
418 
419     return FALSE;
420   }
421 
422   return TRUE;
423 }
424 
425 static void
adw_squeezer_schedule_ticks(AdwSqueezer * self)426 adw_squeezer_schedule_ticks (AdwSqueezer *self)
427 {
428   if (self->tick_id == 0) {
429     self->tick_id =
430       gtk_widget_add_tick_callback (GTK_WIDGET (self), adw_squeezer_transition_cb, self, NULL);
431     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
432   }
433 }
434 
435 static void
adw_squeezer_unschedule_ticks(AdwSqueezer * self)436 adw_squeezer_unschedule_ticks (AdwSqueezer *self)
437 {
438   if (self->tick_id != 0) {
439     gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_id);
440     self->tick_id = 0;
441     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
442   }
443 }
444 
445 static void
adw_squeezer_start_transition(AdwSqueezer * self,AdwSqueezerTransitionType transition_type,guint transition_duration)446 adw_squeezer_start_transition (AdwSqueezer               *self,
447                                AdwSqueezerTransitionType  transition_type,
448                                guint                      transition_duration)
449 {
450   GtkWidget *widget = GTK_WIDGET (self);
451 
452   if (gtk_widget_get_mapped (widget) &&
453       adw_get_enable_animations (widget) &&
454       transition_type != ADW_SQUEEZER_TRANSITION_TYPE_NONE &&
455       transition_duration != 0 &&
456       (self->last_visible_child != NULL || self->allow_none)) {
457     self->active_transition_type = transition_type;
458     self->first_frame_skipped = FALSE;
459     adw_squeezer_schedule_ticks (self);
460     gtk_progress_tracker_start (&self->tracker,
461                                 self->transition_duration * 1000,
462                                 0,
463                                 1.0);
464   } else {
465     adw_squeezer_unschedule_ticks (self);
466     self->active_transition_type = ADW_SQUEEZER_TRANSITION_TYPE_NONE;
467     gtk_progress_tracker_finish (&self->tracker);
468   }
469 
470   adw_squeezer_progress_updated (ADW_SQUEEZER (widget));
471 }
472 
473 static void
set_visible_child(AdwSqueezer * self,AdwSqueezerPage * page,AdwSqueezerTransitionType transition_type,guint transition_duration)474 set_visible_child (AdwSqueezer               *self,
475                    AdwSqueezerPage           *page,
476                    AdwSqueezerTransitionType  transition_type,
477                    guint                      transition_duration)
478 {
479   GtkWidget *widget = GTK_WIDGET (self);
480   GtkRoot *root;
481   GtkWidget *focus;
482   gboolean contains_focus = FALSE;
483   guint old_pos = GTK_INVALID_LIST_POSITION;
484   guint new_pos = GTK_INVALID_LIST_POSITION;
485 
486   /* If we are being destroyed, do not bother with transitions and
487    * notifications.
488    */
489   if (gtk_widget_in_destruction (widget))
490     return;
491 
492   /* If none, pick the first visible. */
493   if (!page && !self->allow_none) {
494     GList *l;
495 
496     for (l = self->children; l; l = l->next) {
497       AdwSqueezerPage *p = l->data;
498       if (gtk_widget_get_visible (p->widget)) {
499         page = p;
500         break;
501       }
502     }
503   }
504 
505   if (page == self->visible_child)
506     return;
507 
508   if (page != NULL && self->pages) {
509     guint position;
510     GList *l;
511 
512     for (l = self->children, position = 0; l; l = l->next, position++) {
513       AdwSqueezerPage *p = l->data;
514       if (p == self->visible_child)
515         old_pos = position;
516       else if (p == page)
517         new_pos = position;
518     }
519   }
520 
521   root = gtk_widget_get_root (widget);
522   if (root)
523     focus = gtk_root_get_focus (root);
524   else
525     focus = NULL;
526 
527   if (focus &&
528       self->visible_child &&
529       self->visible_child->widget &&
530       gtk_widget_is_ancestor (focus, self->visible_child->widget)) {
531     contains_focus = TRUE;
532 
533     if (self->visible_child->last_focus)
534       g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus),
535                                     (gpointer *)&self->visible_child->last_focus);
536     self->visible_child->last_focus = focus;
537     g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus),
538                                (gpointer *)&self->visible_child->last_focus);
539   }
540 
541   if (self->last_visible_child != NULL)
542     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
543   self->last_visible_child = NULL;
544 
545   if (self->visible_child && self->visible_child->widget) {
546     if (gtk_widget_is_visible (widget)) {
547       self->last_visible_child = self->visible_child;
548       self->last_visible_widget_width = gtk_widget_get_width (self->last_visible_child->widget);
549       self->last_visible_widget_height = gtk_widget_get_height (self->last_visible_child->widget);
550     } else {
551       gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
552     }
553   }
554 
555   self->visible_child = page;
556 
557   if (page) {
558     gtk_widget_set_child_visible (page->widget, TRUE);
559 
560     if (contains_focus) {
561       if (page->last_focus)
562         gtk_widget_grab_focus (page->last_focus);
563       else
564         gtk_widget_child_focus (page->widget, GTK_DIR_TAB_FORWARD);
565     }
566   }
567 
568   if (self->homogeneous)
569     gtk_widget_queue_allocate (widget);
570   else
571     gtk_widget_queue_resize (widget);
572 
573   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
574 
575   if (self->pages) {
576     if (old_pos == GTK_INVALID_LIST_POSITION && new_pos == GTK_INVALID_LIST_POSITION)
577       ; /* nothing to do */
578     else if (old_pos == GTK_INVALID_LIST_POSITION)
579       gtk_selection_model_selection_changed (self->pages, new_pos, 1);
580     else if (new_pos == GTK_INVALID_LIST_POSITION)
581       gtk_selection_model_selection_changed (self->pages, old_pos, 1);
582     else
583       gtk_selection_model_selection_changed (self->pages,
584                                              MIN (old_pos, new_pos),
585                                              MAX (old_pos, new_pos) - MIN (old_pos, new_pos) + 1);
586   }
587 
588   adw_squeezer_start_transition (self, transition_type, transition_duration);
589 }
590 
591 static void
update_child_visible(AdwSqueezer * self,AdwSqueezerPage * page)592 update_child_visible (AdwSqueezer     *self,
593                       AdwSqueezerPage *page)
594 {
595   gboolean enabled;
596 
597   enabled = page->enabled && gtk_widget_get_visible (page->widget);
598 
599   if (self->visible_child == NULL && enabled)
600     set_visible_child (self, page, self->transition_type, self->transition_duration);
601   else if (self->visible_child == page && !enabled)
602     set_visible_child (self, NULL, self->transition_type, self->transition_duration);
603 
604   if (page == self->last_visible_child) {
605     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
606     self->last_visible_child = NULL;
607   }
608 }
609 
610 static void
squeezer_child_visibility_notify_cb(GObject * obj,GParamSpec * pspec,gpointer user_data)611 squeezer_child_visibility_notify_cb (GObject    *obj,
612                                      GParamSpec *pspec,
613                                      gpointer    user_data)
614 {
615   AdwSqueezer *self = ADW_SQUEEZER (user_data);
616   GtkWidget *child = GTK_WIDGET (obj);
617   AdwSqueezerPage *page;
618 
619   page = find_page_for_widget (self, child);
620   g_return_if_fail (page != NULL);
621 
622   update_child_visible (self, page);
623 }
624 
625 static void
add_page(AdwSqueezer * self,AdwSqueezerPage * page)626 add_page (AdwSqueezer     *self,
627           AdwSqueezerPage *page)
628 {
629   g_return_if_fail (page->widget != NULL);
630 
631   self->children = g_list_append (self->children, g_object_ref (page));
632 
633   gtk_widget_set_child_visible (page->widget, FALSE);
634   gtk_widget_set_parent (page->widget, GTK_WIDGET (self));
635 
636   if (self->pages)
637     g_list_model_items_changed (G_LIST_MODEL (self->pages), g_list_length (self->children) - 1, 0, 1);
638 
639   g_signal_connect (page->widget, "notify::visible",
640                     G_CALLBACK (squeezer_child_visibility_notify_cb), self);
641 
642   if (self->visible_child == NULL &&
643       gtk_widget_get_visible (page->widget))
644     set_visible_child (self, page, self->transition_type, self->transition_duration);
645 
646   if (self->homogeneous || self->visible_child == page)
647     gtk_widget_queue_resize (GTK_WIDGET (self));
648 }
649 
650 static void
squeezer_remove(AdwSqueezer * self,GtkWidget * child,gboolean in_dispose)651 squeezer_remove (AdwSqueezer *self,
652                  GtkWidget   *child,
653                  gboolean     in_dispose)
654 {
655   AdwSqueezerPage *page;
656   gboolean was_visible;
657 
658   page = find_page_for_widget (self, child);
659   if (!page)
660     return;
661 
662   self->children = g_list_remove (self->children, page);
663 
664   g_signal_handlers_disconnect_by_func (child,
665                                         squeezer_child_visibility_notify_cb,
666                                         self);
667 
668   was_visible = gtk_widget_get_visible (child);
669 
670   g_clear_object (&page->widget);
671 
672   if (self->visible_child == page)
673     {
674       if (in_dispose)
675         self->visible_child = NULL;
676       else
677         set_visible_child (self, NULL, self->transition_type, self->transition_duration);
678     }
679 
680   if (self->last_visible_child == page)
681     self->last_visible_child = NULL;
682 
683   gtk_widget_unparent (child);
684 
685   g_object_unref (page);
686 
687   if (self->homogeneous && was_visible)
688     gtk_widget_queue_resize (GTK_WIDGET (self));
689 }
690 
691 static void
adw_squeezer_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)692 adw_squeezer_get_property (GObject    *object,
693                            guint       property_id,
694                            GValue     *value,
695                            GParamSpec *pspec)
696 {
697   AdwSqueezer *self = ADW_SQUEEZER (object);
698 
699   switch (property_id) {
700   case PROP_HOMOGENEOUS:
701     g_value_set_boolean (value, adw_squeezer_get_homogeneous (self));
702     break;
703   case PROP_VISIBLE_CHILD:
704     g_value_set_object (value, adw_squeezer_get_visible_child (self));
705     break;
706   case PROP_ALLOW_NONE:
707     g_value_set_boolean (value, adw_squeezer_get_allow_none (self));
708     break;
709   case PROP_TRANSITION_DURATION:
710     g_value_set_uint (value, adw_squeezer_get_transition_duration (self));
711     break;
712   case PROP_TRANSITION_TYPE:
713     g_value_set_enum (value, adw_squeezer_get_transition_type (self));
714     break;
715   case PROP_TRANSITION_RUNNING:
716     g_value_set_boolean (value, adw_squeezer_get_transition_running (self));
717     break;
718   case PROP_INTERPOLATE_SIZE:
719     g_value_set_boolean (value, adw_squeezer_get_interpolate_size (self));
720     break;
721   case PROP_XALIGN:
722     g_value_set_float (value, adw_squeezer_get_xalign (self));
723     break;
724   case PROP_YALIGN:
725     g_value_set_float (value, adw_squeezer_get_yalign (self));
726     break;
727   case PROP_ORIENTATION:
728     g_value_set_enum (value, get_orientation (self));
729     break;
730   case PROP_PAGES:
731     g_value_take_object (value, adw_squeezer_get_pages (self));
732     break;
733   default:
734     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
735     break;
736   }
737 }
738 
739 static void
adw_squeezer_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)740 adw_squeezer_set_property (GObject      *object,
741                            guint         property_id,
742                            const GValue *value,
743                            GParamSpec   *pspec)
744 {
745   AdwSqueezer *self = ADW_SQUEEZER (object);
746 
747   switch (property_id) {
748   case PROP_HOMOGENEOUS:
749     adw_squeezer_set_homogeneous (self, g_value_get_boolean (value));
750     break;
751   case PROP_ALLOW_NONE:
752     adw_squeezer_set_allow_none (self, g_value_get_boolean (value));
753     break;
754   case PROP_TRANSITION_DURATION:
755     adw_squeezer_set_transition_duration (self, g_value_get_uint (value));
756     break;
757   case PROP_TRANSITION_TYPE:
758     adw_squeezer_set_transition_type (self, g_value_get_enum (value));
759     break;
760   case PROP_INTERPOLATE_SIZE:
761     adw_squeezer_set_interpolate_size (self, g_value_get_boolean (value));
762     break;
763   case PROP_XALIGN:
764     adw_squeezer_set_xalign (self, g_value_get_float (value));
765     break;
766   case PROP_YALIGN:
767     adw_squeezer_set_yalign (self, g_value_get_float (value));
768     break;
769   case PROP_ORIENTATION:
770     set_orientation (self, g_value_get_enum (value));
771     break;
772   default:
773     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
774     break;
775   }
776 }
777 
778 static void
adw_squeezer_snapshot_crossfade(GtkWidget * widget,GtkSnapshot * snapshot)779 adw_squeezer_snapshot_crossfade (GtkWidget   *widget,
780                                  GtkSnapshot *snapshot)
781 {
782   AdwSqueezer *self = ADW_SQUEEZER (widget);
783   double progress = gtk_progress_tracker_get_progress (&self->tracker, FALSE);
784 
785   gtk_snapshot_push_cross_fade (snapshot, progress);
786 
787   if (self->last_visible_child) {
788     int width_diff = MIN (gtk_widget_get_width (widget) - self->last_visible_widget_width, 0);
789     int height_diff = MIN (gtk_widget_get_height (widget) - self->last_visible_widget_height, 0);
790     float xalign = self->xalign;
791 
792     if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
793       xalign = 1 - xalign;
794 
795     gtk_snapshot_translate (snapshot,
796                             &GRAPHENE_POINT_INIT (
797                               width_diff * xalign,
798                               height_diff * self->yalign
799                             ));
800 
801     gtk_widget_snapshot_child (widget,
802                                self->last_visible_child->widget,
803                                snapshot);
804   }
805 
806   gtk_snapshot_pop (snapshot);
807 
808   if (self->visible_child)
809     gtk_widget_snapshot_child (widget,
810                                self->visible_child->widget,
811                                snapshot);
812   gtk_snapshot_pop (snapshot);
813 }
814 
815 
816 static void
adw_squeezer_snapshot(GtkWidget * widget,GtkSnapshot * snapshot)817 adw_squeezer_snapshot (GtkWidget   *widget,
818                        GtkSnapshot *snapshot)
819 {
820   AdwSqueezer *self = ADW_SQUEEZER (widget);
821 
822   if (self->visible_child || self->allow_none) {
823     if (gtk_progress_tracker_get_state (&self->tracker) != GTK_PROGRESS_STATE_AFTER) {
824       gtk_snapshot_push_clip (snapshot,
825                               &GRAPHENE_RECT_INIT(
826                                   0, 0,
827                                   gtk_widget_get_width (widget),
828                                   gtk_widget_get_height (widget)
829                               ));
830 
831       switch (self->active_transition_type)
832         {
833         case ADW_SQUEEZER_TRANSITION_TYPE_CROSSFADE:
834           adw_squeezer_snapshot_crossfade (widget, snapshot);
835           break;
836         case ADW_SQUEEZER_TRANSITION_TYPE_NONE:
837         default:
838           g_assert_not_reached ();
839         }
840 
841       gtk_snapshot_pop (snapshot);
842     } else if (self->visible_child) {
843       gtk_widget_snapshot_child (widget,
844                                  self->visible_child->widget,
845                                  snapshot);
846     }
847   }
848 }
849 
850 static void
adw_squeezer_size_allocate(GtkWidget * widget,int width,int height,int baseline)851 adw_squeezer_size_allocate (GtkWidget *widget,
852                             int        width,
853                             int        height,
854                             int        baseline)
855 {
856   AdwSqueezer *self = ADW_SQUEEZER (widget);
857   AdwSqueezerPage *page = NULL;
858   GList *l;
859   GtkAllocation child_allocation;
860 
861   for (l = self->children; l; l = l->next) {
862     GtkWidget *child = NULL;
863     int child_min;
864     int for_size = -1;
865 
866     page = l->data;
867     child = page->widget;
868 
869     if (!gtk_widget_get_visible (child))
870       continue;
871 
872     if (!page->enabled)
873       continue;
874 
875     if (self->orientation == GTK_ORIENTATION_VERTICAL) {
876       if (gtk_widget_get_request_mode (child) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
877         for_size = width;
878 
879       gtk_widget_measure (child, self->orientation, for_size,
880                           &child_min, NULL, NULL, NULL);
881 
882       if (child_min <= height)
883         break;
884     } else {
885       if (gtk_widget_get_request_mode (child) == GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT)
886         for_size = height;
887 
888       gtk_widget_measure (child, self->orientation, for_size,
889                           &child_min, NULL, NULL, NULL);
890 
891       if (child_min <= width)
892         break;
893     }
894   }
895 
896   if (l == NULL && self->allow_none)
897     page = NULL;
898 
899   set_visible_child (self, page,
900                      self->transition_type,
901                      self->transition_duration);
902 
903   child_allocation.x = 0;
904   child_allocation.y = 0;
905 
906   if (self->last_visible_child) {
907     int min, nat;
908 
909     if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
910       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
911                           -1, &min, &nat, NULL, NULL);
912       child_allocation.width = MAX (min, width);
913       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_VERTICAL,
914                           child_allocation.width, &min, &nat, NULL, NULL);
915       child_allocation.height = MAX (min, height);
916     } else {
917       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_VERTICAL,
918                           -1, &min, &nat, NULL, NULL);
919       child_allocation.height = MAX (min, height);
920       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
921                           child_allocation.height, &min, &nat, NULL, NULL);
922       child_allocation.width = MAX (min, width);
923     }
924 
925     gtk_widget_size_allocate (self->last_visible_child->widget, &child_allocation, -1);
926   }
927 
928   child_allocation.width = width;
929   child_allocation.height = height;
930 
931   if (self->visible_child) {
932     int min;
933 
934     gtk_widget_measure (self->visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
935                         height, &min, NULL, NULL, NULL);
936     child_allocation.width = MAX (child_allocation.width, min);
937 
938     gtk_widget_measure (self->visible_child->widget, GTK_ORIENTATION_VERTICAL,
939                         width, &min, NULL, NULL, NULL);
940     child_allocation.height = MAX (child_allocation.height, min);
941 
942     if (child_allocation.width > width) {
943       if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
944         child_allocation.x = (width - child_allocation.width) * (1 - self->xalign);
945       else
946         child_allocation.x = (width - child_allocation.width) * self->xalign;
947     }
948 
949     if (child_allocation.height > height)
950       child_allocation.y = (height - child_allocation.height) * self->yalign;
951 
952     gtk_widget_size_allocate (self->visible_child->widget, &child_allocation, -1);
953   }
954 }
955 
956 static void
adw_squeezer_measure(GtkWidget * widget,GtkOrientation orientation,int for_size,int * minimum,int * natural,int * minimum_baseline,int * natural_baseline)957 adw_squeezer_measure (GtkWidget      *widget,
958                       GtkOrientation  orientation,
959                       int             for_size,
960                       int            *minimum,
961                       int            *natural,
962                       int            *minimum_baseline,
963                       int            *natural_baseline)
964 {
965   AdwSqueezer *self = ADW_SQUEEZER (widget);
966   int child_min, child_nat;
967   GList *l;
968   int min = 0, nat = 0;
969 
970   for (l = self->children; l != NULL; l = l->next) {
971     AdwSqueezerPage *page = l->data;
972     GtkWidget *child = page->widget;
973 
974     if (self->orientation != orientation && !self->homogeneous &&
975         self->visible_child != page)
976       continue;
977 
978     if (!gtk_widget_get_visible (child))
979       continue;
980 
981     /* Disabled children are taken into account when measuring the widget, to
982      * keep its size request and allocation consistent. This avoids the
983      * appearant size and position of a child to changes suddenly when a larger
984      * child gets enabled/disabled.
985      */
986     gtk_widget_measure (child, orientation, for_size,
987                         &child_min, &child_nat, NULL, NULL);
988 
989     if (self->orientation == orientation) {
990       if (self->allow_none)
991         min = 0;
992       else
993         min = min == 0 ? child_min : MIN (min, child_min);
994     } else {
995       min = MAX (min, child_min);
996     }
997 
998     nat = MAX (nat, child_nat);
999   }
1000 
1001   if (self->orientation != orientation && !self->homogeneous &&
1002       self->interpolate_size &&
1003       (self->last_visible_child != NULL || self->allow_none)) {
1004     double t = gtk_progress_tracker_get_ease_out_cubic (&self->tracker, FALSE);
1005 
1006     if (orientation == GTK_ORIENTATION_VERTICAL) {
1007       min = adw_lerp (self->last_visible_widget_height, min, t);
1008       nat = adw_lerp (self->last_visible_widget_height, nat, t);
1009     } else {
1010       min = adw_lerp (self->last_visible_widget_width, min, t);
1011       nat = adw_lerp (self->last_visible_widget_width, nat, t);
1012     }
1013   }
1014 
1015   if (minimum)
1016     *minimum = min;
1017   if (natural)
1018     *natural = nat;
1019   if (minimum_baseline)
1020     *minimum_baseline = -1;
1021   if (natural_baseline)
1022     *natural_baseline = -1;
1023 }
1024 
1025 static void
adw_squeezer_dispose(GObject * object)1026 adw_squeezer_dispose (GObject *object)
1027 {
1028   AdwSqueezer *self = ADW_SQUEEZER (object);
1029   GtkWidget *child;
1030 
1031   if (self->pages)
1032     g_list_model_items_changed (G_LIST_MODEL (self->pages), 0,
1033                                 g_list_length (self->children), 0);
1034 
1035   while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
1036     squeezer_remove (self, child, TRUE);
1037 
1038   G_OBJECT_CLASS (adw_squeezer_parent_class)->dispose (object);
1039 }
1040 
1041 static void
adw_squeezer_finalize(GObject * object)1042 adw_squeezer_finalize (GObject *object)
1043 {
1044   AdwSqueezer *self = ADW_SQUEEZER (object);
1045 
1046   if (self->pages)
1047     g_object_remove_weak_pointer (G_OBJECT (self->pages),
1048                                   (gpointer *) &self->pages);
1049 
1050   adw_squeezer_unschedule_ticks (self);
1051 
1052   G_OBJECT_CLASS (adw_squeezer_parent_class)->finalize (object);
1053 }
1054 
1055 static void
adw_squeezer_class_init(AdwSqueezerClass * klass)1056 adw_squeezer_class_init (AdwSqueezerClass *klass)
1057 {
1058   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1059   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1060 
1061   object_class->get_property = adw_squeezer_get_property;
1062   object_class->set_property = adw_squeezer_set_property;
1063   object_class->dispose = adw_squeezer_dispose;
1064   object_class->finalize = adw_squeezer_finalize;
1065 
1066   widget_class->size_allocate = adw_squeezer_size_allocate;
1067   widget_class->snapshot = adw_squeezer_snapshot;
1068   widget_class->measure = adw_squeezer_measure;
1069   widget_class->get_request_mode = adw_widget_get_request_mode;
1070   widget_class->compute_expand = adw_widget_compute_expand;
1071 
1072   g_object_class_override_property (object_class,
1073                                     PROP_ORIENTATION,
1074                                     "orientation");
1075 
1076   /**
1077    * AdwSqueezer:homogeneous: (attributes org.gtk.Property.get=adw_squeezer_get_homogeneous org.gtk.Property.set=adw_squeezer_set_homogeneous)
1078    *
1079    * Whether all children have the same size for the opposite orientation.
1080    *
1081    * For example, if a squeezer is horizontal and is homogeneous, it will request
1082    * the same height for all its children. If it isn't, the squeezer may change
1083    * size when a different child becomes visible.
1084    *
1085    * Since: 1.0
1086    */
1087   props[PROP_HOMOGENEOUS] =
1088     g_param_spec_boolean ("homogeneous",
1089                           "Homogeneous",
1090                           "Whether all children have the same size for the opposite orientation",
1091                           FALSE,
1092                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1093 
1094   /**
1095    * AdwSqueezer:visible-child: (attributes org.gtk.Property.get=adw_squeezer_get_visible_child)
1096    *
1097    * The currently visible child.
1098    *
1099    * Since: 1.0
1100    */
1101   props[PROP_VISIBLE_CHILD] =
1102     g_param_spec_object ("visible-child",
1103                          "Visible child",
1104                          "The currently visible child",
1105                          GTK_TYPE_WIDGET,
1106                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
1107 
1108   /**
1109    * AdwSqueezer:allow-none: (attributes org.gtk.Property.get=adw_squeezer_get_allow_none org.gtk.Property.set=adw_squeezer_set_allow_none)
1110    *
1111    * Whether to allow squeezing beyond the last child's minimum size.
1112    *
1113    * If set to `TRUE`, the squeezer can shrink to the point where no child can
1114    * be shown. This is functionally equivalent to appending a widget with 0x0
1115    * minimum size.
1116    *
1117    * Since: 1.0
1118    */
1119   props[PROP_ALLOW_NONE] =
1120     g_param_spec_boolean ("allow-none",
1121                           "Allow none",
1122                           "Whether to allow squeezing beyond the last child's minimum size",
1123                           FALSE,
1124                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1125 
1126   /**
1127    * AdwSqueezer:transition-duration: (attributes org.gtk.Property.get=adw_squeezer_get_transition_duration org.gtk.Property.set=adw_squeezer_set_transition_duration)
1128    *
1129    * The animation duration, in milliseconds.
1130    *
1131    * Since: 1.0
1132    */
1133   props[PROP_TRANSITION_DURATION] =
1134     g_param_spec_uint ("transition-duration",
1135                        "Transition duration",
1136                        "The animation duration, in milliseconds",
1137                        0, G_MAXUINT, 200,
1138                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1139 
1140   /**
1141    * AdwSqueezer:transition-type: (attributes org.gtk.Property.get=adw_squeezer_get_transition_type org.gtk.Property.set=adw_squeezer_set_transition_type)
1142    *
1143    * The type of animation used for transitions between children.
1144    *
1145    * Since: 1.0
1146    */
1147   props[PROP_TRANSITION_TYPE] =
1148     g_param_spec_enum ("transition-type",
1149                        "Transition type",
1150                        "The type of animation used for transitions between children",
1151                        ADW_TYPE_SQUEEZER_TRANSITION_TYPE,
1152                        ADW_SQUEEZER_TRANSITION_TYPE_NONE,
1153                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1154 
1155   /**
1156    * AdwSqueezer:transition-running: (attributes org.gtk.Property.get=adw_squeezer_get_transition_running)
1157    *
1158    * Whether a transition is currently running.
1159    *
1160    * Since: 1.0
1161    */
1162   props[PROP_TRANSITION_RUNNING] =
1163     g_param_spec_boolean ("transition-running",
1164                           "Transition running",
1165                           "Whether a transition is currently running",
1166                           FALSE,
1167                           G_PARAM_READABLE);
1168 
1169   /**
1170    * AdwSqueezer:interpolate-size: (attributes org.gtk.Property.get=adw_squeezer_get_interpolate_size org.gtk.Property.set=adw_squeezer_set_interpolate_size)
1171    *
1172    * Whether the squeezer interpolates its size when changing the visible child.
1173    *
1174    * If `TRUE`, the squeezer will interpolate its size between the one of the
1175    * previous visible child and the one of the new visible child, according to
1176    * the set transition duration and the orientation, e.g. if the squeezer is
1177    * horizontal, it will interpolate the its height.
1178    *
1179    * Since: 1.0
1180    */
1181   props[PROP_INTERPOLATE_SIZE] =
1182     g_param_spec_boolean ("interpolate-size",
1183                           "Interpolate size",
1184                           "Whether the squeezer interpolates its size when changing the visible child",
1185                           FALSE,
1186                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1187 
1188   /**
1189    * AdwSqueezer:xalign: (attributes org.gtk.Property.get=adw_squeezer_get_xalign org.gtk.Property.set=adw_squeezer_set_xalign)
1190    *
1191    * The horizontal alignment, from 0 (start) to 1 (end).
1192    *
1193    * This affects the children allocation during transitions, when they exceed
1194    * the size of the squeezer.
1195    *
1196    * For example, 0.5 means the child will be centered, 0 means it will keep the
1197    * start side aligned and overflow the end side, and 1 means the opposite.
1198    *
1199    * Since: 1.0
1200    */
1201   props[PROP_XALIGN] =
1202     g_param_spec_float ("xalign",
1203                         "X align",
1204                         "The horizontal alignment, from 0 (start) to 1 (end)",
1205                         0.0, 1.0,
1206                         0.5,
1207                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1208 
1209   /**
1210    * AdwSqueezer:yalign: (attributes org.gtk.Property.get=adw_squeezer_get_yalign org.gtk.Property.set=adw_squeezer_set_yalign)
1211    *
1212    * The vertical alignment, from 0 (top) to 1 (bottom).
1213    *
1214    * This affects the children allocation during transitions, when they exceed
1215    * the size of the squeezer.
1216    *
1217    * For example, 0.5 means the child will be centered, 0 means it will keep the
1218    * top side aligned and overflow the bottom side, and 1 means the opposite.
1219    *
1220    * Since: 1.0
1221    */
1222   props[PROP_YALIGN] =
1223     g_param_spec_float ("yalign",
1224                         "Y align",
1225                         "The vertical alignment, from 0 (top) to 1 (bottom)",
1226                         0.0, 1.0,
1227                         0.5,
1228                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1229 
1230   /**
1231    * AdwSqueezer:pages: (attributes org.gtk.Property.get=adw_squeezer_get_pages)
1232    *
1233    * A selection model with the squeezer's pages.
1234    *
1235    * This can be used to keep an up-to-date view. The model also implements
1236    * [iface@Gtk.SelectionModel] and can be used to track the visible page.
1237    *
1238    * Since: 1.0
1239    */
1240   props[PROP_PAGES] =
1241     g_param_spec_object ("pages",
1242                          "Pages",
1243                          "A selection model with the squeezer's pages",
1244                          GTK_TYPE_SELECTION_MODEL,
1245                          G_PARAM_READABLE);
1246 
1247   g_object_class_install_properties (object_class, LAST_PROP, props);
1248 
1249   gtk_widget_class_set_css_name (widget_class, "squeezer");
1250 }
1251 
1252 static void
adw_squeezer_init(AdwSqueezer * self)1253 adw_squeezer_init (AdwSqueezer *self)
1254 {
1255   self->homogeneous = TRUE;
1256   self->transition_duration = 200;
1257   self->transition_type = ADW_SQUEEZER_TRANSITION_TYPE_NONE;
1258   self->xalign = 0.5;
1259   self->yalign = 0.5;
1260 }
1261 
1262 static void
adw_squeezer_buildable_add_child(GtkBuildable * buildable,GtkBuilder * builder,GObject * child,const char * type)1263 adw_squeezer_buildable_add_child (GtkBuildable *buildable,
1264                                   GtkBuilder   *builder,
1265                                   GObject      *child,
1266                                   const char   *type)
1267 {
1268   if (ADW_IS_SQUEEZER_PAGE (child))
1269     add_page (ADW_SQUEEZER (buildable), ADW_SQUEEZER_PAGE (child));
1270   else if (GTK_IS_WIDGET (child))
1271     adw_squeezer_add (ADW_SQUEEZER (buildable), GTK_WIDGET (child));
1272   else
1273     parent_buildable_iface->add_child (buildable, builder, child, type);
1274 }
1275 
1276 static void
adw_squeezer_buildable_init(GtkBuildableIface * iface)1277 adw_squeezer_buildable_init (GtkBuildableIface *iface)
1278 {
1279   parent_buildable_iface = g_type_interface_peek_parent (iface);
1280 
1281   iface->add_child = adw_squeezer_buildable_add_child;
1282 }
1283 
1284 /**
1285  * adw_squeezer_page_get_child: (attributes org.gtk.Method.get_property=child)
1286  * @self: a `AdwSqueezerPage`
1287  *
1288  * Returns the squeezer child to which @self belongs.
1289  *
1290  * Returns: (transfer none): the child to which @self belongs
1291  *
1292  * Since: 1.0
1293  */
1294 GtkWidget *
adw_squeezer_page_get_child(AdwSqueezerPage * self)1295 adw_squeezer_page_get_child (AdwSqueezerPage *self)
1296 {
1297   g_return_val_if_fail (ADW_IS_SQUEEZER_PAGE (self), NULL);
1298 
1299   return self->widget;
1300 }
1301 
1302 /**
1303  * adw_squeezer_page_get_enabled: (attributes org.gtk.Method.get_property=enabled)
1304  * @self: a `AdwSqueezerPage`
1305  *
1306  * Gets whether @self is enabled.
1307  *
1308  * Returns: whether @self is enabled
1309  *
1310  * Since: 1.0
1311  */
1312 gboolean
adw_squeezer_page_get_enabled(AdwSqueezerPage * self)1313 adw_squeezer_page_get_enabled (AdwSqueezerPage *self)
1314 {
1315   g_return_val_if_fail (ADW_IS_SQUEEZER_PAGE (self), FALSE);
1316 
1317   return self->enabled;
1318 }
1319 
1320 /**
1321  * adw_squeezer_page_set_enabled: (attributes org.gtk.Method.set_property=enabled)
1322  * @self: a `AdwSqueezerPage`
1323  * @enabled: whether @self is enabled
1324  *
1325  * Sets whether @self is enabled.
1326  *
1327  * Since: 1.0
1328  */
1329 void
adw_squeezer_page_set_enabled(AdwSqueezerPage * self,gboolean enabled)1330 adw_squeezer_page_set_enabled (AdwSqueezerPage *self,
1331                                gboolean         enabled)
1332 {
1333   g_return_if_fail (ADW_IS_SQUEEZER_PAGE (self));
1334 
1335   enabled = !!enabled;
1336 
1337   if (enabled == self->enabled)
1338     return;
1339 
1340   self->enabled = enabled;
1341 
1342   if (self->widget && gtk_widget_get_parent (self->widget)) {
1343     AdwSqueezer *squeezer = ADW_SQUEEZER (gtk_widget_get_parent (self->widget));
1344 
1345     gtk_widget_queue_resize (GTK_WIDGET (squeezer));
1346     update_child_visible (squeezer, self);
1347   }
1348 
1349   g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_ENABLED]);
1350 }
1351 
1352 /**
1353  * adw_squeezer_new:
1354  *
1355  * Creates a new `AdwSqueezer`.
1356  *
1357  * Returns: the newly created `AdwSqueezer`
1358  *
1359  * Since: 1.0
1360  */
1361 GtkWidget *
adw_squeezer_new(void)1362 adw_squeezer_new (void)
1363 {
1364   return g_object_new (ADW_TYPE_SQUEEZER, NULL);
1365 }
1366 
1367 /**
1368  * adw_squeezer_add:
1369  * @self: a `AdwSqueezer`
1370  * @child: the widget to add
1371  *
1372  * Adds a child to @self.
1373  *
1374  * Returns: (transfer none): the [class@Adw.SqueezerPage] for @child
1375  *
1376  * Since: 1.0
1377  */
1378 AdwSqueezerPage *
adw_squeezer_add(AdwSqueezer * self,GtkWidget * child)1379 adw_squeezer_add (AdwSqueezer *self,
1380                   GtkWidget   *child)
1381 {
1382   AdwSqueezerPage *page;
1383 
1384   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1385   g_return_val_if_fail (GTK_IS_WIDGET (child), NULL);
1386 
1387   page = g_object_new (ADW_TYPE_SQUEEZER_PAGE, NULL);
1388   page->widget = g_object_ref (child);
1389 
1390   add_page (self, page);
1391 
1392   g_object_unref (page);
1393 
1394   return page;
1395 }
1396 
1397 /**
1398  * adw_squeezer_remove:
1399  * @self: a `AdwSqueezer`
1400  * @child: the child to remove
1401  *
1402  * Removes a child widget from @self.
1403  *
1404  * Since: 1.0
1405  */
1406 void
adw_squeezer_remove(AdwSqueezer * self,GtkWidget * child)1407 adw_squeezer_remove (AdwSqueezer *self,
1408                      GtkWidget   *child)
1409 {
1410   GList *l;
1411   guint position;
1412 
1413   g_return_if_fail (ADW_IS_SQUEEZER (self));
1414   g_return_if_fail (GTK_IS_WIDGET (child));
1415   g_return_if_fail (gtk_widget_get_parent (child) == GTK_WIDGET (self));
1416 
1417   for (l = self->children, position = 0; l; l = l->next, position++) {
1418     AdwSqueezerPage *page = l->data;
1419 
1420     if (page->widget == child)
1421       break;
1422   }
1423 
1424   squeezer_remove (self, child, FALSE);
1425 
1426   if (self->pages)
1427     g_list_model_items_changed (G_LIST_MODEL (self->pages), position, 1, 0);
1428 }
1429 
1430 /**
1431  * adw_squeezer_get_page:
1432  * @self: a `AdwSqueezer`
1433  * @child: a child of @self
1434  *
1435  * Returns the [class@Adw.SqueezerPage] object for @child.
1436  *
1437  * Returns: (transfer none): the page object for @child
1438  *
1439  * Since: 1.0
1440  */
1441 AdwSqueezerPage *
adw_squeezer_get_page(AdwSqueezer * self,GtkWidget * child)1442 adw_squeezer_get_page (AdwSqueezer *self,
1443                        GtkWidget   *child)
1444 {
1445   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1446   g_return_val_if_fail (GTK_IS_WIDGET (child), NULL);
1447 
1448   return find_page_for_widget (self, child);
1449 }
1450 
1451 /**
1452  * adw_squeezer_get_homogeneous: (attributes org.gtk.Method.get_property=homogeneous)
1453  * @self: a `AdwSqueezer`
1454  *
1455  * Gets whether all children have the same size for the opposite orientation.
1456  *
1457  * Returns: whether @self is homogeneous
1458  *
1459  * Since: 1.0
1460  */
1461 gboolean
adw_squeezer_get_homogeneous(AdwSqueezer * self)1462 adw_squeezer_get_homogeneous (AdwSqueezer *self)
1463 {
1464   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1465 
1466   return self->homogeneous;
1467 }
1468 
1469 /**
1470  * adw_squeezer_set_homogeneous: (attributes org.gtk.Method.set_property=homogeneous)
1471  * @self: a `AdwSqueezer`
1472  * @homogeneous: whether @self is homogeneous
1473  *
1474  * Sets whether all children have the same size for the opposite orientation.
1475  *
1476  * Since: 1.0
1477  */
1478 void
adw_squeezer_set_homogeneous(AdwSqueezer * self,gboolean homogeneous)1479 adw_squeezer_set_homogeneous (AdwSqueezer *self,
1480                               gboolean     homogeneous)
1481 {
1482   g_return_if_fail (ADW_IS_SQUEEZER (self));
1483 
1484   homogeneous = !!homogeneous;
1485 
1486   if (self->homogeneous == homogeneous)
1487     return;
1488 
1489   self->homogeneous = homogeneous;
1490 
1491   if (gtk_widget_get_visible (GTK_WIDGET(self)))
1492     gtk_widget_queue_resize (GTK_WIDGET (self));
1493 
1494   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOMOGENEOUS]);
1495 }
1496 
1497 /**
1498  * adw_squeezer_get_allow_none: (attributes org.gtk.Method.get_property=allow-none)
1499  * @self: a `AdwSqueezer`
1500  *
1501  * Gets whether to allow squeezing beyond the last child's minimum size.
1502  *
1503  * Returns: whether @self allows squeezing beyond the last child
1504  *
1505  * Since: 1.0
1506  */
1507 gboolean
adw_squeezer_get_allow_none(AdwSqueezer * self)1508 adw_squeezer_get_allow_none (AdwSqueezer *self)
1509 {
1510   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1511 
1512   return self->allow_none;
1513 }
1514 
1515 /**
1516  * adw_squeezer_set_allow_none: (attributes org.gtk.Method.set_property=allow-none)
1517  * @self: a `AdwSqueezer`
1518  * @allow_none: whether @self allows squeezing beyond the last child
1519  *
1520  * Sets whether to allow squeezing beyond the last child's minimum size.
1521  *
1522  * Since: 1.0
1523  */
1524 void
adw_squeezer_set_allow_none(AdwSqueezer * self,gboolean allow_none)1525 adw_squeezer_set_allow_none (AdwSqueezer *self,
1526                              gboolean     allow_none)
1527 {
1528   g_return_if_fail (ADW_IS_SQUEEZER (self));
1529 
1530   allow_none = !!allow_none;
1531 
1532   if (self->allow_none == allow_none)
1533     return;
1534 
1535   self->allow_none = allow_none;
1536 
1537   gtk_widget_queue_resize (GTK_WIDGET (self));
1538 
1539   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_NONE]);
1540 }
1541 
1542 /**
1543  * adw_squeezer_get_transition_duration: (attributes org.gtk.Method.get_property=transition-duration)
1544  * @self: a `AdwSqueezer`
1545  *
1546  * Gets the transition animation duration for @self.
1547  *
1548  * Returns: the transition duration, in milliseconds
1549  *
1550  * Since: 1.0
1551  */
1552 guint
adw_squeezer_get_transition_duration(AdwSqueezer * self)1553 adw_squeezer_get_transition_duration (AdwSqueezer *self)
1554 {
1555   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0);
1556 
1557   return self->transition_duration;
1558 }
1559 
1560 /**
1561  * adw_squeezer_set_transition_duration: (attributes org.gtk.Method.set_property=transition-duration)
1562  * @self: a `AdwSqueezer`
1563  * @duration: the new duration, in milliseconds
1564  *
1565  * Sets the transition animation duration for @self.
1566  *
1567  * Since: 1.0
1568  */
1569 void
adw_squeezer_set_transition_duration(AdwSqueezer * self,guint duration)1570 adw_squeezer_set_transition_duration (AdwSqueezer *self,
1571                                       guint        duration)
1572 {
1573   g_return_if_fail (ADW_IS_SQUEEZER (self));
1574 
1575   if (self->transition_duration == duration)
1576     return;
1577 
1578   self->transition_duration = duration;
1579   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
1580 }
1581 
1582 /**
1583  * adw_squeezer_get_transition_type: (attributes org.gtk.Method.get_property=transition-type)
1584  * @self: a `AdwSqueezer`
1585  *
1586  * Gets the type of animation used for transitions between children in @self.
1587  *
1588  * Returns: the current transition type of @self
1589  *
1590  * Since: 1.0
1591  */
1592 AdwSqueezerTransitionType
adw_squeezer_get_transition_type(AdwSqueezer * self)1593 adw_squeezer_get_transition_type (AdwSqueezer *self)
1594 {
1595   g_return_val_if_fail (ADW_IS_SQUEEZER (self), ADW_SQUEEZER_TRANSITION_TYPE_NONE);
1596 
1597   return self->transition_type;
1598 }
1599 
1600 /**
1601  * adw_squeezer_set_transition_type: (attributes org.gtk.Method.set_property=transition-type)
1602  * @self: a `AdwSqueezer`
1603  * @transition: the new transition type
1604  *
1605  * Sets the type of animation used for transitions between children in @self.
1606  *
1607  * Since: 1.0
1608  */
1609 void
adw_squeezer_set_transition_type(AdwSqueezer * self,AdwSqueezerTransitionType transition)1610 adw_squeezer_set_transition_type (AdwSqueezer               *self,
1611                                   AdwSqueezerTransitionType  transition)
1612 {
1613   g_return_if_fail (ADW_IS_SQUEEZER (self));
1614 
1615   if (self->transition_type == transition)
1616     return;
1617 
1618   self->transition_type = transition;
1619   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_TYPE]);
1620 }
1621 
1622 /**
1623  * adw_squeezer_get_transition_running: (attributes org.gtk.Method.get_property=transition-running)
1624  * @self: a `AdwSqueezer`
1625  *
1626  * Gets whether a transition is currently running for @self.
1627  *
1628  * Returns: whether a transition is currently running
1629  *
1630  * Since: 1.0
1631  */
1632 gboolean
adw_squeezer_get_transition_running(AdwSqueezer * self)1633 adw_squeezer_get_transition_running (AdwSqueezer *self)
1634 {
1635   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1636 
1637   return (self->tick_id != 0);
1638 }
1639 
1640 /**
1641  * adw_squeezer_get_interpolate_size: (attributes org.gtk.Method.get_property=interpolate-size)
1642  * @self: A `AdwSqueezer`
1643  *
1644  * Gets whether @self interpolates its size when changing the visible child.
1645  *
1646  * Returns: whether the size is interpolated
1647  *
1648  * Since: 1.0
1649  */
1650 gboolean
adw_squeezer_get_interpolate_size(AdwSqueezer * self)1651 adw_squeezer_get_interpolate_size (AdwSqueezer *self)
1652 {
1653   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1654 
1655   return self->interpolate_size;
1656 }
1657 
1658 /**
1659  * adw_squeezer_set_interpolate_size: (attributes org.gtk.Method.set_property=interpolate-size)
1660  * @self: A `AdwSqueezer`
1661  * @interpolate_size: whether to interpolate the size
1662  *
1663  * Sets whether @self interpolates its size when changing the visible child.
1664  *
1665  * Since: 1.0
1666  */
1667 void
adw_squeezer_set_interpolate_size(AdwSqueezer * self,gboolean interpolate_size)1668 adw_squeezer_set_interpolate_size (AdwSqueezer *self,
1669                                    gboolean     interpolate_size)
1670 {
1671   g_return_if_fail (ADW_IS_SQUEEZER (self));
1672 
1673   interpolate_size = !!interpolate_size;
1674 
1675   if (self->interpolate_size == interpolate_size)
1676     return;
1677 
1678   self->interpolate_size = interpolate_size;
1679   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
1680 }
1681 
1682 /**
1683  * adw_squeezer_get_visible_child: (attributes org.gtk.Method.get_property=visible-child)
1684  * @self: a `AdwSqueezer`
1685  *
1686  * Gets the currently visible child of @self.
1687  *
1688  * Returns: (transfer none) (nullable): the visible child
1689  *
1690  * Since: 1.0
1691  */
1692 GtkWidget *
adw_squeezer_get_visible_child(AdwSqueezer * self)1693 adw_squeezer_get_visible_child (AdwSqueezer *self)
1694 {
1695   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1696 
1697   return self->visible_child ? self->visible_child->widget : NULL;
1698 }
1699 
1700 /**
1701  * adw_squeezer_get_xalign: (attributes org.gtk.Method.get_property=xalign)
1702  * @self: a `AdwSqueezer`
1703  *
1704  * Gets the horizontal alignment, from 0 (start) to 1 (end).
1705  *
1706  * Returns: the alignment value
1707  *
1708  * Since: 1.0
1709  */
1710 float
adw_squeezer_get_xalign(AdwSqueezer * self)1711 adw_squeezer_get_xalign (AdwSqueezer *self)
1712 {
1713   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0.5);
1714 
1715   return self->xalign;
1716 }
1717 
1718 /**
1719  * adw_squeezer_set_xalign: (attributes org.gtk.Method.set_property=xalign)
1720  * @self: a `AdwSqueezer`
1721  * @xalign: the new alignment value
1722  *
1723  * Sets the horizontal alignment, from 0 (start) to 1 (end).
1724  *
1725  * Since: 1.0
1726  */
1727 void
adw_squeezer_set_xalign(AdwSqueezer * self,float xalign)1728 adw_squeezer_set_xalign (AdwSqueezer *self,
1729                          float        xalign)
1730 {
1731   g_return_if_fail (ADW_IS_SQUEEZER (self));
1732 
1733   xalign = CLAMP (xalign, 0.0, 1.0);
1734 
1735   if (self->xalign == xalign)
1736     return;
1737 
1738   self->xalign = xalign;
1739   gtk_widget_queue_draw (GTK_WIDGET (self));
1740   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_XALIGN]);
1741 }
1742 
1743 /**
1744  * adw_squeezer_get_yalign: (attributes org.gtk.Method.get_property=yalign)
1745  * @self: a `AdwSqueezer`
1746  *
1747  * Gets the vertical alignment, from 0 (top) to 1 (bottom).
1748  *
1749  * Returns: the alignment value
1750  *
1751  * Since: 1.0
1752  */
1753 float
adw_squeezer_get_yalign(AdwSqueezer * self)1754 adw_squeezer_get_yalign (AdwSqueezer *self)
1755 {
1756   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0.5);
1757 
1758   return self->yalign;
1759 }
1760 
1761 /**
1762  * adw_squeezer_set_yalign: (attributes org.gtk.Method.set_property=yalign)
1763  * @self: a `AdwSqueezer`
1764  * @yalign: the new alignment value
1765  *
1766  * Sets the vertical alignment, from 0 (top) to 1 (bottom).
1767  *
1768  * Since: 1.0
1769  */
1770 void
adw_squeezer_set_yalign(AdwSqueezer * self,float yalign)1771 adw_squeezer_set_yalign (AdwSqueezer *self,
1772                          float        yalign)
1773 {
1774   g_return_if_fail (ADW_IS_SQUEEZER (self));
1775 
1776   yalign = CLAMP (yalign, 0.0, 1.0);
1777 
1778   if (self->yalign == yalign)
1779     return;
1780 
1781   self->yalign = yalign;
1782   gtk_widget_queue_draw (GTK_WIDGET (self));
1783   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_YALIGN]);
1784 }
1785 
1786 /**
1787  * adw_squeezer_get_pages: (attributes org.gtk.Method.get_property=pages)
1788  * @self: a `AdwSqueezer`
1789  *
1790  * Returns a `GListModel` that contains the pages of the squeezer,
1791  *
1792  * This can be used to keep an up-to-date view. The model also implements
1793  * [iface@Gtk.SelectionModel] and can be used to track the visible page.
1794  *
1795  * Returns: (transfer full): a `GtkSelectionModel` for the squeezer's children
1796  *
1797  * Since: 1.0
1798  */
1799 GtkSelectionModel *
adw_squeezer_get_pages(AdwSqueezer * self)1800 adw_squeezer_get_pages (AdwSqueezer *self)
1801 {
1802   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1803 
1804   if (self->pages)
1805     return g_object_ref (self->pages);
1806 
1807   self->pages = GTK_SELECTION_MODEL (adw_squeezer_pages_new (self));
1808   g_object_add_weak_pointer (G_OBJECT (self->pages), (gpointer *) &self->pages);
1809 
1810   return self->pages;
1811 }
1812