1/*
2 * SPDX-License-Identifier: LGPL-2.1-or-later
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17#include "config.h"
18
19#include "gtkcenterlayout.h"
20
21#include "gtkcsspositionvalueprivate.h"
22#include "gtkintl.h"
23#include "gtklayoutchild.h"
24#include "gtkprivate.h"
25#include "gtksizerequest.h"
26#include "gtkwidgetprivate.h"
27#include "gtkcssnodeprivate.h"
28
29/**
30 * GtkCenterLayout:
31 *
32 * `GtkCenterLayout` is a layout manager that manages up to three children.
33 *
34 * The start widget is allocated at the start of the layout (left in
35 * left-to-right locales and right in right-to-left ones), and the end
36 * widget at the end.
37 *
38 * The center widget is centered regarding the full width of the layout's.
39 */
40struct _GtkCenterLayout
41{
42 GtkLayoutManager parent_instance;
43
44 GtkBaselinePosition baseline_pos;
45 GtkOrientation orientation;
46
47 union {
48 struct {
49 GtkWidget *start_widget;
50 GtkWidget *center_widget;
51 GtkWidget *end_widget;
52 };
53 GtkWidget *children[3];
54 };
55};
56
57G_DEFINE_TYPE (GtkCenterLayout, gtk_center_layout, GTK_TYPE_LAYOUT_MANAGER)
58
59static int
60get_spacing (GtkCenterLayout *self,
61 GtkCssNode *node)
62{
63 GtkCssStyle *style = gtk_css_node_get_style (cssnode: node);
64 GtkCssValue *border_spacing;
65 int css_spacing;
66
67 border_spacing = style->size->border_spacing;
68 if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
69 css_spacing = _gtk_css_position_value_get_x (position: border_spacing, one_hundred_percent: 100);
70 else
71 css_spacing = _gtk_css_position_value_get_y (position: border_spacing, one_hundred_percent: 100);
72
73 return css_spacing;
74}
75
76static GtkSizeRequestMode
77gtk_center_layout_get_request_mode (GtkLayoutManager *layout_manager,
78 GtkWidget *widget)
79{
80 GtkCenterLayout *self = GTK_CENTER_LAYOUT (ptr: layout_manager);
81 int count[3] = { 0, 0, 0 };
82
83 if (self->start_widget)
84 count[gtk_widget_get_request_mode (widget: self->start_widget)]++;
85
86 if (self->center_widget)
87 count[gtk_widget_get_request_mode (widget: self->center_widget)]++;
88
89 if (self->end_widget)
90 count[gtk_widget_get_request_mode (widget: self->end_widget)]++;
91
92 if (!count[GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH] &&
93 !count[GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT])
94 return GTK_SIZE_REQUEST_CONSTANT_SIZE;
95 else
96 return count[GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT] > count[GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH]
97 ? GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT
98 : GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
99}
100
101static gboolean
102get_expand (GtkWidget *widget,
103 GtkOrientation orientation)
104{
105 if (orientation == GTK_ORIENTATION_HORIZONTAL)
106 return gtk_widget_get_hexpand (widget);
107 else
108 return gtk_widget_get_vexpand (widget);
109}
110
111static void
112gtk_center_layout_distribute (GtkCenterLayout *self,
113 int for_size,
114 int size,
115 int spacing,
116 GtkRequestedSize *sizes)
117{
118 int center_size = 0;
119 int start_size = 0;
120 int end_size = 0;
121 gboolean center_expand = FALSE;
122 gboolean start_expand = FALSE;
123 gboolean end_expand = FALSE;
124 int avail;
125 int i;
126 int needed_spacing = 0;
127
128 /* Usable space is really less... */
129 for (i = 0; i < 3; i++)
130 {
131 if (self->children[i])
132 needed_spacing += spacing;
133 }
134 needed_spacing -= spacing;
135
136 sizes[0].minimum_size = sizes[0].natural_size = 0;
137 sizes[1].minimum_size = sizes[1].natural_size = 0;
138 sizes[2].minimum_size = sizes[2].natural_size = 0;
139
140 for (i = 0; i < 3; i ++)
141 {
142 if (self->children[i])
143 gtk_widget_measure (widget: self->children[i], orientation: self->orientation, for_size,
144 minimum: &sizes[i].minimum_size, natural: &sizes[i].natural_size,
145 NULL, NULL);
146 }
147
148 if (self->center_widget)
149 {
150 center_size = CLAMP (size - needed_spacing - (sizes[0].minimum_size + sizes[2].minimum_size), sizes[1].minimum_size, sizes[1].natural_size);
151 center_expand = get_expand (widget: self->center_widget, orientation: self->orientation);
152 }
153
154 if (self->start_widget)
155 {
156 avail = MIN ((size - needed_spacing - center_size) / 2, size - needed_spacing - (center_size + sizes[2].minimum_size));
157 start_size = CLAMP (avail, sizes[0].minimum_size, sizes[0].natural_size);
158 start_expand = get_expand (widget: self->start_widget, orientation: self->orientation);
159 }
160
161 if (self->end_widget)
162 {
163 avail = MIN ((size - needed_spacing - center_size) / 2, size - needed_spacing - (center_size + sizes[0].minimum_size));
164 end_size = CLAMP (avail, sizes[2].minimum_size, sizes[2].natural_size);
165 end_expand = get_expand (widget: self->end_widget, orientation: self->orientation);
166 }
167
168 if (self->center_widget)
169 {
170 int center_pos;
171
172 center_pos = (size / 2) - (center_size / 2);
173
174 /* Push in from start/end */
175 if (start_size > 0 && start_size + spacing > center_pos)
176 center_pos = start_size + spacing;
177 else if (end_size > 0 && size - end_size - spacing < center_pos + center_size)
178 center_pos = size - center_size - end_size - spacing;
179 else if (center_expand)
180 {
181 center_size = size - 2 * (MAX (start_size, end_size) + spacing);
182 center_pos = (size / 2) - (center_size / 2) + spacing;
183 }
184
185 if (start_expand)
186 start_size = center_pos - spacing;
187
188 if (end_expand)
189 end_size = size - (center_pos + center_size) - spacing;
190 }
191 else
192 {
193 avail = size - needed_spacing - (start_size + end_size);
194 if (start_expand && end_expand)
195 {
196 start_size += avail / 2;
197 end_size += avail / 2;
198 }
199 else if (start_expand)
200 {
201 start_size += avail;
202 }
203 else if (end_expand)
204 {
205 end_size += avail;
206 }
207 }
208
209 sizes[0].minimum_size = start_size;
210 sizes[1].minimum_size = center_size;
211 sizes[2].minimum_size = end_size;
212}
213
214static void
215gtk_center_layout_measure_orientation (GtkCenterLayout *self,
216 GtkWidget *widget,
217 GtkOrientation orientation,
218 int for_size,
219 int *minimum,
220 int *natural,
221 int *minimum_baseline,
222 int *natural_baseline)
223{
224 int min[3];
225 int nat[3];
226 int n_visible_children = 0;
227 int spacing;
228 int i;
229
230 spacing = get_spacing (self, node: gtk_widget_get_css_node (widget));
231
232 for (i = 0; i < 3; i ++)
233 {
234 GtkWidget *child = self->children[i];
235
236 if (child)
237 {
238 gtk_widget_measure (widget: child,
239 orientation,
240 for_size,
241 minimum: &min[i], natural: &nat[i], NULL, NULL);
242
243 if (_gtk_widget_get_visible (widget: child))
244 n_visible_children ++;
245 }
246 else
247 {
248 min[i] = 0;
249 nat[i] = 0;
250 }
251 }
252
253 *minimum = min[0] + min[1] + min[2];
254 *natural = nat[1] + 2 * MAX (nat[0], nat[2]);
255
256 if (n_visible_children > 0)
257 {
258 *minimum += (n_visible_children - 1) * spacing;
259 *natural += (n_visible_children - 1) * spacing;
260 }
261}
262
263static void
264gtk_center_layout_measure_opposite (GtkCenterLayout *self,
265 GtkOrientation orientation,
266 int for_size,
267 int *minimum,
268 int *natural,
269 int *minimum_baseline,
270 int *natural_baseline)
271{
272 int child_min, child_nat;
273 int child_min_baseline, child_nat_baseline;
274 int total_min, above_min, below_min;
275 int total_nat, above_nat, below_nat;
276 GtkWidget *child[3];
277 GtkRequestedSize sizes[3];
278 int i;
279
280 child[0] = self->start_widget;
281 child[1] = self->center_widget;
282 child[2] = self->end_widget;
283
284 if (for_size >= 0)
285 gtk_center_layout_distribute (self, for_size: -1, size: for_size, spacing: 0, sizes);
286
287 above_min = below_min = above_nat = below_nat = -1;
288 total_min = total_nat = 0;
289
290 for (i = 0; i < 3; i++)
291 {
292 if (child[i] == NULL)
293 continue;
294
295 gtk_widget_measure (widget: child[i],
296 orientation,
297 for_size: for_size >= 0 ? sizes[i].minimum_size : -1,
298 minimum: &child_min, natural: &child_nat,
299 minimum_baseline: &child_min_baseline, natural_baseline: &child_nat_baseline);
300
301 if (child_min_baseline >= 0)
302 {
303 below_min = MAX (below_min, child_min - child_min_baseline);
304 above_min = MAX (above_min, child_min_baseline);
305 below_nat = MAX (below_nat, child_nat - child_nat_baseline);
306 above_nat = MAX (above_nat, child_nat_baseline);
307 }
308 else
309 {
310 total_min = MAX (total_min, child_min);
311 total_nat = MAX (total_nat, child_nat);
312 }
313 }
314
315 if (above_min >= 0)
316 {
317 int min_baseline = -1;
318 int nat_baseline = -1;
319
320 total_min = MAX (total_min, above_min + below_min);
321 total_nat = MAX (total_nat, above_nat + below_nat);
322
323 switch (self->baseline_pos)
324 {
325 case GTK_BASELINE_POSITION_TOP:
326 min_baseline = above_min;
327 nat_baseline = above_nat;
328 break;
329 case GTK_BASELINE_POSITION_CENTER:
330 min_baseline = above_min + (total_min - (above_min + below_min)) / 2;
331 nat_baseline = above_nat + (total_nat - (above_nat + below_nat)) / 2;
332 break;
333 case GTK_BASELINE_POSITION_BOTTOM:
334 min_baseline = total_min - below_min;
335 nat_baseline = total_nat - below_nat;
336 break;
337 default:
338 break;
339 }
340
341 if (minimum_baseline)
342 *minimum_baseline = min_baseline;
343 if (natural_baseline)
344 *natural_baseline = nat_baseline;
345 }
346
347 *minimum = total_min;
348 *natural = total_nat;
349}
350
351
352
353static void
354gtk_center_layout_measure (GtkLayoutManager *layout_manager,
355 GtkWidget *widget,
356 GtkOrientation orientation,
357 int for_size,
358 int *minimum,
359 int *natural,
360 int *minimum_baseline,
361 int *natural_baseline)
362{
363 GtkCenterLayout *self = GTK_CENTER_LAYOUT (ptr: layout_manager);
364
365 if (self->orientation == orientation)
366 gtk_center_layout_measure_orientation (self, widget, orientation, for_size,
367 minimum, natural, minimum_baseline, natural_baseline);
368 else
369 gtk_center_layout_measure_opposite (self, orientation, for_size,
370 minimum, natural, minimum_baseline, natural_baseline);
371}
372
373static void
374gtk_center_layout_allocate (GtkLayoutManager *layout_manager,
375 GtkWidget *widget,
376 int width,
377 int height,
378 int baseline)
379{
380 GtkCenterLayout *self = GTK_CENTER_LAYOUT (ptr: layout_manager);
381 GtkWidget *child[3];
382 int child_size[3];
383 int child_pos[3];
384 GtkRequestedSize sizes[3];
385 int size;
386 int for_size;
387 int i;
388 int spacing;
389
390 spacing = get_spacing (self, node: gtk_widget_get_css_node (widget));
391
392 if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
393 {
394 size = width;
395 for_size = height;
396 }
397 else
398 {
399 size = height;
400 for_size = width;
401 baseline = -1;
402 }
403
404 /* Allocate child sizes */
405
406 gtk_center_layout_distribute (self, for_size, size, spacing, sizes);
407
408 child[1] = self->center_widget;
409 child_size[1] = sizes[1].minimum_size;
410
411 if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
412 gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
413 {
414 child[0] = self->end_widget;
415 child[2] = self->start_widget;
416 child_size[0] = sizes[2].minimum_size;
417 child_size[2] = sizes[0].minimum_size;
418 }
419 else
420 {
421 child[0] = self->start_widget;
422 child[2] = self->end_widget;
423 child_size[0] = sizes[0].minimum_size;
424 child_size[2] = sizes[2].minimum_size;
425 }
426
427 /* Determine baseline */
428 if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
429 baseline == -1)
430 {
431 int min_above, nat_above;
432 int min_below, nat_below;
433 gboolean have_baseline;
434
435 have_baseline = FALSE;
436 min_above = nat_above = 0;
437 min_below = nat_below = 0;
438
439 for (i = 0; i < 3; i++)
440 {
441 if (child[i] && gtk_widget_get_valign (widget: child[i]) == GTK_ALIGN_BASELINE)
442 {
443 int child_min_height, child_nat_height;
444 int child_min_baseline, child_nat_baseline;
445
446 child_min_baseline = child_nat_baseline = -1;
447
448 gtk_widget_measure (widget: child[i], orientation: GTK_ORIENTATION_VERTICAL,
449 for_size: child_size[i],
450 minimum: &child_min_height, natural: &child_nat_height,
451 minimum_baseline: &child_min_baseline, natural_baseline: &child_nat_baseline);
452
453 if (child_min_baseline >= 0)
454 {
455 have_baseline = TRUE;
456 min_below = MAX (min_below, child_min_height - child_min_baseline);
457 nat_below = MAX (nat_below, child_nat_height - child_nat_baseline);
458 min_above = MAX (min_above, child_min_baseline);
459 nat_above = MAX (nat_above, child_nat_baseline);
460 }
461 }
462 }
463
464 if (have_baseline)
465 {
466 /* TODO: This is purely based on the minimum baseline.
467 * When things fit we should use the natural one
468 */
469 switch (self->baseline_pos)
470 {
471 default:
472 case GTK_BASELINE_POSITION_TOP:
473 baseline = min_above;
474 break;
475 case GTK_BASELINE_POSITION_CENTER:
476 baseline = min_above + (height - (min_above + min_below)) / 2;
477 break;
478 case GTK_BASELINE_POSITION_BOTTOM:
479 baseline = height - min_below;
480 break;
481 }
482 }
483 }
484
485 /* Allocate child positions */
486
487 child_pos[0] = 0;
488 child_pos[1] = (size / 2) - (child_size[1] / 2);
489 child_pos[2] = size - child_size[2];
490
491 if (child[1])
492 {
493 /* Push in from start/end */
494 if (child_size[0] > 0 && child_size[0] + spacing > child_pos[1])
495 child_pos[1] = child_size[0] + spacing;
496 else if (child_size[2] > 0 && size - child_size[2] - spacing < child_pos[1] + child_size[1])
497 child_pos[1] = size - child_size[1] - child_size[2] - spacing;
498 }
499
500 for (i = 0; i < 3; i++)
501 {
502 GtkAllocation child_allocation;
503
504 if (child[i] == NULL)
505 continue;
506
507 if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
508 {
509 child_allocation.x = child_pos[i];
510 child_allocation.y = 0;
511 child_allocation.width = child_size[i];
512 child_allocation.height = height;
513 }
514 else
515 {
516 child_allocation.x = 0;
517 child_allocation.y = child_pos[i];
518 child_allocation.width = width;
519 child_allocation.height = child_size[i];
520 }
521
522 gtk_widget_size_allocate (widget: child[i], allocation: &child_allocation, baseline);
523 }
524}
525
526static void
527gtk_center_layout_class_init (GtkCenterLayoutClass *klass)
528{
529 GtkLayoutManagerClass *layout_class = GTK_LAYOUT_MANAGER_CLASS (ptr: klass);
530
531 layout_class->get_request_mode = gtk_center_layout_get_request_mode;
532 layout_class->measure = gtk_center_layout_measure;
533 layout_class->allocate = gtk_center_layout_allocate;
534}
535
536static void
537gtk_center_layout_init (GtkCenterLayout *self)
538{
539 self->orientation = GTK_ORIENTATION_HORIZONTAL;
540 self->baseline_pos = GTK_BASELINE_POSITION_CENTER;
541}
542
543/**
544 * gtk_center_layout_new:
545 *
546 * Creates a new `GtkCenterLayout`.
547 *
548 * Returns: the newly created `GtkCenterLayout`
549 */
550GtkLayoutManager *
551gtk_center_layout_new (void)
552{
553 return g_object_new (GTK_TYPE_CENTER_LAYOUT, NULL);
554}
555
556/**
557 * gtk_center_layout_set_orientation:
558 * @self: a `GtkCenterLayout`
559 * @orientation: the new orientation
560 *
561 * Sets the orientation of @self.
562 */
563void
564gtk_center_layout_set_orientation (GtkCenterLayout *self,
565 GtkOrientation orientation)
566{
567 g_return_if_fail (GTK_IS_CENTER_LAYOUT (self));
568
569 if (orientation != self->orientation)
570 {
571 self->orientation = orientation;
572 gtk_layout_manager_layout_changed (manager: GTK_LAYOUT_MANAGER (ptr: self));
573 }
574}
575
576/**
577 * gtk_center_layout_get_orientation:
578 * @self: a `GtkCenterLayout`
579 *
580 * Gets the current orienration of the layout manager.
581 *
582 * Returns: The current orientation of @self
583 */
584GtkOrientation
585gtk_center_layout_get_orientation (GtkCenterLayout *self)
586{
587 g_return_val_if_fail (GTK_IS_CENTER_LAYOUT (self), GTK_ORIENTATION_HORIZONTAL);
588
589 return self->orientation;
590}
591
592/**
593 * gtk_center_layout_set_baseline_position:
594 * @self: a `GtkCenterLayout`
595 * @baseline_position: the new baseline position
596 *
597 * Sets the new baseline position of @self
598 */
599void
600gtk_center_layout_set_baseline_position (GtkCenterLayout *self,
601 GtkBaselinePosition baseline_position)
602{
603 g_return_if_fail (GTK_IS_CENTER_LAYOUT (self));
604
605 if (baseline_position != self->baseline_pos)
606 {
607 self->baseline_pos = baseline_position;
608 gtk_layout_manager_layout_changed (manager: GTK_LAYOUT_MANAGER (ptr: self));
609 }
610}
611
612/**
613 * gtk_center_layout_get_baseline_position:
614 * @self: a `GtkCenterLayout`
615 *
616 * Returns the baseline position of the layout.
617 *
618 * Returns: The current baseline position of @self.
619 */
620GtkBaselinePosition
621gtk_center_layout_get_baseline_position (GtkCenterLayout *self)
622{
623 g_return_val_if_fail (GTK_IS_CENTER_LAYOUT (self), GTK_BASELINE_POSITION_TOP);
624
625 return self->baseline_pos;
626}
627
628/**
629 * gtk_center_layout_set_start_widget:
630 * @self: a `GtkCenterLayout`
631 * @widget: (nullable): the new start widget
632 *
633 * Sets the new start widget of @self.
634 *
635 * To remove the existing start widget, pass %NULL.
636 */
637void
638gtk_center_layout_set_start_widget (GtkCenterLayout *self,
639 GtkWidget *widget)
640{
641 g_return_if_fail (GTK_IS_CENTER_LAYOUT (self));
642 g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
643
644 if (self->start_widget == widget)
645 return;
646
647 self->start_widget = widget;
648 gtk_layout_manager_layout_changed (manager: GTK_LAYOUT_MANAGER (ptr: self));
649}
650
651/**
652 * gtk_center_layout_get_start_widget:
653 * @self: a `GtkCenterLayout`
654 *
655 * Returns the start widget fo the layout.
656 *
657 * Returns: (nullable) (transfer none): The current start widget of @self
658 */
659GtkWidget *
660gtk_center_layout_get_start_widget (GtkCenterLayout *self)
661{
662 g_return_val_if_fail (GTK_IS_CENTER_LAYOUT (self), NULL);
663
664 return self->start_widget;
665}
666
667/**
668 * gtk_center_layout_set_center_widget:
669 * @self: a `GtkCenterLayout`
670 * @widget: (nullable): the new center widget
671 *
672 * Sets the new center widget of @self.
673 *
674 * To remove the existing center widget, pass %NULL.
675 */
676void
677gtk_center_layout_set_center_widget (GtkCenterLayout *self,
678 GtkWidget *widget)
679{
680 g_return_if_fail (GTK_IS_CENTER_LAYOUT (self));
681 g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
682
683 if (self->center_widget == widget)
684 return;
685
686 self->center_widget = widget;
687 gtk_layout_manager_layout_changed (manager: GTK_LAYOUT_MANAGER (ptr: self));
688}
689
690/**
691 * gtk_center_layout_get_center_widget:
692 * @self: a `GtkCenterLayout`
693 *
694 * Returns the center widget of the layout.
695 *
696 * Returns: (nullable) (transfer none): the current center widget of @self
697 */
698GtkWidget *
699gtk_center_layout_get_center_widget (GtkCenterLayout *self)
700{
701 g_return_val_if_fail (GTK_IS_CENTER_LAYOUT (self), NULL);
702
703 return self->center_widget;
704}
705
706/**
707 * gtk_center_layout_set_end_widget:
708 * @self: a `GtkCenterLayout`
709 * @widget: (nullable) (transfer none): the new end widget
710 *
711 * Sets the new end widget of @self.
712 *
713 * To remove the existing center widget, pass %NULL.
714 */
715void
716gtk_center_layout_set_end_widget (GtkCenterLayout *self,
717 GtkWidget *widget)
718{
719 g_return_if_fail (GTK_IS_CENTER_LAYOUT (self));
720 g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
721
722 if (self->end_widget == widget)
723 return;
724
725 self->end_widget = widget;
726 gtk_layout_manager_layout_changed (manager: GTK_LAYOUT_MANAGER (ptr: self));
727}
728
729/**
730 * gtk_center_layout_get_end_widget:
731 * @self: a `GtkCenterLayout`
732 *
733 * Returns the end widget of the layout.
734 *
735 * Returns: (nullable) (transfer none): the current end widget of @self
736 */
737GtkWidget *
738gtk_center_layout_get_end_widget (GtkCenterLayout *self)
739{
740 g_return_val_if_fail (GTK_IS_CENTER_LAYOUT (self), NULL);
741
742 return self->end_widget;
743}
744

source code of gtk/gtk/gtkcenterlayout.c