Container secrets: size allocation, part 6

Baselines

We are entering another of the more mysterious areas of GTK+ size allocation. Baselines move widgets from a simple box-with-width-and-height model to one where widgets can be aligned vertically in more interesting ways. The main place where this is matters is text. The readers eye is very sensitive to words moving up and down as you move along a line of text. Baselines are there to avoid that.

 

Since this is about aligning children vertically wrt. to each other, baselines are only relevant when the container is in horizontal orientation.

Measure above and below

Since children can now have a ‘forced’ alignment, simply t aking the maximum of the children’s heights is no longer sufficient. The alignment might cause children to ‘stick out’ at the top or the bottom, requiring a greater overall height. In order to handle this, we measure the parts ‘above the baseline’ and the parts ‘below the baseline’ separately, and maximize them separately.

for (i = 0; i < 3; i++) {
  gtk_widget_measure (child[i],
                      orientation,
                      sizes[i].minimum_size,
                      &child_min, &child_nat,
                      &child_min_baseline, &child_nat_baseline);

   below_min = MAX (below_min, child_min - child_min_baseline);
   above_min = MAX (above_min, child_min_baseline);
   below_nat = MAX (below_nat, child_nat - child_nat_baseline);
   above_nat = MAX (above_nat, child_nat_baseline);
}

total_min = above_min + below_min;
total_nat = above_nat + below_nat;

This code leaves out some details, such as dealing with children that don’t return a baseline.

Allocate on baseline

On the allocation side, there are two cases: either we are given a baseline that we have to align our children to, or we have to determine a baseline ourselves. In the latter case, we need to do essentially the same we already did for measuring: determine the below and above sizes separately, and use them to find our baseline:

for (i = 0; i < 3; i++) {
  if (gtk_widget_get_valign (child[i]) != GTK_ALIGN_BASELINE)
    continue;

  gtk_widget_measure (child[i],
                      GTK_ORIENTATION_VERTICAL,
                      child_size[i],
                      &child_min, &child_nat,
                      &child_min_baseline, &child_nat_baseline);

  below_min = MAX (below_min, child_min - child_min_baseline);
  below_nat = MAX (below_nat, child_nat - child_nat_baseline);
  above_min = MAX (above_min, child_min_baseline);
  above_nat = MAX (above_nat, child_nat_baseline);
}

When it comes to determining the baseline, we again have a choice to make. When there is more space available than the minimum, do we place the baseline as high as possible, or as low as possible, or somewhere in the middle? GtkBox has a ::baseline-position property to leave this choice to the user, and we do the same here.

switch (baseline_position) {
  case GTK_BASELINE_POSITION_TOP:
    baseline = above_min;
    break;
  case GTK_BASELINE_POSITION_CENTER:
    baseline = above_min + (height - (above_min + below_min)) / 2;
    break;
  case GTK_BASELINE_POSITION_BOTTOM:
    baseline = height - below_min;
    break;
}
Expanded, baseline position: center
Compressed, baseline position: top
Compressed, baseline position: center
Compressed, baseline position: bottom

Summary

This ends our journey through GTK+’s size allocation machinery. I hope you enjoyed it.

References

  1. Container secrets: size allocation
  2. Container secrets: size allocation, part 2
  3. Container secrets: size allocation, part 3
  4. Container secrets: size allocation, part 4
  5. Container secrets: size allocation, part 5
  6. The code with these changes

Container secrets: size allocation, part 5

Orientation

Many widgets in GTK+ can be oriented either horizontally or vertically. Anything from a separator to a toolbar is implementing the GtkOrientable interface to allow this to be changed at runtime, by setting the ::orientation property. So, obviously, GtkCenterBox should follow this pattern too.

I’m not explaining in detail how to add the interface and implement the property. The interesting part for us is how we are going to use the orientation property during size allocation.

Thankfully, much of our machinery is already written in terms of a single dimension, and can be applied to a height just as well as a width. What remains to be done is going through all the functions, and making sure that we take the orientation into account whenever we do something that depends on it. For example, we introduce a little helper to query the proper expand property.

static gboolean
get_expand (GtkWidget *widget,
            GtkOrientation orientation)
{
  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    return gtk_widget_get_hexpand (widget);
  else
    return gtk_widget_get_vexpand (widget);
}

One thing to keep in mind is that some of the features we implement here only apply in horizontal orientation, such as right-to-left flipping, or baselines.

The measure() function changes to avoid hardcoding horizontal orientation:

if (orientation == self->orientation)
  gtk_center_box_measure_orientation (widget, orientation, for_size,
                                      minimum, natural,
                                      min_baseline, nat_baseline);
else
  gtk_center_box_measure_opposite (widget, orientation, for_size,
                                   minimum, natural,
                                   min_baseline, nat_baseline);

The size_allocate() function calls distribute() to distribute either the width or the height, depending on orientation:

if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
  size = width;
  for_size = height;
} else {
  size = height;
  for_size = width;
}
distribute (self, for_size, size, sizes);

After these straightforward, but tedious changes, we can orient a center box vertically:

References

  1. Container secrets: size allocation
  2. Container secrets: size allocation, part 2
  3. Container secrets: size allocation, part 3
  4. Container secrets: size allocation, part 4
  5. The code with these changes

Container secrets: size allocation, part 4

Height-for-width

This is where we enter the deeper parts of GTK+ size allocation. Height-for-width means that a widget does not have a single minimum size, but it might be able to accommodate a smaller width in return for getting a bigger height. Most widgets are not like this. The typical example for this behavior is a label that can wrap its text in multiple lines:

  

Height-for-width makes size allocation more expensive, so containers have to enable it explicitly, by setting a request mode. In general, containers should look at their children and use the request mode that is preferred by the majority of them. For simplicity, we just hardcode height-for-width here:

static GtkSizeRequestMode
gtk_center_box_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

Measure both ways

The idiomatic way to write a measure() function that can handle height-for-width is to break it down into two cases: one where we are measuring along the orientation of the layout, and one where we are measuring in the opposite direction.

if (orientation == GTK_ORIENTATION_HORIZONTAL)
  measure_orientation (widget, for_size,
                       orientation,
                       minimum, natural,
                       minimum_baseline, natural_baseline);
else
  measure_opposite (widget, for_size,
                    orientation,
                    minimum, natural,
                    minimum_baseline, natural_baseline);

Measuring in the direction of the orientation is just like what our measure() function has done all along: we get a height, so we ask all children how much width they need for that height, and we sum up the answers.

Measuring in the opposite direction means to answer the question: given this width, how much height do you need ? We want to ask the children the same question, but what width should we give to each child ? We can’t just pass the full width to each child, since we don’t want them to overlap.

Distribute

To solve this, we need to distribute the available width among the children. This is just what our size_allocate() function is doing, so we need to break out the guts of size_allocate() out into a separate function.

Unsurprisingly, we will call the new function distribute().

static void
distribute (GtkCenterBox *self,
            int for_size,
            int size,
            GtkRequestedSize *sizes)
{
   /* Do whatever size_allocate() used to do
    * to determine sizes
    */

  sizes[0].minimum_size = start_size;
  sizes[1].minimum_size = center_size;
  sizes[2].minimum_size = end_size;
}

Now that we know how to get candidate widths for the children, we can complete the function to measure in the opposite direction. As before, we eventually return the maximum of the required heights for our children, since our layout is horizontal.

Note that orientation is GTK_ORIENTATION_VERTICAL in this case, so the min and nat values returned by the gtk_widget_measure() calls are heights.

distribute (self, -1, width, sizes);

gtk_widget_measure (start_widget,
                    orientation,
                    sizes[0].minimum_size,
                    &start_min, &start_nat,
                    &min_baseline, &nat_baseline);

gtk_widget_measure (center_widget,
                    orientation,
                    sizes[1].minimum_size,
                    &center_min, &center_nat,
                    &min_baseline, &nat_baseline);

gtk_widget_measure (end_widget,
                    orientation,
                    sizes[2].minimum_size,
                    &end_min, &end_nat,
                    &min_baseline, &nat_baseline);

*minimum = MAX (start_min, center_min, end_min);
*natural = MAX (start_nat, center_nat, end_nat);

Since we have now broken out the bulk of size_allocate() into the distribute() function, we can just call it from there and then do the remaining work that is necessary to assign positions to the children (since distribute already gave us the sizes).

Expanded
Slightly below natural size
Smaller
and smaller
and smaller

References

  1. Container secrets: size allocation
  2. Container secrets: size allocation, part 2
  3. Container secrets: size allocation, part 3
  4. The code with these changes
  5. Documentation of height-for-width geometry management

Container secrets: size allocation, part 3

 Expanding children

The next stop in our quest for featureful size allocation is the ::expand property. There are actually two of them,  ::hexpand and ::vexpand, and they have a somewhat interesting behavior of propagating upwards in the widget hierarchy. But that is not what we are going to discuss today, we simply want to give the children of our center box widget all the available space if they have the ::hexpand flag set.

Once again, the measure() implementation can stay as it is. And once again, we need to make a decision about who to expand first, if more than one child has an expand flag set. GtkBox tries to treat all its children the same, and evenly distributes the available extra space among all expanding children. We, on the other hand, prefer the center child, since it is the most important one.

But how do we go about this ? After some experimentation, I figured that if we already have to push the center child to the left or right because it does not fit, there is not point in making it even larger. Therefore we only respect the expand flag if that is not the case:

center_expand = gtk_widget_get_hexpand (center);

if (left_size > center_x)
  center_x = left_size;
else if (width - right_size < center_pos + center_size)
  center_x = width - center_width - right_size;
else if (center_expand) {
  center_width = width - 2 * MAX (left_size, right_size);
  center_x = (width / 2) - (center_width / 2);
}

After doing this, there may still be some space left that we can give to the outer children, if they are expanding:

if (left_expand)
  left_size = center_pos - left_pos;
if (right_expand)
  right_size = pos + width - (center_pos + center_width);
No expanding children
Center child expanding
End child expanding
Center and end children expanding

References

  1. Container secrets: size allocation
  2. Container secrets: size allocation, part 2
  3. The code with these changes

This week in GTK+ – 36

In this last week, the master branch of GTK+ has seen 22 commits, with 1165 lines added and 904 lines removed.

Planning and status
Notable changes

On the master branch:

  • Robert Ancell updated the icon browser utility to improve the error messages when loading an icon failed
  • Matthias Clasen improve the newly added GtkCenterBox widget; you can follow along his work in the “Container Secrets” series of articles
Bugs fixed
  • 783552 – Translation interpretation
  • 759308 – Instant apply in printing dialog (number of copies)
  • 783445 – Incomplete documentation of gtk_widget_insert_after/before()
Getting involved

Interested in working on GTK+? Look at the list of bugs for newcomers and join the IRC channel #gtk+ on irc.gnome.org.

Container secrets: size allocation, part 2

Right-to-left languages

As the first thing in this series, we add back support for right-to-left languages.

It may be a little surprising if you are only used to writing software in English, but GTK+ has traditionally tried to ‘do the right thing’ automatically for languages that are written from right to left like Hebrew. Concretely, that means that we interpret the starting position in horizontal layouts to be on the right in these languages, i.e. we ‘flip’ horizontal arrangements. This can of course be overridden, by setting the ::text-direction property of the container. By default, the text direction gets determined from the locale.

This is of course very easy to do. The code I showed in the first post assumes left-to-right order of the children. We keep it that way and just reorder the children when necessary. Note that the measure() code doesn’t need any change, since it does not depend on the order at all. So, we just change size_allocate():

if (text_direction == rtl) {
  child[0] = last;
  child[1] = center;
  child[2] = first;
} else {
  child[0] = first;
  child[1] = center;
  child[2] = last;
}

One small gotcha that I haven’t mentioned yet: CSS assumes that :first-child is always the leftmost element, regardless of text direction. So, when we are moving child widgets from the left side to the right side in RTL context, we need to reorder the corresponding CSS nodes when the text direction changes.

if (direction == GTK_TEXT_DIR_LTR) {
  first = gtk_widget_get_css_node (start_widget);
  last = gtk_widget_get_css_node (end_widget);
} else {
  first = gtk_widget_get_css_node (end_widget);
  last = gtk_widget_get_css_node (start_widget);
}

parent = gtk_widget_get_css_node (box);
gtk_css_node_insert_after (parent, first, NULL);
gtk_css_node_insert_before (parent, last, NULL);

Natural size

Since this was easy, we’ll press on and make our size allocation respect natural size too.

In GTK+ every widget has not just a minimum size, which is the smallest size that it can usefully present itself in, but also a preferred or natural size. The measure() function returns both of these sizes.

For natural size, we can do slightly better than the code we showed last time. Back then, we simply calculated the natural size of the box by adding up the natural sizes of all children. But we want to center the middle child, so lets ask for enough room to give all children their natural size and put the middle child in the center:

*natural = child2_nat + 2 * MAX (child1_nat, child3_nat);

The size_allocate() function needs more work, and here we have some decisions to make. With 3 children vying for the available space, and the extra constraint imposed by centering, we could prefer to give more space to the outer children, or make the center child bigger. Since centering is the defining feature of this widget, I went with the latter choice.

So, how much space can we give to the center widget ? We don’t want to make it smaller than the minimum size, or bigger than the natural size, and we need to leave at least the minimum required space for the outer children.

center_size = CLAMP (width - (left_min + right_min),
                     center_min, center_nat);

Next, lets figure out how much space we can give to the outer children. We obviously can’t hand out more than we have left after giving the center child its part, and we have to split the remaining space evenly in order for centering to work (which is what avail in the code below is about). Again, want to respect the childs minimum and natural size.

avail = MIN ( (width - center_size) / 2,
              width - (center_size + right_min));
left_size = CLAMP (avail, left_min, left_nat);

And similarly for the right child. After determining the child sizes, all that is left is to assign the positions in the same way we’ve seen in part one: put the outer children at the very left and right, then center the middle child and push it to the right or left to avoid overlap.

Expanded
Natural size
Below natural size
Smaller
Minimum size

References

  1. Container secrets, size allocation
  2. The code with these changes

Container secrets: size allocation

I recently had an opportunity to reimplement size allocation for a container with all the things that GTK+ supports:

  • RTL support
  • Natural size
  • Align and expand
  • Height-for-width
  • Orientation support
  • Baselines

If you do a one-off container in an application, most of these may not matter, but in a general-purpose GTK+ widget, all of them are likely to become relevant sooner or later.

Since this is quite a lot of ground to cover, it will take a few posts to get through this. Lets get started!

The starting point

GtkCenterBox is a simple widget that can contain three child widgets – it is not a GtkContainer, at least not currently. In GTK+ 4, any widget can be a parent of other widgets. The layout that GtkCenterBox applies to its children is to center the middle child, as long as that is possible.  This is functionality that GtkBox provides in GTK+ 3, but the center child handling complicates an already complex container. Therefore we are moving it into a separate widget in GTK+ 4.

Expanded
Natural size
Below natural size
Minimum size

When I started looking at the GtkCenterBox size allocation code, it was very simple. The two methods to look at are measure() and size_allocate().

The measure implementation was just measuring the three children, adding up the minimum and natural sizes in horizontal direction, and taking their maximum in vertical direction. In rough pseudo-code:

if (orientation == GTK_ORIENTATION_HORIZONTAL) {
  *minimum = child1_min + child2_min + child3_min;
  *natural = child1_nat + child2_nat + child3_nat;
} else {
  *minimum = MAX(child1_min, child2_min, child3_min);
  *natural = MAX(child1_min, child2_min, child3_min);
}

The size_allocate implementation was putting the first child to the left, the last child to the right, and then placed the middle child in the center, eliminating overlaps by pushing it to the right or left, as needed.

child1_width = child1_min;
child2_width = child2_min;
child3_width = child3_min;
child1_x = 0;
child3_x = total_width - child3_width;
child2_x = total_width/2 - child2_width/2;
if (child2_x < child1_x + child1_width)
  child2_2 = child1_x + child1_width;
else if (child2_x + child2_width > child3_x)
  child2_x = child3_x - child2_width;

As you can see, this is pretty straightforward. Sadly, it does not have any of the features I listed above:

  • Children always get their minimum size
  • The ::expand property is not taken into account
  • The first child is always placed at the left, regardless of text direction
  • No vertical orientation
  • No height-for-width support
  • Baselines are ignored

Over the next few posts, I’ll try to show how to add these features back, hopefully clarifying some of the mysteries of how GTK+ size allocation works, along the way.

References

  1. First commit in the series
  2. Documentation for GtkWidget size allocation
  3. The code that we start from

This week in GTK+ – 35

In this last week, the master branch of GTK+ has seen 33 commits, with 5011 lines added and 8140 lines removed.

Planning and status
  • The GTK+ road map is available on the wiki
  • Patrick Griffis is experimenting with a feature branch to deprecate and remove gtk_dialog_run(); see this comment on the pitfalls of nested main loops with regards to UI threads, IPC threads, and I/O threads
  • Matthias Clasen is experimenting with re-using the fuzzy search in libdazzle in the icon browser
Notable changes

On the master branch:

  • Matthias Clasen added the ability to copy the icon name to the clipboard to the icon browser utility
  • Matthias also made the GtkCenterBox widget public; this widget replaces the equivalent functionality of GtkBox to have a centered widget
  • Olivier Fourdan fixed various bugs in the Wayland backend, and backported the fixes to the gtk-3-22 stable branch
  • Chun-wei Fan pushed various fixes to ensure that GTK+ keeps building with MSVC on Windows
  • Emmanuele Bassi modified the Meson build to ensure that all the SASS-based themes are regenerated when building GTK+, if sassc is installed; Lapo Calamandrei removed the Gem file for Ruby/Sass, and thus GTK+ switched to sassc as the preferred SASS compiler
Bugs fixed
  • 770513 – MainToolbar in full-screen mode has rounded corners, which show video pixel bleed-thru underneath it
  • 783347 – gtkfilechoosernativewin32: Fix support for non-ASCII paths
  • 781945 – SIGSEGV dragging window on Wayland when toplevel window set_transient_for is set to another toplevel
  • 782283 – Wayland: Crash when dismissing a menu when a tooltip is visible
  • 781285 – Key repeat cancel under Wayland should depend on which key is repeating
  • 783397 – Remove unused code in gtktextdisplay.c
Getting involved

Interested in working on GTK+? Look at the list of bugs for newcomers and join the IRC channel #gtk+ on irc.gnome.org.

Drag-and-Drop in lists, revisited

My previous post on Drag-and-Drop in lists made some compromises in order to present the simplest, fully functional implementation. One of these was that we could only draw the drop target highlight around entire rows, whereas the drop really inserts the dropped row between rows. Lets try to do better!

Changing the target

In the simplified version, we made every row a drop target. This time around, we will change this and have just the list itself accept drops.

gtk_drag_dest_set (list,
                   GTK_DEST_DEFAULT_MOTION|GTK_DEST_DEFAULT_DROP, 
                   entries, 1,
                    GDK_ACTION_MOVE);
g_signal_connect (list, "drag-data-received",
                  G_CALLBACK (drag_data_received), NULL);

If you compare this to what was done before, you may notice another change: Instead of GTK_DEST_DEFAULT_ALL, we now just request the default behavior for motion and drop. We no longer request the default behavior for highlighting, since we want to handle that ourselves.

In order to do so, we need to connect to the drag-motion and drag-leave signals. These are emitted on the drop target, i.e. the list:

 g_signal_connect (list, "drag-motion",
                   G_CALLBACK (drag_motion), NULL);
 g_signal_connect (list, "drag-leave",
                   G_CALLBACK (drag_leave), NULL);

Mind the gap

The basic idea for our improved drag highlighting is that we keep track of the two adjacent rows between which the drop will happen, and use the GTK+ CSS machinery to create a suitable highlight of the gap.

This requires a bit too much code to show in full here, but the idea is as follows: Find the row that is currently under the cursor with gtk_list_box_get_row_at_y(). Look at its allocation to to find out if the cursor is in the top or bottom half of the row. Depending on that, we pick either the following or the preceding row as the other member for our pair of rows.

row = gtk_list_box_get_row_at_y (list, y);
gtk_widget_get_allocation (row, &alloc);
if (y < alloc.y + alloc.height/2)
  {
    row_after = row;
    row_before = get_row_before (list, row);
  }
else
  {
    row_before = row;
    row_after = get_row_after (list, row);
  }

There are some corner cases which I am omitting here, e.g. when the hovered row is the first or last row, or when we are hovering over ’empty space’ in the list.

CSS Highlights

I said we would be using CSS for creating the highlight. The easiest way to do so is to just add style classes to the two rows we’ve found:

gtk_style_context_add_class (
                      gtk_widget_get_style_context (row_before),
                      "drag-hover-bottom");
gtk_style_context_add_class (
                      gtk_widget_get_style_context (row_after),
                      "drag-hover-top");

And then we will use some custom CSS to create our highlight. In case you are wondering: #4e9a06 is the drag highlight color used in the Adwaita theme.

.row.drag-hover-top {
  border-top: 1px solid #4e9a06; 
}
.row.drag-hover-bottom {
  border-bottom: 1px solid #4e9a06; 
}

This is again omitting the corner cases that I’ve mentioned before.

How did we get here ?

Dropping the row onto the place it came from does not achieve anything. It makes sense to mark the place where the drag started in some way to make this obvious. We can again use CSS for this, and add a style class to the dragged row in our drag_begin() method:

gtk_style_context_add_class (gtk_widget_get_style_context (row),
                             "drag-row");

In order to give the row a bit a highlight when we are hovering over it, we add an extra style class to it in our drag_motion() handler:

if (row == drag_row)
  {
    gtk_style_context_add_class (gtk_widget_get_style_context (row),
                                 "drag-hover");
    row_before = get_row_before (row);
    row_after = get_row_after (row);
  }
else …

And here is the CSS for these classes:

.row.drag-row {
  color: gray;
  background: alpha(gray,0.2);
 }
 .row.drag-row.drag-hover {
  border-top: 1px solid #4e9a06;
  border-bottom: 1px solid #4e9a06;
  color: #4e9a06;
}

Putting it all together

(Sorry about the broken cursors. We should really fix this in gnome-shell’s screen recorder.)

References

  1. The complete example, testlist3.c
  2. The GTK+ DND documentation