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