diff options
Diffstat (limited to 'subprojects/libhandy/src/hdy-stackable-box.c')
-rw-r--r-- | subprojects/libhandy/src/hdy-stackable-box.c | 3151 |
1 files changed, 3151 insertions, 0 deletions
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) +{ +} |