/* bge-carousel.c * * Copyright 2026 Eva M * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * BgeCarousel: * * Arranges widgets into a horizontal carousel */ #define G_LOG_DOMAIN "BGE::CAROUSEL" #include "bge.h" #include "bge-marshalers.h" #include "util.h" #define RAISE_FACTOR 0.025 /* `ratio < 1.0` means it overshoots */ #define ANIMATION_DAMPING_RATIO 1.15 struct _BgeCarousel { GtkWidget parent_instance; GtkEventController *motion; double motion_x; double motion_y; GtkEventController *scroll; gboolean scrolling; int hscroll_start; int hscroll_current; GtkGesture *drag; gboolean dragging; double drag_x; double drag_y; BgeAnimation *animation; gboolean allow_mouse_drag; gboolean allow_overshoot; gboolean allow_scroll_wheel; gboolean allow_raise; GtkSingleSelection *model; GPtrArray *mirror; GPtrArray *widgets; }; G_DEFINE_FINAL_TYPE (BgeCarousel, bge_carousel, GTK_TYPE_WIDGET); enum { PROP_0, PROP_ALLOW_MOUSE_DRAG, PROP_ALLOW_OVERSHOOT, PROP_ALLOW_SCROLL_WHEEL, PROP_ALLOW_RAISE, PROP_MODEL, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; enum { SIGNAL_CREATE_WIDGET, SIGNAL_REMOVE_WIDGET, LAST_SIGNAL, }; static guint signals[LAST_SIGNAL]; BGE_DEFINE_DATA ( carousel_widget, CarouselWidget, { GtkWidget *widget; int width; graphene_rect_t rect; graphene_rect_t target; gboolean raised; GCancellable *cancellable; }, BGE_RELEASE_DATA (widget, gtk_widget_unparent); BGE_RELEASE_DATA (cancellable, g_object_unref)) static void items_changed (BgeCarousel *self, guint position, guint removed, guint added, GListModel *model); static void model_selected_changed (BgeCarousel *self, GParamSpec *pspec, GtkSingleSelection *selection); static void move_to_idx (BgeCarousel *self, guint idx, /* damping_ratio <= 0.0 means no animation */ double damping_ratio); static void animate (BgeCarousel *self, const char *key, double value, CarouselWidgetData *data); static void ensure_viewport (BgeCarousel *self, GtkSingleSelection *model, gboolean animate); static void motion_enter (BgeCarousel *self, gdouble x, gdouble y, GtkEventControllerMotion *controller); static void motion_event (BgeCarousel *self, gdouble x, gdouble y, GtkEventControllerMotion *controller); static void motion_leave (BgeCarousel *self, GtkEventControllerMotion *controller); static void update_motion (BgeCarousel *self, gdouble x, gdouble y); static void scroll_begin (BgeCarousel *self, GtkEventControllerScroll *controller); static void scroll_end (BgeCarousel *self, GtkEventControllerScroll *controller); static gboolean scroll (BgeCarousel *self, gdouble dx, gdouble dy, GtkEventControllerScroll *controller); static void drag_begin (BgeCarousel *self, gdouble start_x, gdouble start_y, GtkGestureDrag *gesture); static void drag_end (BgeCarousel *self, gdouble offset_x, gdouble offset_y, GtkGestureDrag *gesture); static void drag_update (BgeCarousel *self, gdouble offset_x, gdouble offset_y, GtkGestureDrag *gesture); static void finish_horizontal_gesture (BgeCarousel *self, int offset_x, int offset_y); static void bge_carousel_dispose (GObject *object) { BgeCarousel *self = BGE_CAROUSEL (object); if (self->model != NULL) { g_signal_handlers_disconnect_by_func (self->model, items_changed, self); g_signal_handlers_disconnect_by_func (self->model, model_selected_changed, self); } g_clear_pointer (&self->animation, g_object_unref); g_clear_pointer (&self->model, g_object_unref); g_clear_pointer (&self->mirror, g_ptr_array_unref); g_clear_pointer (&self->widgets, g_ptr_array_unref); G_OBJECT_CLASS (bge_carousel_parent_class)->dispose (object); } static void bge_carousel_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BgeCarousel *self = BGE_CAROUSEL (object); switch (prop_id) { case PROP_ALLOW_MOUSE_DRAG: g_value_set_boolean (value, bge_carousel_get_allow_mouse_drag (self)); break; case PROP_ALLOW_OVERSHOOT: g_value_set_boolean (value, bge_carousel_get_allow_overshoot (self)); break; case PROP_ALLOW_SCROLL_WHEEL: g_value_set_boolean (value, bge_carousel_get_allow_scroll_wheel (self)); break; case PROP_ALLOW_RAISE: g_value_set_boolean (value, bge_carousel_get_allow_raise (self)); break; case PROP_MODEL: g_value_set_object (value, bge_carousel_get_model (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bge_carousel_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BgeCarousel *self = BGE_CAROUSEL (object); switch (prop_id) { case PROP_ALLOW_MOUSE_DRAG: bge_carousel_set_allow_mouse_drag (self, g_value_get_boolean (value)); break; case PROP_ALLOW_OVERSHOOT: bge_carousel_set_allow_overshoot (self, g_value_get_boolean (value)); break; case PROP_ALLOW_SCROLL_WHEEL: bge_carousel_set_allow_scroll_wheel (self, g_value_get_boolean (value)); break; case PROP_ALLOW_RAISE: bge_carousel_set_allow_raise (self, g_value_get_boolean (value)); break; case PROP_MODEL: bge_carousel_set_model (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bge_carousel_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { BgeCarousel *self = BGE_CAROUSEL (widget); for (guint i = 0; i < self->widgets->len; i++) { CarouselWidgetData *child = NULL; int tmp_minimum = 0; int tmp_natural = 0; int tmp_minimum_baseline = 0; int tmp_natural_baseline = 0; child = g_ptr_array_index (self->widgets, i); gtk_widget_measure ( child->widget, orientation, for_size, &tmp_minimum, &tmp_natural, &tmp_minimum_baseline, &tmp_natural_baseline); if (tmp_minimum > 0 && tmp_minimum > *minimum) *minimum = tmp_minimum; if (tmp_natural > 0 && tmp_natural > *natural) *natural = tmp_natural; if (tmp_minimum_baseline > 0 && tmp_minimum_baseline > *minimum_baseline) *minimum_baseline = tmp_minimum_baseline; if (tmp_natural_baseline > 0 && tmp_natural_baseline > *natural_baseline) *natural_baseline = tmp_natural_baseline; } } static void bge_carousel_size_allocate (GtkWidget *widget, int width, int height, int baseline) { BgeCarousel *self = BGE_CAROUSEL (widget); ensure_viewport (self, self->model, FALSE); for (guint i = 0; i < self->widgets->len; i++) { CarouselWidgetData *child = NULL; g_autoptr (GskTransform) transform = NULL; child = g_ptr_array_index (self->widgets, i); transform = gsk_transform_translate ( gsk_transform_new (), &child->rect.origin); gtk_widget_allocate ( child->widget, child->rect.size.width, child->rect.size.height, baseline, g_steal_pointer (&transform)); } } static void bge_carousel_class_init (BgeCarouselClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->set_property = bge_carousel_set_property; object_class->get_property = bge_carousel_get_property; object_class->dispose = bge_carousel_dispose; /** * BgeCarousel:allow-mouse-drag: * * Whether to allow dragging with the mouse. */ props[PROP_ALLOW_MOUSE_DRAG] = g_param_spec_boolean ( "allow-mouse-drag", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeCarousel:allow-overshoot: * * Whether to allow overshooting the ends of the carousel with drag/touchpad * input. Once the event completes, the carousel offset value will go back to * that of the start/end widget. Setting this value to FALSE will prevent this * widget from capturing input events which would result in an overshoot. */ props[PROP_ALLOW_OVERSHOOT] = g_param_spec_boolean ( "allow-overshoot", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeCarousel:allow-scroll-wheel: * * Whether to allow moving the carousel contents with the horizontal scroll * wheel. */ props[PROP_ALLOW_SCROLL_WHEEL] = g_param_spec_boolean ( "allow-scroll-wheel", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeCarousel:allow-raise: * * Whether to allow raise animations when motion input events hover over them * carousel widgets. */ props[PROP_ALLOW_RAISE] = g_param_spec_boolean ( "allow-raise", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * BgeCarousel:model: * * The selection model for the carousel contents. */ props[PROP_MODEL] = g_param_spec_object ( "model", NULL, NULL, GTK_TYPE_SINGLE_SELECTION, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); /** * BgeCarousel::create-widget: * @carousel: the object that received the signal * @object: a list item object from [property@Bge.Carousel:model] * * Emitted when an object is being bound to the carousel * * Return: a newly allocated widget to add to the carousel */ signals[SIGNAL_CREATE_WIDGET] = g_signal_new ( "create-widget", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, bge_marshal_OBJECT__OBJECT, GTK_TYPE_WIDGET, 1, G_TYPE_OBJECT); g_signal_set_va_marshaller ( signals[SIGNAL_CREATE_WIDGET], G_TYPE_FROM_CLASS (klass), bge_marshal_OBJECT__OBJECTv); /** * BgeCarousel::remove-widget: * @carousel: the object that received the signal * @widget: the widget which was created by [signal@Bge.Carousel::create-widget] * @object: a list item object from [property@Bge.Carousel:model] * * Emitted when an object is being unbound from the carousel */ signals[SIGNAL_REMOVE_WIDGET] = g_signal_new ( "remove-widget", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, bge_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, GTK_TYPE_WIDGET, G_TYPE_OBJECT); g_signal_set_va_marshaller ( signals[SIGNAL_REMOVE_WIDGET], G_TYPE_FROM_CLASS (klass), bge_marshal_VOID__OBJECT_OBJECTv); widget_class->measure = bge_carousel_measure; widget_class->size_allocate = bge_carousel_size_allocate; } static void bge_carousel_init (BgeCarousel *self) { self->animation = bge_animation_new (GTK_WIDGET (self)); self->mirror = g_ptr_array_new_with_free_func ( (GDestroyNotify) g_object_unref); self->widgets = g_ptr_array_new_with_free_func ( carousel_widget_data_unref); gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN); self->motion = gtk_event_controller_motion_new (); g_signal_connect_swapped (self->motion, "enter", G_CALLBACK (motion_enter), self); g_signal_connect_swapped (self->motion, "motion", G_CALLBACK (motion_event), self); g_signal_connect_swapped (self->motion, "leave", G_CALLBACK (motion_leave), self); gtk_widget_add_controller (GTK_WIDGET (self), self->motion); self->scroll = gtk_event_controller_scroll_new ( GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL | GTK_EVENT_CONTROLLER_SCROLL_KINETIC); g_signal_connect_swapped (self->scroll, "scroll-begin", G_CALLBACK (scroll_begin), self); g_signal_connect_swapped (self->scroll, "scroll-end", G_CALLBACK (scroll_end), self); g_signal_connect_swapped (self->scroll, "scroll", G_CALLBACK (scroll), self); gtk_widget_add_controller (GTK_WIDGET (self), self->scroll); self->drag = gtk_gesture_drag_new (); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->drag), GTK_PHASE_CAPTURE); g_signal_connect_swapped (self->drag, "drag-begin", G_CALLBACK (drag_begin), self); g_signal_connect_swapped (self->drag, "drag-end", G_CALLBACK (drag_end), self); g_signal_connect_swapped (self->drag, "drag-update", G_CALLBACK (drag_update), self); gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->drag)); } /** * bge_carousel_new: * * Creates a new `BgeCarousel` object. * * Returns: The newly created `BgeCarousel` object. */ GtkWidget * bge_carousel_new (void) { return g_object_new (BGE_TYPE_CAROUSEL, NULL); } /** * bge_carousel_get_allow_mouse_drag: * @self: a `BgeCarousel` * * Gets [property@Bge.Carousel:allow-mouse-drag]. * * Returns: the value of the property */ gboolean bge_carousel_get_allow_mouse_drag (BgeCarousel *self) { g_return_val_if_fail (BGE_IS_CAROUSEL (self), FALSE); return self->allow_mouse_drag; } /** * bge_carousel_get_allow_overshoot: * @self: a `BgeCarousel` * * Gets [property@Bge.Carousel:allow-overshoot]. * * Returns: the value of the property */ gboolean bge_carousel_get_allow_overshoot (BgeCarousel *self) { g_return_val_if_fail (BGE_IS_CAROUSEL (self), FALSE); return self->allow_overshoot; } /** * bge_carousel_get_allow_scroll_wheel: * @self: a `BgeCarousel` * * Gets [property@Bge.Carousel:allow-scroll-wheel]. * * Returns: the value of the property */ gboolean bge_carousel_get_allow_scroll_wheel (BgeCarousel *self) { g_return_val_if_fail (BGE_IS_CAROUSEL (self), FALSE); return self->allow_scroll_wheel; } /** * bge_carousel_get_allow_raise: * @self: a `BgeCarousel` * * Gets [property@Bge.Carousel:allow-raise]. * * Returns: the value of the property */ gboolean bge_carousel_get_allow_raise (BgeCarousel *self) { g_return_val_if_fail (BGE_IS_CAROUSEL (self), FALSE); return self->allow_raise; } /** * bge_carousel_get_model: * @self: a `BgeCarousel` * * Gets [property@Bge.Carousel:model]. * * Returns: (nullable): the value of the property */ GtkSingleSelection * bge_carousel_get_model (BgeCarousel *self) { g_return_val_if_fail (BGE_IS_CAROUSEL (self), NULL); return self->model; } /** * bge_carousel_get_nth_page: * @self: a `BgeCarousel` * @index: Index of the page. * * Returns: (nullable) (transfer none): the page at @index, or NULL if out of bounds */ GtkWidget * bge_carousel_get_nth_page (BgeCarousel *self, guint index) { CarouselWidgetData *data = NULL; g_return_val_if_fail (BGE_IS_CAROUSEL (self), NULL); if (index >= self->widgets->len) return NULL; data = g_ptr_array_index (self->widgets, index); return data->widget; } /** * bge_carousel_set_allow_mouse_drag: * @self: a `BgeCarousel` * @allow_mouse_drag: a boolean * * Sets [property@Bge.Carousel:allow-mouse-drag]. */ void bge_carousel_set_allow_mouse_drag (BgeCarousel *self, gboolean allow_mouse_drag) { g_return_if_fail (BGE_IS_CAROUSEL (self)); if (!!allow_mouse_drag == !!self->allow_mouse_drag) return; self->allow_mouse_drag = allow_mouse_drag; if (!allow_mouse_drag && self->dragging) { self->dragging = FALSE; finish_horizontal_gesture (self, self->drag_x, self->drag_y); self->drag_x = 0.0; self->drag_y = 0.0; } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]); } /** * bge_carousel_set_allow_overshoot: * @self: a `BgeCarousel` * @allow_overshoot: a boolean * * Sets [property@Bge.Carousel:allow-overshoot]. */ void bge_carousel_set_allow_overshoot (BgeCarousel *self, gboolean allow_overshoot) { g_return_if_fail (BGE_IS_CAROUSEL (self)); if (!!allow_overshoot == !!self->allow_overshoot) return; self->allow_overshoot = allow_overshoot; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_OVERSHOOT]); } /** * bge_carousel_set_allow_scroll_wheel: * @self: a `BgeCarousel` * @allow_scroll_wheel: a boolean * * Sets [property@Bge.Carousel:allow-scroll-wheel]. */ void bge_carousel_set_allow_scroll_wheel (BgeCarousel *self, gboolean allow_scroll_wheel) { g_return_if_fail (BGE_IS_CAROUSEL (self)); if (!!allow_scroll_wheel == !!self->allow_scroll_wheel) return; self->allow_scroll_wheel = allow_scroll_wheel; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_SCROLL_WHEEL]); } /** * bge_carousel_set_allow_raise: * @self: a `BgeCarousel` * @allow_raise: a boolean * * Sets [property@Bge.Carousel:allow-raise]. */ void bge_carousel_set_allow_raise (BgeCarousel *self, gboolean allow_raise) { g_return_if_fail (BGE_IS_CAROUSEL (self)); if (!!allow_raise == !!self->allow_raise) return; self->allow_raise = allow_raise; if (self->model != NULL) ensure_viewport (self, self->model, TRUE); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_RAISE]); } /** * bge_carousel_set_model: * @self: a `BgeCarousel` * @model: a `GtkSingleSelection` object * * Sets [property@Bge.Carousel:model]. */ void bge_carousel_set_model (BgeCarousel *self, GtkSingleSelection *model) { g_return_if_fail (BGE_IS_CAROUSEL (self)); g_return_if_fail (model == NULL || GTK_IS_SINGLE_SELECTION (model)); if (model == self->model) return; if (self->model != NULL) { g_signal_handlers_disconnect_by_func (self->model, items_changed, self); g_signal_handlers_disconnect_by_func (self->model, model_selected_changed, self); items_changed ( self, 0, g_list_model_get_n_items (G_LIST_MODEL (self->model)), 0, G_LIST_MODEL (self->model)); } g_clear_pointer (&self->model, g_object_unref); if (model != NULL) { self->model = g_object_ref (model); items_changed ( self, 0, 0, g_list_model_get_n_items (G_LIST_MODEL (model)), G_LIST_MODEL (model)); g_signal_connect_swapped ( model, "items-changed", G_CALLBACK (items_changed), self); g_signal_connect_swapped ( model, "notify::selected", G_CALLBACK (model_selected_changed), self); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODEL]); } static void items_changed (BgeCarousel *self, guint position, guint removed, guint added, GListModel *model) { for (guint i = 0; i < removed; i++) { GObject *object = NULL; CarouselWidgetData *child = NULL; object = g_ptr_array_index (self->mirror, position + i); child = g_ptr_array_index (self->widgets, position + i); if (child->cancellable != NULL) g_cancellable_cancel (child->cancellable); g_signal_emit (self, signals[SIGNAL_REMOVE_WIDGET], 0, child->widget, object); } if (removed > 0) { g_ptr_array_remove_range (self->mirror, position, removed); g_ptr_array_remove_range (self->widgets, position, removed); } for (guint i = 0; i < added; i++) { g_autoptr (GObject) object = NULL; GtkWidget *child = NULL; CarouselWidgetData *data = NULL; object = g_list_model_get_item (model, position + i); g_signal_emit (self, signals[SIGNAL_CREATE_WIDGET], 0, object, &child); if (child == NULL) { g_critical ("Failed to populate child for carousel widget"); child = gtk_fixed_new (); } if (position + i == 0) gtk_widget_set_parent (child, GTK_WIDGET (self)); else { CarouselWidgetData *prev = NULL; prev = g_ptr_array_index (self->widgets, position + i - 1); gtk_widget_insert_after (child, GTK_WIDGET (self), prev->widget); } data = carousel_widget_data_new (); data->widget = child; g_ptr_array_insert (self->mirror, position + i, g_object_ref (object)); g_ptr_array_insert (self->widgets, position + i, data); } ensure_viewport (self, GTK_SINGLE_SELECTION (model), FALSE); } static void model_selected_changed (BgeCarousel *self, GParamSpec *pspec, GtkSingleSelection *selection) { guint idx = 0; idx = gtk_single_selection_get_selected (selection); if (idx != G_MAXUINT) move_to_idx (self, idx, ANIMATION_DAMPING_RATIO); gtk_widget_queue_allocate (GTK_WIDGET (self)); } static void move_to_idx (BgeCarousel *self, guint idx, /* damping_ratio <= 0.0 means no animation */ double damping_ratio) { int width = 0; int height = 0; int offset = 0; width = gtk_widget_get_width (GTK_WIDGET (self)); height = gtk_widget_get_height (GTK_WIDGET (self)); if (width == 0 || height == 0) { gtk_widget_queue_allocate (GTK_WIDGET (self)); return; } offset = width / 2; if (self->scrolling) offset += self->hscroll_start - self->hscroll_current; if (self->dragging) offset += self->drag_x; for (guint i = 0; i <= idx; i++) { CarouselWidgetData *child = NULL; int hminimum = 0; int hnatural = 0; int unused = 0; int child_width = 0; child = g_ptr_array_index (self->widgets, i); gtk_widget_measure ( child->widget, GTK_ORIENTATION_HORIZONTAL, height, &hminimum, &hnatural, &unused, &unused); if (gtk_widget_get_hexpand (child->widget)) child_width = MAX (hminimum, width); else child_width = CLAMP (hnatural, hminimum, width); if (i == idx) offset -= child_width / 2; else offset -= child_width; } for (guint i = 0; i < self->widgets->len; i++) { CarouselWidgetData *child = NULL; int hminimum = 0; int hnatural = 0; int unused = 0; int rect_width = 0; int child_width = 0; int child_height = 0; int child_x = 0; int child_y = 0; graphene_rect_t target = { 0 }; gboolean avoid_animation = FALSE; child = g_ptr_array_index (self->widgets, i); gtk_widget_measure ( child->widget, GTK_ORIENTATION_HORIZONTAL, height, &hminimum, &hnatural, &unused, &unused); if (gtk_widget_get_hexpand (child->widget)) rect_width = MAX (hminimum, width); else rect_width = CLAMP (hnatural, hminimum, width); if (child->raised || !self->allow_raise) { child_width = rect_width; child_height = height; child_x = offset; child_y = 0; } else { child_height = round ((double) height * (1.0 - RAISE_FACTOR)); gtk_widget_measure ( child->widget, GTK_ORIENTATION_HORIZONTAL, child_height, &hminimum, &hnatural, &unused, &unused); child_width = CLAMP (hnatural, hminimum, width); child_x = offset + round ((double) (rect_width - child_width) * 0.5); child_y = round ((double) height * (0.5 * RAISE_FACTOR)); } target = GRAPHENE_RECT_INIT (child_x, child_y, child_width, child_height); avoid_animation = graphene_rect_equal (&target, &child->target); if (child->width != rect_width || (damping_ratio < 0.0 && !avoid_animation)) { if (child->cancellable != NULL) g_cancellable_cancel (child->cancellable); child->width = rect_width; child->rect = target; child->target = target; } else if (avoid_animation) child->target = target; else { char buf[64] = { 0 }; g_clear_object (&child->cancellable); child->cancellable = g_cancellable_new (); #define MASS 1.0 #define STIFFNESS 800.0 /* pointer is to ensure a unique identifier so as not to overwrite any other child's key */ g_snprintf (buf, sizeof (buf), "x%p", child); bge_animation_add_spring ( self->animation, buf, child->rect.origin.x, target.origin.x, damping_ratio, MASS, STIFFNESS, (BgeAnimationCallback) animate, carousel_widget_data_ref (child), carousel_widget_data_unref, child->cancellable); g_snprintf (buf, sizeof (buf), "y%p", child); bge_animation_add_spring ( self->animation, buf, child->rect.origin.y, target.origin.y, damping_ratio, MASS, STIFFNESS, (BgeAnimationCallback) animate, carousel_widget_data_ref (child), carousel_widget_data_unref, child->cancellable); g_snprintf (buf, sizeof (buf), "w%p", child); bge_animation_add_spring ( self->animation, buf, child->rect.size.width, target.size.width, damping_ratio, MASS, STIFFNESS, (BgeAnimationCallback) animate, carousel_widget_data_ref (child), carousel_widget_data_unref, child->cancellable); g_snprintf (buf, sizeof (buf), "h%p", child); bge_animation_add_spring ( self->animation, buf, child->rect.size.height, target.size.height, damping_ratio, MASS, STIFFNESS, (BgeAnimationCallback) animate, carousel_widget_data_ref (child), carousel_widget_data_unref, child->cancellable); #undef STIFFNESS #undef MASS child->target = target; } offset += rect_width; } gtk_widget_queue_allocate (GTK_WIDGET (self)); } static void animate (BgeCarousel *self, const char *key, double value, CarouselWidgetData *data) { switch (*key) { case 'x': data->rect.origin.x = value; break; case 'y': data->rect.origin.y = value; break; case 'w': data->rect.size.width = value; break; case 'h': data->rect.size.height = value; break; default: g_assert_not_reached (); } gtk_widget_queue_allocate (GTK_WIDGET (self)); } static void ensure_viewport (BgeCarousel *self, GtkSingleSelection *model, gboolean animate) { guint n_items = 0; n_items = g_list_model_get_n_items (G_LIST_MODEL (model)); if (n_items > 0) { guint selected = 0; selected = gtk_single_selection_get_selected (model); if (selected == G_MAXUINT) { gtk_single_selection_set_selected (model, 0); move_to_idx (self, 0, animate ? ANIMATION_DAMPING_RATIO : -1.0); } else move_to_idx (self, selected, animate ? ANIMATION_DAMPING_RATIO : -1.0); } gtk_widget_queue_allocate (GTK_WIDGET (self)); } static void motion_enter (BgeCarousel *self, gdouble x, gdouble y, GtkEventControllerMotion *controller) { self->motion_x = x; self->motion_y = y; update_motion (self, x, y); } static void motion_event (BgeCarousel *self, gdouble x, gdouble y, GtkEventControllerMotion *controller) { self->motion_x = x; self->motion_y = y; update_motion (self, x, y); } static void motion_leave (BgeCarousel *self, GtkEventControllerMotion *controller) { if (self->dragging) return; self->motion_x = -1.0; self->motion_y = -1.0; update_motion (self, -1.0, -1.0); } static void update_motion (BgeCarousel *self, gdouble x, gdouble y) { graphene_point_t point = { 0 }; gboolean ensure = FALSE; point = GRAPHENE_POINT_INIT (x, y); if (self->scrolling) return; for (guint i = 0; i < self->widgets->len; i++) { CarouselWidgetData *child = NULL; gboolean contained = FALSE; child = g_ptr_array_index (self->widgets, i); contained = graphene_rect_contains_point (&child->target, &point); if (!!contained != !!child->raised) { child->raised = contained; ensure = TRUE; } } if (self->dragging) return; if (ensure) ensure_viewport (self, self->model, TRUE); } static void scroll_begin (BgeCarousel *self, GtkEventControllerScroll *controller) { self->scrolling = TRUE; self->hscroll_start = self->motion_x; self->hscroll_current = self->motion_x; } static void scroll_end (BgeCarousel *self, GtkEventControllerScroll *controller) { self->scrolling = FALSE; finish_horizontal_gesture ( self, self->hscroll_start - self->hscroll_current, 0); self->hscroll_start = -1; self->hscroll_current = -1; update_motion (self, self->motion_x, self->motion_y); } static gboolean scroll (BgeCarousel *self, gdouble dx, gdouble dy, GtkEventControllerScroll *controller) { guint n_items = 0; GdkDevice *device = NULL; GdkInputSource source = 0; if (self->model == NULL) { self->scrolling = FALSE; return FALSE; } n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model)); if (n_items == 0) { self->scrolling = FALSE; return FALSE; } device = gtk_event_controller_get_current_event_device ( GTK_EVENT_CONTROLLER (controller)); source = gdk_device_get_source (device); switch (source) { case GDK_SOURCE_TOUCHPAD: case GDK_SOURCE_TRACKPOINT: { if (self->widgets->len > 0 && !self->allow_overshoot) { guint selection = 0; selection = gtk_single_selection_get_selected (self->model); if ((selection == 0 && self->hscroll_current + dx < 0) || (selection == self->widgets->len - 1 && self->hscroll_current + dx > 0)) { self->scrolling = FALSE; return FALSE; } } self->hscroll_current += dx; ensure_viewport (self, self->model, FALSE); } break; case GDK_SOURCE_MOUSE: case GDK_SOURCE_PEN: case GDK_SOURCE_KEYBOARD: case GDK_SOURCE_TOUCHSCREEN: case GDK_SOURCE_TABLET_PAD: default: { guint selected = 0; guint new_selected = 0; if (!self->allow_scroll_wheel) break; selected = gtk_single_selection_get_selected (self->model); if (dx > 0) new_selected = MIN (selected + 1, n_items - 1); else { if (selected == 0) new_selected = 0; else new_selected = selected - 1; } gtk_single_selection_set_selected (self->model, new_selected); } break; } return TRUE; } static void drag_begin (BgeCarousel *self, gdouble start_x, gdouble start_y, GtkGestureDrag *gesture) { if (!self->allow_mouse_drag) return; self->dragging = TRUE; if (self->model == NULL) return; ensure_viewport (self, self->model, TRUE); } static void drag_end (BgeCarousel *self, gdouble offset_x, gdouble offset_y, GtkGestureDrag *gesture) { if (!self->dragging) /* This situation will happen if the `allow-mouse-drag` prop is set to FALSE while a drag is taking place */ return; self->dragging = FALSE; finish_horizontal_gesture (self, self->drag_x, self->drag_y); self->drag_x = 0.0; self->drag_y = 0.0; if (offset_x < -3 || offset_x > 3 || offset_y < -3 || offset_y > 3) gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } static void drag_update (BgeCarousel *self, gdouble offset_x, gdouble offset_y, GtkGestureDrag *gesture) { if (!self->dragging) return; self->drag_x = offset_x; self->drag_y = offset_y; if (self->model == NULL) return; if (self->widgets->len > 0 && !self->allow_overshoot) { guint selected = 0; selected = gtk_single_selection_get_selected (self->model); if (selected == 0) self->drag_x = MIN (self->drag_x, 0.0); if (selected == self->widgets->len - 1) self->drag_x = MAX (self->drag_x, 0.0); } ensure_viewport (self, self->model, FALSE); } static void finish_horizontal_gesture (BgeCarousel *self, int offset_x, int offset_y) { guint selected = 0; double width = 0.0; guint new_selected = G_MAXUINT; int min_distance = G_MAXINT; if (self->model == NULL || self->widgets->len == 0) return; if (offset_x == 0) return; selected = gtk_single_selection_get_selected (self->model); width = gtk_widget_get_width (GTK_WIDGET (self)); for (guint i = 0; i < self->widgets->len; i++) { CarouselWidgetData *child = NULL; int left_distance = 0; int right_distance = 0; int distance_from_center = 0; child = g_ptr_array_index (self->widgets, i); if (child->rect.origin.x > width / 2.0) left_distance = child->rect.origin.x - width / 2.0; else left_distance = width / 2.0 - child->rect.origin.x; if ((child->rect.origin.x + child->rect.size.width) > width / 2.0) right_distance = (child->rect.origin.x + child->rect.size.width) - width / 2.0; else right_distance = width / 2.0 - (child->rect.origin.x + child->rect.size.width); distance_from_center = MIN (left_distance, right_distance); if (distance_from_center < min_distance) { new_selected = i; min_distance = distance_from_center; } } if (new_selected == selected) { /* Ensure dragging is not too stiff; meaning if we drag the content at least 15 pixels in either direction, it will automatically snap to the next widget */ if (offset_x > 15 && selected > 0) new_selected--; else if (offset_x < -15 && selected < self->widgets->len - 1) new_selected++; } if (new_selected == G_MAXUINT || new_selected == selected) ensure_viewport (self, self->model, TRUE); else gtk_single_selection_set_selected (self->model, new_selected); } /* End of bge-carousel.c */