GTK+ 3.92

昨天,我们发布了 GTK+ 3.92.1,重庆市。由于距离上次 3.91 版本发布已有一段时间,这里简要介绍一下主要更改。

此版本是我们迈向 GTK+ 4 的又一个里程碑。虽然很多工作仍需完成,但此版本让我们初步了解了我们希望在 GTK+ 4 中实现的一些目标。

GSK

自上次发布以来,大部分工作都投入到了 GSK 中。Vulkan 渲染器现在接近完成,可以避免使用 cairo 回退。唯一缺失的部分是模糊阴影(诚然,这是一个重要的部分)。

自 3.91.2 版本以来的一大进步是,我们不再对所有文本使用 cairo 回退。相反,文本(在标签和条目中,遗憾的是尚未在文本视图中)被转换为文本节点。每个文本节点包含一个 PangoGlyphString 和一个 PangoFont。Vulkan 渲染器使用字形缓存来避免为每一帧重新渲染字形。

Vulkan 渲染器的内部逻辑已重新设计为使用纹理而不是 cairo 表面作为中间结果,从而避免更多的 cairo 回退。

Vulkan 渲染器中获得支持的其他节点类型包括模糊、重复节点、混合模式和交叉淡化。在某些情况下,我们使用的着色器是非常简单的实现。欢迎帮助改进它们!

作为我们可以使用渲染节点的一个初步示例,我们为 GtkOverlay 实现了模糊底层功能。它的工作原理是将叠加层的“主子项”捕获为渲染节点,然后多次重复使用它,并进行正确的剪切,有时还会使用模糊节点。

检查器

为了帮助您探索 GSK,检查器现在会显示 Vulkan 信息,并且记录器会显示有关渲染节点的更多信息。

输入

在输入方面,事件获得了访问器,我们不再直接访问它们的字段。这是一个中间步骤,清理事件仍在进行中。我们已将传统的窗口小部件事件信号(例如 ::key-press-event)移动到事件控制器,并且 GTK+ 中的大多数窗口小部件都已完全停止使用它们。

构建系统

我们已切换为专门使用 Meson 进行 GTK+ 构建,3.92.1 版本是第一个使用 Meson 的 dist 支持完成的版本。为了发布该版本,我们还必须将文档、测试套件和已安装的测试移植到使用 Meson。

仍然存在一些粗糙的边缘(我们无法 100% 正确地获取所有依赖项),但总的来说,Meson 对我们来说效果很好。

其余部分

当然,每个人都喜欢 Emoji,并且 GTK+ 3.22 中引入的相同颜色 Emoji 支持也在此版本中提供。除此之外,CSS 中的字体支持在 CSS3 font-variant 属性的支持下略有改进。

当然,这依赖于具有相应功能的字体。

尝试一下

借助 GTK+ 3.92.1,您应该可以轻松地自己尝试一下其中的一些功能。

如果您一直想参与 GTK+ 开发但从未找到合适的机会,那么现在是参与的好时机!

滚动入门

几年前,我写了一篇关于 GTK+ 3 中滚动的文章。现在是再次回顾的时候了!

常见情况

当时描述的更改的基本思想仍然相同。我们希望触控板(或轨迹点)滚动是最常见的滚动形式之一,而滚动条则充当此滚动形式的窄指示器。

如您所见,我们更改光标以指示滚动,并且您可以自由地在所有方向上滚动。它也是动态的。

传统滚动

当然,仍然可以单击并拖动滑块进行传统滚动。

“传统”滚动的另一个方面是,您可以单击滑块外部的槽,然后将位置变形到您单击的位置,或按页面大小的增量跳转。

默认情况下,主单击变形,Shift+主单击按页面滚动。我们刚刚重新添加了中键单击作为 Shift+主单击的替代方法,因为这是许多人习惯的常见替代方法。主要是出于历史原因,GTK+ 有一个设置 gtk-primary-button-warps-slider,它会切换主单击和 Shift+主单击在此处的角色。

典型的键盘快捷键(Page Up、Page Down、Home、End)允许您使用键盘控制滚动。

平滑滚动

GTK+ 中的滚动还有您可能不了解的更多内容。我们很久以前引入的一项功能是“缩放”或“精细调整”模式,它可以减慢滚动速度以实现像素精确定位。

要触发此模式,您可以在滑块中长按或按住 Shift 键单击。如您在视频中所见,一旦您将指针移动到滚动条下方或上方,它将保持相同的放松速度滚动,直到您松开鼠标。

作为此主题的变体,我们最近添加了平滑滚动的变速变体。

要触发它,请在滑块外部的槽中进行辅助单击。滚动开始后,您可以通过将指针移近或远离滚动条来控制速度。这非常让人上瘾,一旦您发现它!

自定义位置

作为最后一个功能,应用程序可以添加一个上下文菜单,该菜单由对滑块的辅助单击触发,并使其滚动到重要位置。

就这样,继续滚动吧!

GTK+ 4 的进展

上周在曼彻斯特举行的 GUADEC 上,GTK+ 的维护人员和感兴趣的人员在非会议日举行了工作会议。

Georges 已经在他的博客文章中很好地总结了结果,您应该阅读一下(即使只是为了查看一些 GTK+ 人员的合影)。

GTK+ 3

我们确实简要讨论了 GTK+ 3。我们的印象是,大多数人都很享受 GTK+ 3.22 带来的稳定性,并且不急于跳转到新的、稳定性较差的工具包版本。

普遍的共识是,我们应该对 GTK+ 3 中的 API 添加保持非常严格的立场,但在收益足够高的情况下允许添加新功能。出现的示例是 Wayland 的客户端与服务器端协商协议支持或彩色表情符号支持。

GTK+ 4

大部分时间都花在了讨论我们希望或需要为 GTK+ 4 完成的所有事项上。我们对谁将处理这些事项有一个很好的了解,但我们没有确定完成它们的非常详细的时间表。

最后,我们收集了我们认为的阻塞项列表

  • 基于约束的布局
  • 支持在 ui 文件中定义状态和转换
  • 设计器支持
  • 将键盘处理转换为事件控制器
  • 非回退文本渲染
  • 一个完成的 GL 渲染器
  • 在 GDK 中干净地支持子表面
  • GDK 中不再有根窗口
  • 事件清理

其中一些要点值得更详细的讨论。

基于约束的布局、状态和设计器支持

使用约束进行布局是一个灵活的系统,它在其他平台上取得了成功。更重要的是,它更接近于大多数人思考在屏幕或纸张上布局事物的方式,并且它有望提供一种通用的语言,GTK+ 应用程序的设计人员和开发人员可以使用这种语言进行交流。

Emmanuele 和其他人一直在开发一段时间的 Emeus 窗口小部件正在使用约束来查找各个容器的子窗口小部件的位置和大小。

将其集成到 GTK+ 的计划更雄心勃勃:我们设想每个顶层都有一个约束求解器,窗口内的所有容器都将它们的约束添加到该求解器中。这将要求 GTK+ 中的当前容器以约束的形式表达它们的布局算法,这在大多数情况下应该不会太难,并且可以逐步完成。

状态和它们之间的转换是 Christian Hergert 在 libdazzle 中原型化的内容。这里的想法不仅是定义一个复杂的窗口小部件(例如对话框),还包括其主要状态以及它们之间的转换应如何在 ui 文件中工作。这将使我们拥有一个 UI 设计器工具,它不仅是关于在画布上排列窗口小部件,而且还走向故事板和设计转换。这当然说起来容易做起来难……

键盘处理

Christian 花了一些时间来描述他为 gnome builder 编写的快捷方式引擎,该引擎目前位于 libdazzle 中。它具有一些有趣的功能,例如捕获气泡事件处理、和弦(即多键序列,例如 Ctrl-C Ctrl-X)、与操作的紧密集成以及自动生成键盘快捷方式帮助的功能。

这方面的计划是从 Christian 的引擎中提取最佳功能,并将它们转换为一个或多个 GtkEventController。完成此工作后,我们将转换所有窗口小部件以使用事件控制器而不是按键信号处理程序。

GtkBindingSet 也将由事件控制器替换。

文本渲染

GSK 的 Vulkan 渲染器或多或少已完成。它可以有效地使用着色器渲染 CSS 机制产生的大部分内容。最大的例外是文本:目前文本的处理方式是使用 cairo 将其渲染到表面,然后将表面上传到纹理,然后在渲染节点中使用它。每一帧都是如此。

这里需要发生的是,我们将需要的字形上传到我们保留作为图集的大型纹理中,然后创建引用该图集的文本渲染节点。

由于文本是用户界面中非常重要的组成部分,因此在我们为 Vulkan 实现正确的文本渲染之前,我们不能真正声称我们已经验证了渲染节点方法。

GL 渲染器

Benjamin 完成了将 Vulkan 渲染器实现到几乎完成状态的大部分工作。当他这样做时,GL 渲染器已经落后了——它没有 Vulkan 中使用的着色器。

这里需要做的是抽象出通用部分,并将剩余部分从 Vulkan 向后移植到其对应的 GL 实现。一个不太令人愉快的地方是,我们最终可能需要多个 GL 渲染器变体,用于传统的 GL 和 GLES 平台。但我们或许可以仅使用一个现代 GL 渲染器,至少在初始阶段是这样。

字体和文本

有一个单独的会议专门讨论了我们文本渲染堆栈中的新功能。这里讨论的主题是可变字体和彩色表情符号。不幸的是,我错过了大部分讨论,但结果的总结是:

  • Behdad 对在 pango 和 fontconfig 中支持可变字体所需的工作有一个大致的计划。这涉及到在 PangoFontDescription 中使用新的语法来指定轴的值,以及在 PangoFontFamily 中使用新的 API 来获取有关可用轴的信息。
  • 在 GUADEC 期间,Behdad 合并了 cairo、fontconfig 和 pango 中对彩色表情符号的支持,我开始在 GTK+ 中开发一些简单的表情符号输入。现在这些也都已经落地,包括 GTK+ 3 和 master 分支。

其他

我们涉及了太多其他主题,无法在此一一总结。其中一个主题是辅助功能的现状,但这是一个以后再讨论的话题。

容器的秘密:尺寸分配,第 6 部分

基线

我们正在进入 GTK+ 尺寸分配的另一个更为神秘的领域。基线将窗口小部件从简单的具有宽度和高度的盒子模型转变为窗口小部件可以在垂直方向以更有趣的方式对齐的模型。这主要是在文本方面起作用。当您沿着一行文本移动时,读者的眼睛对单词的上下移动非常敏感。基线的作用就是避免这种情况。

 

由于这涉及子元素彼此之间的垂直对齐,因此只有当容器处于水平方向时,基线才相关。

测量上方和下方

由于子元素现在可以具有“强制”对齐方式,因此仅采用子元素高度的最大值不再足够。对齐可能会导致子元素在顶部或底部“突出”,从而需要更大的整体高度。为了处理这种情况,我们分别测量“基线上方”的部分和“基线下方”的部分,并分别最大化它们。

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;

这段代码省略了一些细节,例如处理不返回基线的子元素。

在基线上分配

在分配方面,有两种情况:要么我们被赋予一个必须将子元素对齐到的基线,要么我们必须自己确定一个基线。在后一种情况下,我们需要做的基本上与我们已经为测量所做的工作相同:分别确定下方和上方的大小,并使用它们来找到我们的基线

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);
}

当涉及到确定基线时,我们再次需要做出选择。当可用空间大于最小值时,我们将基线尽可能地放置在高处,还是尽可能地放置在低处,还是放置在中间的某个位置?GtkBox 有一个 ::baseline-position 属性,将此选择留给用户,我们在这里也这样做。

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;
}
展开,基线位置:居中
压缩,基线位置:顶部
压缩,基线位置:居中
压缩,基线位置:底部

总结

我们 GTK+ 的尺寸分配机制之旅到此结束。我希望你喜欢它。

参考资料

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第 2 部分
  3. 容器的秘密:尺寸分配,第 3 部分
  4. 容器的秘密:尺寸分配,第 4 部分
  5. 容器的秘密:尺寸分配,第 5 部分
  6. 包含这些更改的代码

容器的秘密:尺寸分配,第 5 部分

方向

GTK+ 中的许多窗口小部件可以水平或垂直方向显示。从分隔符到工具栏的任何东西都实现了 GtkOrientable 接口,允许通过设置 ::orientation 属性在运行时更改方向。因此,很明显,GtkCenterBox 也应该遵循此模式。

我不会详细解释如何添加接口和实现属性。对我们来说有趣的部分是我们将如何在尺寸分配期间使用方向属性。

值得庆幸的是,我们的大部分机制已经用单个维度编写,并且可以像应用于宽度一样应用于高度。剩下的工作是遍历所有函数,并确保我们在执行任何依赖于它的操作时都考虑方向。例如,我们引入一个小的助手来查询正确的 expand 属性。

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);
}

需要记住的一件事是,我们在此处实现的一些功能仅适用于水平方向,例如从右到左的翻转或基线。

measure() 函数更改为避免硬编码水平方向

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);

size_allocate() 函数调用 distribute() 来分配宽度或高度,具体取决于方向

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

经过这些直接但繁琐的更改后,我们可以垂直方向显示中心框

参考资料

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第 2 部分
  3. 容器的秘密:尺寸分配,第 3 部分
  4. 容器的秘密:尺寸分配,第 4 部分
  5. 包含这些更改的代码

容器的秘密:尺寸分配,第 4 部分

高度随宽度变化

这是我们进入 GTK+ 尺寸分配更深层的地方。高度随宽度变化意味着窗口小部件不是只有一个最小尺寸,而是可能会为了获得更大的高度而适应较小的宽度。大多数窗口小部件并非如此。此行为的典型示例是一个标签,它可以将其文本包装为多行

  

高度随宽度变化使尺寸分配更加昂贵,因此容器必须通过设置请求模式来显式启用它。通常,容器应查看其子元素并使用大多数子元素首选的请求模式。为简单起见,我们在此处硬编码高度随宽度变化

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

双向测量

编写可以处理高度随宽度变化的 measure() 函数的惯用方法是将其分解为两种情况:一种是我们沿着布局的方向进行测量,另一种是我们沿着相反的方向进行测量。

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);

沿着方向进行测量就像我们一直以来所做的 measure() 函数一样:我们得到一个高度,所以我们询问所有子元素对于该高度需要多少宽度,然后将答案相加。

沿着相反方向进行测量意味着回答这个问题:给定此宽度,您需要多少高度?我们想问子元素相同的问题,但是我们应该给每个子元素多少宽度?我们不能只是将完整宽度传递给每个子元素,因为我们不希望它们重叠。

分配

为了解决这个问题,我们需要在子元素之间分配可用的宽度。这正是我们的 size_allocate() 函数正在做的事情,因此我们需要将 size_allocate() 的核心部分分解为一个单独的函数。

毫不奇怪,我们将新函数称为 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;
}

现在我们知道如何获取子元素的候选宽度,我们可以完成沿着相反方向进行测量的函数。与之前一样,我们最终返回子元素所需高度的最大值,因为我们的布局是水平的。

请注意,在这种情况下,方向是 GTK_ORIENTATION_VERTICAL,因此 gtk_widget_measure() 调用返回的 min 和 nat 值是高度。

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);

由于我们现在已经将 size_allocate() 的大部分内容分解为 distribute() 函数,我们可以直接从那里调用它,然后执行将位置分配给子元素所需的其余工作(因为 distribute 已经为我们提供了尺寸)。

展开
略低于自然大小
更小
和更小
和更小

参考资料

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第 2 部分
  3. 容器的秘密:尺寸分配,第 3 部分
  4. 包含这些更改的代码
  5. 关于高度随宽度变化的几何管理的文档

容器的秘密:尺寸分配,第 3 部分

 展开子元素

在我们的特性丰富的尺寸分配探索中,下一站是 ::expand 属性。实际上有两个这样的属性,即 ::hexpand 和 ::vexpand,它们有一个相当有趣的特性,即在部件层级结构中向上传播。但这不是我们今天要讨论的内容,我们只是想让我们的中心框部件的子部件在设置了 ::hexpand 标志时,能够占用所有可用空间。

同样,measure() 的实现可以保持原样。我们再次需要决定,如果多个子部件都设置了 expand 标志,应该先扩展哪个。GtkBox 尝试平等对待所有子部件,并在所有扩展子部件之间平均分配可用的额外空间。而另一方面,我们更偏爱中心子部件,因为它最重要。

但是我们该如何实现呢?经过一些实验,我发现,如果我们已经因为中心子部件不适合而必须将其向左或向右推,那么再使其更大就没有意义了。因此,我们只在不是这种情况时才考虑 expand 标志。

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);
}

这样做之后,如果外部子部件正在扩展,可能还会有一些空间留给它们。

if (left_expand)
  left_size = center_pos - left_pos;
if (right_expand)
  right_size = pos + width - (center_pos + center_width);
没有扩展的子部件
中心子部件正在扩展
末端子部件正在扩展
中心和末端子部件正在扩展

参考资料

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第 2 部分
  3. 包含这些更改的代码

容器的秘密:尺寸分配,第 2 部分

从右到左的语言

作为本系列的第一项,我们重新添加了对从右到左语言的支持。

如果您只习惯于用英语编写软件,这可能会有点令人惊讶,但是 GTK+ 传统上一直尝试为像希伯来语这样从右到左书写的语言自动“做正确的事”。具体来说,这意味着我们在这些语言中将水平布局的起始位置解释为在右侧,即我们“翻转”水平排列。当然,可以通过设置容器的 ::text-direction 属性来覆盖此设置。默认情况下,文本方向是从区域设置中确定的。

这当然很容易做到。我在第一篇文章中展示的代码假定子部件的顺序是从左到右的。我们保持这种方式,并在必要时重新排序子部件。请注意,measure() 代码不需要任何更改,因为它根本不依赖于顺序。因此,我们只需要更改 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;
}

我还没有提到一个小问题:CSS 假设 :first-child 始终是最左边的元素,而不管文本方向如何。因此,当我们在 RTL 上下文中将子部件从左侧移动到右侧时,我们需要在文本方向更改时重新排序相应的 CSS 节点。

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);

自然大小

既然这很简单,我们将继续前进,使我们的尺寸分配也尊重自然大小。

在 GTK+ 中,每个部件不仅有一个最小尺寸,这是它可以有效呈现自身的最小尺寸,而且还有一个首选或自然尺寸。measure() 函数返回这两个尺寸。

对于自然尺寸,我们可以比上次展示的代码做得更好一点。当时,我们只是通过将所有子部件的自然尺寸加起来来计算框的自然尺寸。但是我们希望将中间的子部件居中,因此让我们要求足够的空间来使所有子部件都达到它们的自然尺寸并且将中间的子部件放在中心。

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

size_allocate() 函数需要做更多的工作,在这里我们需要做一些决定。在 3 个子部件争夺可用空间,以及居中施加的额外约束的情况下,我们可以选择给外部子部件更多的空间,或者使中心子部件更大。由于居中是此部件的定义特征,因此我选择了后者。

那么,我们可以给中心部件多少空间呢?我们不想使其小于最小尺寸,或大于自然尺寸,并且我们需要为外部子部件至少保留所需的最小空间。

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

接下来,让我们计算一下我们可以给外部子部件多少空间。显然,我们不能分配比在给予中心子部件其部分后剩下的更多的空间,并且为了使居中工作(这就是下面代码中 avail 的含义),我们必须平均分配剩余空间。同样,要尊重子部件的最小和自然尺寸。

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

右子部件也类似。确定子部件尺寸后,剩下的就是按照我们在第一部分中看到的方式分配位置:将外部子部件放在最左侧和最右侧,然后将中间的子部件居中,并将其向右或向左推以避免重叠。

展开
自然大小
低于自然大小
更小
最小尺寸

参考资料

  1. 容器秘密,尺寸分配
  2. 包含这些更改的代码

容器的秘密:尺寸分配

我最近有机会为容器重新实现尺寸分配,其中包含 GTK+ 支持的所有功能

  • RTL 支持
  • 自然大小
  • 对齐和扩展
  • 高度随宽度变化
  • 方向支持
  • 基线

如果您在应用程序中进行一次性容器,其中大多数可能无关紧要,但在通用的 GTK+ 部件中,所有这些迟早都可能会变得相关。

由于要涵盖的内容很多,因此需要几篇文章才能完成。让我们开始吧!

起点

GtkCenterBox 是一个简单的部件,可以包含三个子部件 - 它不是一个 GtkContainer,至少目前不是。在 GTK+ 4 中,任何部件都可以是其他部件的父部件。GtkCenterBox 应用于其子部件的布局是将中间的子部件居中,只要这是可能的。这是 GtkBox 在 GTK+ 3 中提供的功能,但是中心子部件处理使已经很复杂的容器更加复杂。因此,我们在 GTK+ 4 中将其移到一个单独的部件中。

展开
自然大小
低于自然大小
最小尺寸

当我开始查看 GtkCenterBox 尺寸分配代码时,它非常简单。需要查看的两个方法是 measure()size_allocate()。

measure 实现只是测量三个子部件,在水平方向上将最小尺寸和自然尺寸相加,然后在垂直方向上取它们的最大值。用粗略的伪代码表示

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);
}

size_allocate 实现是将第一个子部件放在左侧,将最后一个子部件放在右侧,然后将中间的子部件放在中心,并通过根据需要将其向右或向左推来消除重叠。

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;

如您所见,这非常简单。遗憾的是,它没有任何我上面列出的功能

  • 子部件始终获得其最小尺寸
  • ::expand 属性未被考虑在内
  • 第一个子部件始终位于左侧,而不管文本方向如何
  • 没有垂直方向
  • 不支持高度换宽度
  • 基线被忽略

在接下来的几篇文章中,我将尝试展示如何重新添加这些功能,希望能在此过程中阐明 GTK+ 尺寸分配工作原理的一些奥秘。

参考资料

  1. 本系列中的第一个提交
  2. GtkWidget 的尺寸分配文档
  3. 我们开始使用的代码

列表中的拖放,再次回顾

我之前关于列表中的拖放的文章为了展示最简单的、功能齐全的实现,做了一些妥协。其中之一是我们只能在整个行周围绘制放置目标高亮,而实际的放置是将放置的行插入行之间。让我们尝试做得更好!

更改目标

在简化版本中,我们将每一行都设为放置目标。这一次,我们将改变这一点,只让列表本身接受放置操作。

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);

如果你将此与之前所做的比较,你可能会注意到另一个变化:我们现在只请求移动和放置的默认行为,而不是 GTK_DEST_DEFAULT_ALL。我们不再请求高亮的默认行为,因为我们想自己处理它。

为了做到这一点,我们需要连接到 drag-motion 和 drag-leave 信号。这些信号在放置目标上发出,也就是列表。

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

注意间隙

我们改进的拖动高亮的基本思路是,跟踪放置操作将要发生的两个相邻行,并使用 GTK+ CSS 机制来创建合适的间隙高亮。

这需要展示的代码有点多,但思路如下:使用 gtk_list_box_get_row_at_y() 找到当前光标下的行。查看它的分配,以找出光标是在行的上半部分还是下半部分。根据这一点,我们选择后一行或前一行作为我们行对中的另一个成员。

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);
  }

这里省略了一些边界情况,例如,当悬停的行是第一行或最后一行,或者当我们悬停在列表中的“空白区域”时。

CSS高亮

我说过我们将使用 CSS 来创建高亮。最简单的方法就是给找到的两行添加样式类

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");

然后我们将使用一些自定义 CSS 来创建我们的高亮。如果你想知道:#4e9a06 是 Adwaita 主题中使用的拖动高亮颜色。

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

这再次省略了我之前提到的边界情况。

我们是怎么到这里的?

将行放到它来的地方不会有任何作用。以某种方式标记拖动开始的位置是有意义的,这样可以使其变得显而易见。我们可以再次使用 CSS 来实现这一点,并在我们的 drag_begin() 方法中给拖动的行添加一个样式类

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

为了在悬停时给该行一点高亮,我们在 drag_motion() 处理程序中给它添加一个额外的样式类

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 …

这是这些类的 CSS 代码

.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;
}

整合在一起

(抱歉光标断了。我们应该在 gnome-shell 的屏幕录像机中修复这个问题。)

参考资料

  1. 完整的示例,testlist3.c
  2. GTK+ DND 文档