1 /*
2  * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
3  *
4  * SPDX-License-Identifier: LGPL-2.1-or-later
5  */
6 
7 #include "config.h"
8 
9 #include "adw-carousel-indicator-dots.h"
10 
11 #include "adw-animation-util-private.h"
12 #include "adw-animation-private.h"
13 #include "adw-swipeable.h"
14 
15 #include <math.h>
16 
17 #define DOTS_RADIUS 3
18 #define DOTS_RADIUS_SELECTED 4
19 #define DOTS_OPACITY 0.3
20 #define DOTS_OPACITY_SELECTED 0.9
21 #define DOTS_SPACING 7
22 #define DOTS_MARGIN 6
23 
24 /**
25  * AdwCarouselIndicatorDots:
26  *
27  * A dots indicator for [class@Adw.Carousel].
28  *
29  * The `AdwCarouselIndicatorDots` widget shows a set of dots for each page of a
30  * given [class@Adw.Carousel]. The dot representing the carousel's active page
31  * is larger and more opaque than the others, the transition to the active and
32  * inactive state is gradual to match the carousel's position.
33  *
34  * See also [class@Adw.CarouselIndicatorLines].
35  *
36  * ## CSS nodes
37  *
38  * `AdwCarouselIndicatorDots` has a single CSS node with name
39  * `carouselindicatordots`.
40  *
41  * Since: 1.0
42  */
43 
44 struct _AdwCarouselIndicatorDots
45 {
46   GtkWidget parent_instance;
47 
48   AdwCarousel *carousel;
49   GtkOrientation orientation;
50 
51   AdwAnimation *animation;
52 };
53 
54 G_DEFINE_TYPE_WITH_CODE (AdwCarouselIndicatorDots, adw_carousel_indicator_dots, GTK_TYPE_WIDGET,
55                          G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
56 
57 enum {
58   PROP_0,
59   PROP_CAROUSEL,
60 
61   /* GtkOrientable */
62   PROP_ORIENTATION,
63   LAST_PROP = PROP_CAROUSEL + 1,
64 };
65 
66 static GParamSpec *props[LAST_PROP];
67 
68 static void
value_cb(double value,GtkWidget * widget)69 value_cb (double     value,
70           GtkWidget *widget)
71 {
72   gtk_widget_queue_draw (widget);
73 }
74 
75 static void
done_cb(AdwCarouselIndicatorDots * self)76 done_cb (AdwCarouselIndicatorDots *self)
77 {
78   g_clear_object (&self->animation);
79 }
80 
81 static void
animate(AdwCarouselIndicatorDots * self,gint64 duration)82 animate (AdwCarouselIndicatorDots *self,
83          gint64                    duration)
84 {
85   if (self->animation)
86     adw_animation_stop (self->animation);
87 
88   self->animation =
89     adw_animation_new (GTK_WIDGET (self), 0, 1, duration,
90                        (AdwAnimationTargetFunc) value_cb,
91                        self);
92 
93   g_signal_connect_swapped (self->animation, "done", G_CALLBACK (done_cb), self);
94 
95   adw_animation_start (self->animation);
96 }
97 
98 static GdkRGBA
get_color(GtkWidget * widget)99 get_color (GtkWidget *widget)
100 {
101   GtkStyleContext *context;
102   GdkRGBA color;
103 
104   context = gtk_widget_get_style_context (widget);
105   gtk_style_context_get_color (context, &color);
106 
107   return color;
108 }
109 
110 static void
snapshot_dots(GtkWidget * widget,GtkSnapshot * snapshot,GtkOrientation orientation,double position,double * sizes,guint n_pages)111 snapshot_dots (GtkWidget      *widget,
112                GtkSnapshot    *snapshot,
113                GtkOrientation  orientation,
114                double          position,
115                double         *sizes,
116                guint           n_pages)
117 {
118   GdkRGBA color;
119   int i, widget_length, widget_thickness;
120   double x, y, indicator_length, dot_size, full_size;
121   double current_position, remaining_progress;
122   graphene_rect_t rect;
123 
124   color = get_color (widget);
125   dot_size = 2 * DOTS_RADIUS_SELECTED + DOTS_SPACING;
126 
127   indicator_length = -DOTS_SPACING;
128   for (i = 0; i < n_pages; i++)
129     indicator_length += dot_size * sizes[i];
130 
131   if (orientation == GTK_ORIENTATION_HORIZONTAL) {
132     widget_length = gtk_widget_get_width (widget);
133     widget_thickness = gtk_widget_get_height (widget);
134   } else {
135     widget_length = gtk_widget_get_height (widget);
136     widget_thickness = gtk_widget_get_width (widget);
137   }
138 
139   /* Ensure the indicators are aligned to pixel grid when not animating */
140   full_size = round (indicator_length / dot_size) * dot_size;
141   if ((widget_length - (int) full_size) % 2 == 0)
142     widget_length--;
143 
144   if (orientation == GTK_ORIENTATION_HORIZONTAL) {
145     x = (widget_length - indicator_length) / 2.0;
146     y = widget_thickness / 2;
147   } else {
148     x = widget_thickness / 2;
149     y = (widget_length - indicator_length) / 2.0;
150   }
151 
152   current_position = 0;
153   remaining_progress = 1;
154 
155   graphene_rect_init (&rect, -DOTS_RADIUS, -DOTS_RADIUS, DOTS_RADIUS * 2, DOTS_RADIUS * 2);
156 
157   for (i = 0; i < n_pages; i++) {
158     double progress, radius, opacity;
159     GskRoundedRect clip;
160 
161     if (orientation == GTK_ORIENTATION_HORIZONTAL)
162       x += dot_size * sizes[i] / 2.0;
163     else
164       y += dot_size * sizes[i] / 2.0;
165 
166     current_position += sizes[i];
167 
168     progress = CLAMP (current_position - position, 0, remaining_progress);
169     remaining_progress -= progress;
170 
171     radius = adw_lerp (DOTS_RADIUS, DOTS_RADIUS_SELECTED, progress) * sizes[i];
172     opacity = adw_lerp (DOTS_OPACITY, DOTS_OPACITY_SELECTED, progress) * sizes[i];
173 
174     gsk_rounded_rect_init_from_rect (&clip, &rect, radius);
175 
176     gtk_snapshot_save (snapshot);
177     gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x, y));
178     gtk_snapshot_scale (snapshot, radius / DOTS_RADIUS, radius / DOTS_RADIUS);
179 
180     gtk_snapshot_push_rounded_clip (snapshot, &clip);
181     gtk_snapshot_push_opacity (snapshot, opacity);
182 
183     gtk_snapshot_append_color (snapshot, &color, &rect);
184 
185     gtk_snapshot_pop (snapshot);
186     gtk_snapshot_pop (snapshot);
187 
188     gtk_snapshot_restore (snapshot);
189 
190     if (orientation == GTK_ORIENTATION_HORIZONTAL)
191       x += dot_size * sizes[i] / 2.0;
192     else
193       y += dot_size * sizes[i] / 2.0;
194   }
195 }
196 
197 static void
n_pages_changed_cb(AdwCarouselIndicatorDots * self)198 n_pages_changed_cb (AdwCarouselIndicatorDots *self)
199 {
200   animate (self, adw_carousel_get_reveal_duration (self->carousel));
201 }
202 
203 static void
adw_carousel_indicator_dots_measure(GtkWidget * widget,GtkOrientation orientation,int for_size,int * minimum,int * natural,int * minimum_baseline,int * natural_baseline)204 adw_carousel_indicator_dots_measure (GtkWidget      *widget,
205                                      GtkOrientation  orientation,
206                                      int             for_size,
207                                      int            *minimum,
208                                      int            *natural,
209                                      int            *minimum_baseline,
210                                      int            *natural_baseline)
211 {
212   AdwCarouselIndicatorDots *self = ADW_CAROUSEL_INDICATOR_DOTS (widget);
213   int size = 0;
214 
215   if (orientation == self->orientation) {
216     int n_pages = 0;
217     if (self->carousel)
218       n_pages = adw_carousel_get_n_pages (self->carousel);
219 
220     size = MAX (0, (2 * DOTS_RADIUS_SELECTED + DOTS_SPACING) * n_pages - DOTS_SPACING);
221   } else {
222     size = 2 * DOTS_RADIUS_SELECTED;
223   }
224 
225   size += 2 * DOTS_MARGIN;
226 
227   if (minimum)
228     *minimum = size;
229 
230   if (natural)
231     *natural = size;
232 
233   if (minimum_baseline)
234     *minimum_baseline = -1;
235 
236   if (natural_baseline)
237     *natural_baseline = -1;
238 }
239 
240 static void
adw_carousel_indicator_dots_snapshot(GtkWidget * widget,GtkSnapshot * snapshot)241 adw_carousel_indicator_dots_snapshot (GtkWidget   *widget,
242                                       GtkSnapshot *snapshot)
243 {
244   AdwCarouselIndicatorDots *self = ADW_CAROUSEL_INDICATOR_DOTS (widget);
245   int i, n_points;
246   double position;
247   g_autofree double *points = NULL;
248   g_autofree double *sizes = NULL;
249 
250   if (!self->carousel)
251     return;
252 
253   points = adw_swipeable_get_snap_points (ADW_SWIPEABLE (self->carousel), &n_points);
254   position = adw_carousel_get_position (self->carousel);
255 
256   if (n_points < 2)
257     return;
258 
259   if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
260       gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
261     position = points[n_points - 1] - position;
262 
263   sizes = g_new0 (double, n_points);
264 
265   sizes[0] = points[0] + 1;
266   for (i = 1; i < n_points; i++)
267     sizes[i] = points[i] - points[i - 1];
268 
269   snapshot_dots (widget, snapshot, self->orientation, position, sizes, n_points);
270 }
271 
272 static void
adw_carousel_dispose(GObject * object)273 adw_carousel_dispose (GObject *object)
274 {
275   AdwCarouselIndicatorDots *self = ADW_CAROUSEL_INDICATOR_DOTS (object);
276 
277   adw_carousel_indicator_dots_set_carousel (self, NULL);
278 
279   G_OBJECT_CLASS (adw_carousel_indicator_dots_parent_class)->dispose (object);
280 }
281 
282 static void
adw_carousel_indicator_dots_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)283 adw_carousel_indicator_dots_get_property (GObject    *object,
284                                           guint       prop_id,
285                                           GValue     *value,
286                                           GParamSpec *pspec)
287 {
288   AdwCarouselIndicatorDots *self = ADW_CAROUSEL_INDICATOR_DOTS (object);
289 
290   switch (prop_id) {
291   case PROP_CAROUSEL:
292     g_value_set_object (value, adw_carousel_indicator_dots_get_carousel (self));
293     break;
294 
295   case PROP_ORIENTATION:
296     g_value_set_enum (value, self->orientation);
297     break;
298 
299   default:
300     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
301   }
302 }
303 
304 static void
adw_carousel_indicator_dots_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)305 adw_carousel_indicator_dots_set_property (GObject      *object,
306                                           guint         prop_id,
307                                           const GValue *value,
308                                           GParamSpec   *pspec)
309 {
310   AdwCarouselIndicatorDots *self = ADW_CAROUSEL_INDICATOR_DOTS (object);
311 
312   switch (prop_id) {
313   case PROP_CAROUSEL:
314     adw_carousel_indicator_dots_set_carousel (self, g_value_get_object (value));
315     break;
316 
317   case PROP_ORIENTATION:
318     {
319       GtkOrientation orientation = g_value_get_enum (value);
320       if (orientation != self->orientation) {
321         self->orientation = orientation;
322         gtk_widget_queue_resize (GTK_WIDGET (self));
323         g_object_notify (G_OBJECT (self), "orientation");
324       }
325     }
326     break;
327 
328   default:
329     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
330   }
331 }
332 
333 static void
adw_carousel_indicator_dots_class_init(AdwCarouselIndicatorDotsClass * klass)334 adw_carousel_indicator_dots_class_init (AdwCarouselIndicatorDotsClass *klass)
335 {
336   GObjectClass *object_class = G_OBJECT_CLASS (klass);
337   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
338 
339   object_class->dispose = adw_carousel_dispose;
340   object_class->get_property = adw_carousel_indicator_dots_get_property;
341   object_class->set_property = adw_carousel_indicator_dots_set_property;
342 
343   widget_class->measure = adw_carousel_indicator_dots_measure;
344   widget_class->snapshot = adw_carousel_indicator_dots_snapshot;
345 
346   /**
347    * AdwCarouselIndicatorDots:carousel: (attributes org.gtk.Property.get=adw_carousel_indicator_dots_get_carousel org.gtk.Property.set=adw_carousel_indicator_dots_set_carousel)
348    *
349    * The displayed carousel.
350    *
351    * Since: 1.0
352    */
353   props[PROP_CAROUSEL] =
354     g_param_spec_object ("carousel",
355                          "Carousel",
356                          "The displayed carousel",
357                          ADW_TYPE_CAROUSEL,
358                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
359 
360   g_object_class_override_property (object_class,
361                                     PROP_ORIENTATION,
362                                     "orientation");
363 
364   g_object_class_install_properties (object_class, LAST_PROP, props);
365 
366   gtk_widget_class_set_css_name (widget_class, "carouselindicatordots");
367 }
368 
369 static void
adw_carousel_indicator_dots_init(AdwCarouselIndicatorDots * self)370 adw_carousel_indicator_dots_init (AdwCarouselIndicatorDots *self)
371 {
372 }
373 
374 /**
375  * adw_carousel_indicator_dots_new:
376  *
377  * Creates a new `AdwCarouselIndicatorDots`.
378  *
379  * Returns: the newly created `AdwCarouselIndicatorDots`
380  *
381  * Since: 1.0
382  */
383 GtkWidget *
adw_carousel_indicator_dots_new(void)384 adw_carousel_indicator_dots_new (void)
385 {
386   return g_object_new (ADW_TYPE_CAROUSEL_INDICATOR_DOTS, NULL);
387 }
388 
389 /**
390  * adw_carousel_indicator_dots_get_carousel: (attributes org.gtk.Method.get_property=carousel)
391  * @self: a `AdwCarouselIndicatorDots`
392  *
393  * Gets the displayed carousel.
394  *
395  * Returns: (nullable) (transfer none): the displayed carousel
396  *
397  * Since: 1.0
398  */
399 AdwCarousel *
adw_carousel_indicator_dots_get_carousel(AdwCarouselIndicatorDots * self)400 adw_carousel_indicator_dots_get_carousel (AdwCarouselIndicatorDots *self)
401 {
402   g_return_val_if_fail (ADW_IS_CAROUSEL_INDICATOR_DOTS (self), NULL);
403 
404   return self->carousel;
405 }
406 
407 /**
408  * adw_carousel_indicator_dots_set_carousel: (attributes org.gtk.Method.set_property=carousel)
409  * @self: a `AdwCarouselIndicatorDots`
410  * @carousel: (nullable): a carousel
411  *
412  * Sets the displayed carousel.
413  *
414  * Since: 1.0
415  */
416 void
adw_carousel_indicator_dots_set_carousel(AdwCarouselIndicatorDots * self,AdwCarousel * carousel)417 adw_carousel_indicator_dots_set_carousel (AdwCarouselIndicatorDots *self,
418                                           AdwCarousel              *carousel)
419 {
420   g_return_if_fail (ADW_IS_CAROUSEL_INDICATOR_DOTS (self));
421   g_return_if_fail (ADW_IS_CAROUSEL (carousel) || carousel == NULL);
422 
423   if (self->carousel == carousel)
424     return;
425 
426   if (self->animation)
427     adw_animation_stop (self->animation);
428 
429   if (self->carousel) {
430     g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self);
431     g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self);
432   }
433 
434   g_set_object (&self->carousel, carousel);
435 
436   if (self->carousel) {
437     g_signal_connect_object (self->carousel, "notify::position",
438                              G_CALLBACK (gtk_widget_queue_draw), self,
439                              G_CONNECT_SWAPPED);
440     g_signal_connect_object (self->carousel, "notify::n-pages",
441                              G_CALLBACK (n_pages_changed_cb), self,
442                              G_CONNECT_SWAPPED);
443   }
444 
445   gtk_widget_queue_draw (GTK_WIDGET (self));
446 
447   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]);
448 }
449