diff options
Diffstat (limited to '')
-rw-r--r-- | subprojects/libhandy/src/hdy-carousel-box.c | 1768 |
1 files changed, 1768 insertions, 0 deletions
diff --git a/subprojects/libhandy/src/hdy-carousel-box.c b/subprojects/libhandy/src/hdy-carousel-box.c new file mode 100644 index 0000000..1e0355f --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-box.c @@ -0,0 +1,1768 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-animation-private.h" +#include "hdy-cairo-private.h" +#include "hdy-carousel-box-private.h" + +#include <math.h> + +/** + * PRIVATE:hdy-carousel-box + * @short_description: Scrolling box used in #HdyCarousel + * @title: HdyCarouselBox + * @See_also: #HdyCarousel + * @stability: Private + * + * The #HdyCarouselBox object is meant to be used exclusively as part of the + * #HdyCarousel implementation. + * + * Since: 1.0 + */ + +typedef struct _HdyCarouselBoxAnimation HdyCarouselBoxAnimation; + +struct _HdyCarouselBoxAnimation +{ + gint64 start_time; + gint64 end_time; + gdouble start_value; + gdouble end_value; +}; + +typedef struct _HdyCarouselBoxChildInfo HdyCarouselBoxChildInfo; + +struct _HdyCarouselBoxChildInfo +{ + GtkWidget *widget; + GdkWindow *window; + gint position; + gboolean visible; + gdouble size; + gdouble snap_point; + gboolean adding; + gboolean removing; + + gboolean shift_position; + HdyCarouselBoxAnimation resize_animation; + + cairo_surface_t *surface; + cairo_region_t *dirty_region; +}; + +struct _HdyCarouselBox +{ + GtkContainer parent_instance; + + HdyCarouselBoxAnimation animation; + HdyCarouselBoxChildInfo *destination_child; + GList *children; + + gint child_width; + gint child_height; + + gdouble distance; + gdouble position; + guint spacing; + GtkOrientation orientation; + guint reveal_duration; + + guint tick_cb_id; +}; + +G_DEFINE_TYPE_WITH_CODE (HdyCarouselBox, hdy_carousel_box, GTK_TYPE_CONTAINER, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)); + +enum { + PROP_0, + PROP_N_PAGES, + PROP_POSITION, + PROP_SPACING, + PROP_REVEAL_DURATION, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_REVEAL_DURATION + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_ANIMATION_STOPPED, + SIGNAL_POSITION_SHIFTED, + SIGNAL_LAST_SIGNAL, +}; +static guint signals[SIGNAL_LAST_SIGNAL]; + +static HdyCarouselBoxChildInfo * +find_child_info (HdyCarouselBox *self, + GtkWidget *widget) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (widget == info->widget) + return info; + } + + return NULL; +} + +static gint +find_child_index (HdyCarouselBox *self, + GtkWidget *widget, + gboolean count_removing) +{ + GList *l; + gint i; + + i = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->removing && !count_removing) + continue; + + if (widget == info->widget) + return i; + + i++; + } + + return -1; +} + +static GList * +get_nth_link (HdyCarouselBox *self, + gint n) +{ + + GList *l; + gint i; + + i = n; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->removing) + continue; + + if (i-- == 0) + return l; + } + + return NULL; +} + +static HdyCarouselBoxChildInfo * +find_child_info_by_window (HdyCarouselBox *self, + GdkWindow *window) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (window == info->window) + return info; + } + + return NULL; +} + +static HdyCarouselBoxChildInfo * +get_closest_child_at (HdyCarouselBox *self, + gdouble position, + gboolean count_adding, + gboolean count_removing) +{ + GList *l; + HdyCarouselBoxChildInfo *closest_child = NULL; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (child->adding && !count_adding) + continue; + + if (child->removing && !count_removing) + continue; + + if (!closest_child || + ABS (closest_child->snap_point - position) > + ABS (child->snap_point - position)) + closest_child = child; + } + + return closest_child; +} + +static void +free_child_info (HdyCarouselBoxChildInfo *info) +{ + if (info->surface) + cairo_surface_destroy (info->surface); + if (info->dirty_region) + cairo_region_destroy (info->dirty_region); + g_free (info); +} + +static void +invalidate_handler_cb (GdkWindow *window, + cairo_region_t *region) +{ + gpointer user_data; + HdyCarouselBox *self; + HdyCarouselBoxChildInfo *info; + + gdk_window_get_user_data (window, &user_data); + g_assert (HDY_IS_CAROUSEL_BOX (user_data)); + self = HDY_CAROUSEL_BOX (user_data); + + info = find_child_info_by_window (self, window); + + if (!info->dirty_region) + info->dirty_region = cairo_region_create (); + + cairo_region_union (info->dirty_region, region); +} + +static void +register_window (HdyCarouselBoxChildInfo *info, + HdyCarouselBox *self) +{ + GtkWidget *widget; + GdkWindow *window; + GdkWindowAttr attributes; + GtkAllocation allocation; + gint attributes_mask; + + if (info->removing) + return; + + widget = GTK_WIDGET (self); + gtk_widget_get_allocation (info->widget, &allocation); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = gtk_widget_get_events (widget); + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + window = gdk_window_new (gtk_widget_get_parent_window (widget), + &attributes, attributes_mask); + gtk_widget_register_window (widget, window); + gtk_widget_set_parent_window (info->widget, window); + + gdk_window_set_user_data (window, self); + + gdk_window_show (window); + + info->window = window; + + gdk_window_set_invalidate_handler (window, invalidate_handler_cb); +} + +static void +unregister_window (HdyCarouselBoxChildInfo *info, + HdyCarouselBox *self) +{ + if (!info->widget) + return; + + gtk_widget_set_parent_window (info->widget, NULL); + gtk_widget_unregister_window (GTK_WIDGET (self), info->window); + gdk_window_destroy (info->window); + info->window = NULL; +} + +static gdouble +get_animation_value (HdyCarouselBoxAnimation *animation, + GdkFrameClock *frame_clock) +{ + gint64 frame_time, duration; + gdouble t; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + frame_time = MIN (frame_time, animation->end_time); + + duration = animation->end_time - animation->start_time; + t = (gdouble) (frame_time - animation->start_time) / duration; + t = hdy_ease_out_cubic (t); + + return hdy_lerp (animation->start_value, animation->end_value, t); +} + +static gboolean +animate_position (HdyCarouselBox *self, + GdkFrameClock *frame_clock) +{ + gint64 frame_time; + gdouble value; + + if (!hdy_carousel_box_is_animating (self)) + return G_SOURCE_REMOVE; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + self->animation.end_value = self->destination_child->snap_point; + value = get_animation_value (&self->animation, frame_clock); + hdy_carousel_box_set_position (self, value); + + if (frame_time >= self->animation.end_time) { + self->animation.start_time = 0; + self->animation.end_time = 0; + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void update_windows (HdyCarouselBox *self); + +static void +complete_child_animation (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + update_windows (self); + + if (child->adding) + child->adding = FALSE; + + if (child->removing) { + self->children = g_list_remove (self->children, child); + + free_child_info (child); + } +} + +static gboolean +animate_child_size (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child, + GdkFrameClock *frame_clock, + gdouble *delta) +{ + gint64 frame_time; + gdouble d, new_value; + + if (child->resize_animation.start_time == 0) + return G_SOURCE_REMOVE; + + new_value = get_animation_value (&child->resize_animation, frame_clock); + d = new_value - child->size; + + child->size += d; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + if (delta) + *delta = d; + + if (frame_time >= child->resize_animation.end_time) { + child->resize_animation.start_time = 0; + child->resize_animation.end_time = 0; + complete_child_animation (self, child); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +set_position (HdyCarouselBox *self, + gdouble position) +{ + gdouble lower, upper; + + hdy_carousel_box_get_range (self, &lower, &upper); + + position = CLAMP (position, lower, upper); + + self->position = position; + update_windows (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]); +} + +static gboolean +animation_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + g_autoptr (GList) children = NULL; + GList *l; + gboolean should_continue; + gdouble position_shift; + + should_continue = G_SOURCE_REMOVE; + + position_shift = 0; + + children = g_list_copy (self->children); + for (l = children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + gdouble delta; + gboolean shift; + + delta = 0; + shift = child->shift_position; + + should_continue |= animate_child_size (self, child, frame_clock, &delta); + + if (shift) + position_shift += delta; + } + + update_windows (self); + + if (position_shift != 0) { + set_position (self, self->position + position_shift); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, position_shift); + } + + should_continue |= animate_position (self, frame_clock); + + update_windows (self); + + if (!should_continue) + self->tick_cb_id = 0; + + return should_continue; +} + +static void +update_shift_position_flag (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + HdyCarouselBoxChildInfo *closest_child; + gint animating_index, closest_index; + + /* We want to still shift position when the active child is being removed */ + closest_child = get_closest_child_at (self, self->position, FALSE, TRUE); + + if (!closest_child) + return; + + animating_index = g_list_index (self->children, child); + closest_index = g_list_index (self->children, closest_child); + + child->shift_position = (closest_index >= animating_index); +} + +static void +animate_child (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child, + gdouble value, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + + if (child->resize_animation.start_time > 0) { + child->resize_animation.start_time = 0; + child->resize_animation.end_time = 0; + } + + update_shift_position_flag (self, child); + + if (!gtk_widget_get_realized (GTK_WIDGET (self)) || + duration <= 0 || + !hdy_get_enable_animations (GTK_WIDGET (self))) { + gdouble delta = value - child->size; + + child->size = value; + + if (child->shift_position) { + set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); + } + + complete_child_animation (self, child); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + gdouble delta = value - child->size; + + child->size = value; + + if (child->shift_position) { + set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); + } + + complete_child_animation (self, child); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + child->resize_animation.start_value = child->size; + child->resize_animation.end_value = value; + + child->resize_animation.start_time = frame_time / 1000; + child->resize_animation.end_time = child->resize_animation.start_time + duration; + if (self->tick_cb_id == 0) + self->tick_cb_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL); +} + +static gboolean +hdy_carousel_box_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->adding || info->removing) + continue; + + if (!info->visible) + continue; + + if (info->dirty_region && !info->removing) { + g_autoptr (cairo_t) surface_cr = NULL; + GtkAllocation child_alloc; + + if (!info->surface) { + gint width, height; + + width = gdk_window_get_width (info->window); + height = gdk_window_get_height (info->window); + + info->surface = gdk_window_create_similar_surface (info->window, + CAIRO_CONTENT_COLOR_ALPHA, + width, height); + } + + gtk_widget_get_allocation (info->widget, &child_alloc); + + surface_cr = cairo_create (info->surface); + + gdk_cairo_region (surface_cr, info->dirty_region); + cairo_clip (surface_cr); + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + cairo_translate (surface_cr, 0, -info->position); + else + cairo_translate (surface_cr, -info->position, 0); + + cairo_save (surface_cr); + cairo_set_source_rgba (surface_cr, 0, 0, 0, 0); + cairo_set_operator (surface_cr, CAIRO_OPERATOR_SOURCE); + cairo_paint (surface_cr); + cairo_restore (surface_cr); + + gtk_container_propagate_draw (GTK_CONTAINER (self), info->widget, surface_cr); + + cairo_region_destroy (info->dirty_region); + info->dirty_region = NULL; + } + + if (!info->surface) + continue; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + cairo_set_source_surface (cr, info->surface, 0, info->position); + else + cairo_set_source_surface (cr, info->surface, info->position, 0); + cairo_paint (cr); + } + + return GDK_EVENT_PROPAGATE; +} + +static void +measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + GList *children; + + if (minimum) + *minimum = 0; + if (natural) + *natural = 0; + + if (minimum_baseline) + *minimum_baseline = -1; + if (natural_baseline) + *natural_baseline = -1; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + gint child_min, child_nat; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_VERTICAL) { + if (for_size < 0) + gtk_widget_get_preferred_height (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat); + } else { + if (for_size < 0) + gtk_widget_get_preferred_width (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat); + } + + if (minimum) + *minimum = MAX (*minimum, child_min); + if (natural) + *natural = MAX (*natural, child_nat); + } +} + +static void +hdy_carousel_box_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_width_for_height (GtkWidget *widget, + gint for_height, + gint *minimum_width, + gint *natural_width) +{ + measure (widget, GTK_ORIENTATION_HORIZONTAL, for_height, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_height_for_width (GtkWidget *widget, + gint for_width, + gint *minimum_height, + gint *natural_height) +{ + measure (widget, GTK_ORIENTATION_VERTICAL, for_width, + minimum_height, natural_height, NULL, NULL); +} + +static void +invalidate_cache_for_child (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + cairo_rectangle_int_t rect; + + rect.x = 0; + rect.y = 0; + rect.width = self->child_width; + rect.height = self->child_height; + + if (child->surface) + g_clear_pointer (&child->surface, cairo_surface_destroy); + + if (child->dirty_region) + cairo_region_destroy (child->dirty_region); + child->dirty_region = cairo_region_create_rectangle (&rect); +} + +static void +invalidate_drawing_cache (HdyCarouselBox *self) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child_info = l->data; + + invalidate_cache_for_child (self, child_info); + } +} + +static void +update_windows (HdyCarouselBox *self) +{ + GList *children; + GtkAllocation alloc; + gdouble x, y, offset; + gboolean is_rtl; + gdouble snap_point; + + snap_point = 0; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + child_info->snap_point = snap_point + child_info->size - 1; + + snap_point += child_info->size; + } + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + return; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + x = alloc.x; + y = alloc.y; + + is_rtl = (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL); + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + offset = (self->distance * self->position) - (alloc.height - self->child_height) / 2.0; + else if (is_rtl) + offset = -(self->distance * self->position) + (alloc.width - self->child_width) / 2.0; + else + offset = (self->distance * self->position) - (alloc.width - self->child_width) / 2.0; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + y -= offset; + else + x -= offset; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + if (!child_info->removing) { + if (!gtk_widget_get_visible (child_info->widget)) + continue; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) { + child_info->position = y; + child_info->visible = child_info->position < alloc.height && + child_info->position + self->child_height > 0; + gdk_window_move (child_info->window, alloc.x, alloc.y + child_info->position); + } else { + child_info->position = x; + child_info->visible = child_info->position < alloc.width && + child_info->position + self->child_width > 0; + gdk_window_move (child_info->window, alloc.x + child_info->position, alloc.y); + } + + if (!child_info->visible) + invalidate_cache_for_child (self, child_info); + } + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + y += self->distance * child_info->size; + else if (is_rtl) + x -= self->distance * child_info->size; + else + x += self->distance * child_info->size; + } +} + +static void +hdy_carousel_box_map (GtkWidget *widget) +{ + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->map (widget); + + gtk_widget_queue_draw (GTK_WIDGET (widget)); +} + +static void +hdy_carousel_box_realize (GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->realize (widget); + + g_list_foreach (self->children, (GFunc) register_window, self); + + gtk_widget_queue_allocate (widget); +} + +static void +hdy_carousel_box_unrealize (GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + + g_list_foreach (self->children, (GFunc) unregister_window, self); + + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->unrealize (widget); +} + +static void +hdy_carousel_box_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + gint size, width, height; + GList *children; + + gtk_widget_set_allocation (widget, allocation); + + size = 0; + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + gint min, nat; + gint child_size; + + if (child_info->removing) + continue; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + gtk_widget_get_preferred_width_for_height (child, allocation->height, + &min, &nat); + if (gtk_widget_get_hexpand (child)) + child_size = MAX (min, allocation->width); + else + child_size = MAX (min, nat); + } else { + gtk_widget_get_preferred_height_for_width (child, allocation->width, + &min, &nat); + if (gtk_widget_get_vexpand (child)) + child_size = MAX (min, allocation->height); + else + child_size = MAX (min, nat); + } + + size = MAX (size, child_size); + } + + self->distance = size + self->spacing; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + width = size; + height = allocation->height; + } else { + width = allocation->width; + height = size; + } + + if (width != self->child_width || height != self->child_height) + invalidate_drawing_cache (self); + + self->child_width = width; + self->child_height = height; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child_info->widget)) + continue; + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + continue; + + gdk_window_resize (child_info->window, width, height); + } + + update_windows (self); + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + GtkAllocation alloc; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child)) + continue; + + alloc.x = 0; + alloc.y = 0; + alloc.width = width; + alloc.height = height; + gtk_widget_size_allocate (child, &alloc); + } + + invalidate_drawing_cache (self); + gtk_widget_set_clip (widget, allocation); +} + +static void +hdy_carousel_box_add (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + + hdy_carousel_box_insert (self, widget, -1); +} + +static void +shift_position (HdyCarouselBox *self, + gdouble delta) +{ + hdy_carousel_box_set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); +} + +static void +hdy_carousel_box_remove (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + HdyCarouselBoxChildInfo *info; + + info = find_child_info (self, widget); + if (!info) + return; + + info->removing = TRUE; + + gtk_widget_unparent (widget); + + if (gtk_widget_get_realized (GTK_WIDGET (container))) + unregister_window (info, self); + + info->widget = NULL; + + if (!gtk_widget_in_destruction (GTK_WIDGET (container))) + animate_child (self, info, 0, self->reveal_duration); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]); +} + +static void +hdy_carousel_box_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + g_autoptr (GList) children = NULL; + GList *l; + + children = g_list_copy (self->children); + for (l = children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (!child->removing) + (* callback) (child->widget, callback_data); + } +} + +static void +hdy_carousel_box_finalize (GObject *object) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + if (self->tick_cb_id > 0) + gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id); + + g_list_free_full (self->children, (GDestroyNotify) free_child_info); + + G_OBJECT_CLASS (hdy_carousel_box_parent_class)->finalize (object); +} + +static void +hdy_carousel_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + switch (prop_id) { + case PROP_N_PAGES: + g_value_set_uint (value, hdy_carousel_box_get_n_pages (self)); + break; + + case PROP_POSITION: + g_value_set_double (value, hdy_carousel_box_get_position (self)); + break; + + case PROP_SPACING: + g_value_set_uint (value, hdy_carousel_box_get_spacing (self)); + break; + + case PROP_REVEAL_DURATION: + g_value_set_uint (value, hdy_carousel_box_get_reveal_duration (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + switch (prop_id) { + case PROP_POSITION: + hdy_carousel_box_set_position (self, g_value_get_double (value)); + break; + + case PROP_SPACING: + hdy_carousel_box_set_spacing (self, g_value_get_uint (value)); + break; + + case PROP_REVEAL_DURATION: + hdy_carousel_box_set_reveal_duration (self, g_value_get_uint (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_box_class_init (HdyCarouselBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = hdy_carousel_box_finalize; + object_class->get_property = hdy_carousel_box_get_property; + object_class->set_property = hdy_carousel_box_set_property; + widget_class->draw = hdy_carousel_box_draw; + widget_class->get_preferred_width = hdy_carousel_box_get_preferred_width; + widget_class->get_preferred_height = hdy_carousel_box_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_carousel_box_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_carousel_box_get_preferred_height_for_width; + widget_class->map = hdy_carousel_box_map; + widget_class->realize = hdy_carousel_box_realize; + widget_class->unrealize = hdy_carousel_box_unrealize; + widget_class->size_allocate = hdy_carousel_box_size_allocate; + container_class->add = hdy_carousel_box_add; + container_class->remove = hdy_carousel_box_remove; + container_class->forall = hdy_carousel_box_forall; + + /** + * HdyCarouselBox:n-pages: + * + * The number of pages in a #HdyCarouselBox + * + * Since: 1.0 + */ + props[PROP_N_PAGES] = + g_param_spec_uint ("n-pages", + _("Number of pages"), + _("Number of pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:position: + * + * Current scrolling position, unitless. 1 matches 1 page. + * + * Since: 1.0 + */ + props[PROP_POSITION] = + g_param_spec_double ("position", + _("Position"), + _("Current scrolling position"), + 0, + G_MAXDOUBLE, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:spacing: + * + * Spacing between pages in pixels. + * + * Since: 1.0 + */ + props[PROP_SPACING] = + g_param_spec_uint ("spacing", + _("Spacing"), + _("Spacing between pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:reveal-duration: + * + * Duration of the animation used when adding or removing pages, in + * milliseconds. + * + * Since: 1.0 + */ + props[PROP_REVEAL_DURATION] = + g_param_spec_uint ("reveal-duration", + _("Reveal duration"), + _("Page reveal duration"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * HdyCarouselBox::animation-stopped: + * @self: The #HdyCarouselBox instance + * + * This signal is emitted after an animation has been stopped. If animations + * are disabled, the signal is emitted as well. + * + * Since: 1.0 + */ + signals[SIGNAL_ANIMATION_STOPPED] = + g_signal_new ("animation-stopped", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * HdyCarouselBox::position-shifted: + * @self: The #HdyCarouselBox instance + * @delta: The amount to shift the position by + * + * This signal is emitted when position has been programmatically shifted. + * + * Since: 1.0 + */ + signals[SIGNAL_POSITION_SHIFTED] = + g_signal_new ("position-shifted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_DOUBLE); +} + +static void +hdy_carousel_box_init (HdyCarouselBox *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + + self->orientation = GTK_ORIENTATION_HORIZONTAL; + self->reveal_duration = 0; + + gtk_widget_set_has_window (widget, FALSE); +} + +/** + * hdy_carousel_box_new: + * + * Create a new #HdyCarouselBox widget. + * + * Returns: The newly created #HdyCarouselBox widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_new (void) +{ + return g_object_new (HDY_TYPE_CAROUSEL_BOX, NULL); +} + +/** + * hdy_carousel_box_insert: + * @self: a #HdyCarouselBox + * @widget: a widget to add + * @position: the position to insert @widget in. + * + * Inserts @widget into @self at position @position. + * + * If position is -1, or larger than the number of pages, @widget will be + * appended to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_box_insert (HdyCarouselBox *self, + GtkWidget *widget, + gint position) +{ + HdyCarouselBoxChildInfo *info; + GList *prev_link; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + info = g_new0 (HdyCarouselBoxChildInfo, 1); + info->widget = widget; + info->size = 0; + info->adding = TRUE; + + if (gtk_widget_get_realized (GTK_WIDGET (self))) + register_window (info, self); + + if (position >= 0) + prev_link = get_nth_link (self, position); + else + prev_link = NULL; + + self->children = g_list_insert_before (self->children, prev_link, info); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + + update_windows (self); + + animate_child (self, info, 1, self->reveal_duration); + + invalidate_drawing_cache (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]); +} + +/** + * hdy_carousel_box_reorder: + * @self: a #HdyCarouselBox + * @widget: a widget to add + * @position: the position to move @widget to. + * + * Moves @widget into position @position. + * + * If position is -1, or larger than the number of pages, @widget will be moved + * to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_box_reorder (HdyCarouselBox *self, + GtkWidget *widget, + gint position) +{ + HdyCarouselBoxChildInfo *info, *prev_info; + GList *link, *prev_link; + gint old_position; + gdouble closest_point, old_point, new_point; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + closest_point = hdy_carousel_box_get_closest_snap_point (self); + + info = find_child_info (self, widget); + link = g_list_find (self->children, info); + old_position = g_list_position (self->children, link); + + if (position == old_position) + return; + + old_point = ((HdyCarouselBoxChildInfo *) link->data)->snap_point; + + if (position < 0 || position >= hdy_carousel_box_get_n_pages (self)) + prev_link = g_list_last (self->children); + else + prev_link = get_nth_link (self, position); + + prev_info = prev_link->data; + new_point = prev_info->snap_point; + if (new_point > old_point) + new_point -= prev_info->size; + + self->children = g_list_remove_link (self->children, link); + self->children = g_list_insert_before (self->children, prev_link, link->data); + + if (closest_point == old_point) + shift_position (self, new_point - old_point); + else if (old_point > closest_point && closest_point >= new_point) + shift_position (self, info->size); + else if (new_point >= closest_point && closest_point > old_point) + shift_position (self, -info->size); +} + +/** + * hdy_carousel_box_is_animating: + * @self: a #HdyCarouselBox + * + * Get whether @self is animating position. + * + * Returns: %TRUE if an animation is running + * + * Since: 1.0 + */ +gboolean +hdy_carousel_box_is_animating (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), FALSE); + + return (self->animation.start_time != 0); +} + +/** + * hdy_carousel_box_stop_animation: + * @self: a #HdyCarouselBox + * + * Stops a running animation. If there's no animation running, does nothing. + * + * It does not reset position to a non-transient value automatically. + * + * Since: 1.0 + */ +void +hdy_carousel_box_stop_animation (HdyCarouselBox *self) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->animation.start_time == 0) + return; + + self->animation.start_time = 0; + self->animation.end_time = 0; +} + +/** + * hdy_carousel_box_scroll_to: + * @self: a #HdyCarouselBox + * @widget: a child of @self + * @duration: animation duration in milliseconds + * + * Scrolls to @widget position over the next @duration milliseconds using + * easeOutCubic interpolator. + * + * If an animation was already running, it will be cancelled automatically. + * + * @duration can be 0, in that case the position will be + * changed immediately. + * + * Since: 1.0 + */ +void +hdy_carousel_box_scroll_to (HdyCarouselBox *self, + GtkWidget *widget, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + gdouble position; + HdyCarouselBoxChildInfo *child; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (duration >= 0); + + child = find_child_info (self, widget); + position = child->snap_point; + + hdy_carousel_box_stop_animation (self); + + if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) { + hdy_carousel_box_set_position (self, position); + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + hdy_carousel_box_set_position (self, position); + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + self->destination_child = child; + + self->animation.start_value = self->position; + self->animation.end_value = position; + + self->animation.start_time = frame_time / 1000; + self->animation.end_time = self->animation.start_time + duration; + if (self->tick_cb_id == 0) + self->tick_cb_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL); +} + +/** + * hdy_carousel_box_get_n_pages: + * @self: a #HdyCarouselBox + * + * Gets the number of pages in @self. + * + * Returns: The number of pages in @self + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_n_pages (HdyCarouselBox *self) +{ + GList *l; + guint n_pages; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + n_pages = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (!child->removing) + n_pages++; + } + + return n_pages; +} + +/** + * hdy_carousel_box_get_distance: + * @self: a #HdyCarouselBox + * + * Gets swiping distance between two adjacent children in pixels. + * + * Returns: The swiping distance in pixels + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_distance (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->distance; +} + +/** + * hdy_carousel_box_get_position: + * @self: a #HdyCarouselBox + * + * Gets current scroll position in @self. It's unitless, 1 matches 1 page. + * + * Returns: The scroll position + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_position (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->position; +} + +/** + * hdy_carousel_box_set_position: + * @self: a #HdyCarouselBox + * @position: the new position value + * + * Sets current scroll position in @self, unitless, 1 matches 1 page. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_position (HdyCarouselBox *self, + gdouble position) +{ + GList *l; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + set_position (self, position); + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (child->adding || child->removing) + update_shift_position_flag (self, child); + } +} + +/** + * hdy_carousel_box_get_spacing: + * @self: a #HdyCarouselBox + * + * Gets spacing between pages in pixels. + * + * Returns: Spacing between pages + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_spacing (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->spacing; +} + +/** + * hdy_carousel_box_set_spacing: + * @self: a #HdyCarouselBox + * @spacing: the new spacing value + * + * Sets spacing between pages in pixels. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_spacing (HdyCarouselBox *self, + guint spacing) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->spacing == spacing) + return; + + self->spacing = spacing; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]); +} + +/** + * hdy_carousel_box_get_reveal_duration: + * @self: a #HdyCarouselBox + * + * Gets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Returns: Page reveal duration + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->reveal_duration; +} + +/** + * hdy_carousel_box_set_reveal_duration: + * @self: a #HdyCarouselBox + * @reveal_duration: the new reveal duration value + * + * Sets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self, + guint reveal_duration) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->reveal_duration == reveal_duration) + return; + + self->reveal_duration = reveal_duration; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]); +} + +/** + * hdy_carousel_box_get_nth_child: + * @self: a #HdyCarouselBox + * @n: the child index + * + * Retrieves @n-th child widget of @self. + * + * Returns: The @n-th child widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_get_nth_child (HdyCarouselBox *self, + guint n) +{ + HdyCarouselBoxChildInfo *info; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + g_return_val_if_fail (n < hdy_carousel_box_get_n_pages (self), NULL); + + info = get_nth_link (self, n)->data; + + return info->widget; +} + +/** + * hdy_carousel_box_get_snap_points: + * @self: a #HdyCarouselBox + * @n_snap_points: (out) + * + * Gets the snap points of @self, representing the points between each page, + * before the first page and after the last page. + * + * Returns: (array length=n_snap_points) (transfer full): the snap points of @self + * + * Since: 1.0 + */ +gdouble * +hdy_carousel_box_get_snap_points (HdyCarouselBox *self, + gint *n_snap_points) +{ + guint i, n_pages; + gdouble *points; + GList *l; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + + n_pages = MAX (g_list_length (self->children), 1); + + points = g_new0 (gdouble, n_pages); + + i = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + points[i++] = info->snap_point; + } + + if (n_snap_points) + *n_snap_points = n_pages; + + return points; +} + +/** + * hdy_carousel_box_get_range: + * @self: a #HdyCarouselBox + * @lower: (out) (optional): location to store the lowest possible position, or %NULL + * @upper: (out) (optional): location to store the maximum possible position, or %NULL + * + * Gets the range of possible positions. + * + * Since: 1.0 + */ +void +hdy_carousel_box_get_range (HdyCarouselBox *self, + gdouble *lower, + gdouble *upper) +{ + GList *l; + HdyCarouselBoxChildInfo *child; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + l = g_list_last (self->children); + child = l ? l->data : NULL; + + if (lower) + *lower = 0; + + if (upper) + *upper = child ? child->snap_point : 0; +} + +/** + * hdy_carousel_box_get_closest_snap_point: + * @self: a #HdyCarouselBox + * + * Gets the snap point closest to the current position. + * + * Returns: the closest snap point. + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self) +{ + HdyCarouselBoxChildInfo *closest_child; + + closest_child = get_closest_child_at (self, self->position, TRUE, TRUE); + + if (!closest_child) + return 0; + + return closest_child->snap_point; +} + +/** + * hdy_carousel_box_get_page_at_position: + * @self: a #HdyCarouselBox + * @position: a scroll position + * + * Gets the page closest to @position. For example, if @position matches + * the current position, the returned widget will match the currently + * displayed page. + * + * Returns: the closest page. + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_get_page_at_position (HdyCarouselBox *self, + gdouble position) +{ + gdouble lower, upper; + HdyCarouselBoxChildInfo *child; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + + hdy_carousel_box_get_range (self, &lower, &upper); + + position = CLAMP (position, lower, upper); + + child = get_closest_child_at (self, position, TRUE, FALSE); + + return child->widget; +} + +/** + * hdy_carousel_box_get_current_page_index: + * @self: a #HdyCarouselBox + * + * Gets the index of the currently displayed page. + * + * Returns: the index of the current page. + * + * Since: 1.0 + */ +gint +hdy_carousel_box_get_current_page_index (HdyCarouselBox *self) +{ + GtkWidget *child; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + child = hdy_carousel_box_get_page_at_position (self, self->position); + + return find_child_index (self, child, FALSE); +} |