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 分支。

其他

我们涉及的主题太多,无法在此处全部总结。其中之一是可访问性的状态,但这将在另一个时间讨论。

GTK+ 4.0 中的窗口小部件层次结构

今天我们将有客座作者 Timm Bäder,Corebird 的维护者和 GTK+ 的贡献者,来谈谈在 GTK+ 4.0 中编写复合窗口小部件的更改。

(注意:这里的一些信息基于尚未合并到 master 分支中的分支,但我相信它们会在不久的将来合并)

在 GTK+3 中,只有 GtkContainer 的子类可以拥有子窗口小部件。这对于我们所知道的“公共”容器子项来说很有意义,例如 GtkBox — 即开发人员可以任意添加、删除和重新排序子窗口小部件,并且容器只是进行布局。

但是,GTK+3 中有更多复杂的窗口小部件不继承自 GtkContainer,例如 GtkSpinButtonGtkSwitch。这些从未有过真正的 GtkWidget 子项。例如,考虑 GtkSpinButton 中的两个可点击区域。我在这里不称它们为“按钮”是有原因的,因为在 GTK+3 中,它们不是实际的 GtkButton 实例,因为 GtkSpinButton 不是 GtkContainer。相反,GtkSpinButton 必须解决这个事实,并为向上/向下区域创建两个 GdkWindow,然后在其中渲染两个图标;关注悬停和 CSS 状态;各种按钮向上/向下事件;以及 GdkWindow 生命周期等。为了解决 GtkContainer 要求,我们在 GTK+3 中引入了小工具GtkCssGadget)。在样式方面,小工具对应于 CSS 框,因此代表 CSS 树中的一个节点。在窗口小部件方面,它们被用于为非容器窗口小部件提供“类似窗口小部件的”、可 CSS 样式化的子项。

GtkWidget 更改

当然,在 GTK+4 中支持这些用例需要进行大量更改。我不会在这里列出所有这些更改(特别是那些更丑陋的更改,如焦点处理),但我认为其中大多数对于应用程序开发人员和自定义窗口小部件作者来说都非常有趣和重要。总的来说,我们正试图摆脱特殊情况,并通过尽可能地重用窗口小部件来走更通用的道路。因此,与其使用 PangoLayout 来显示文本,窗口小部件应该使用 GtkLabel。如果您有一个具有类似按钮语义的可点击区域,请尝试使用 GtkButton。如果您想在水平或垂直方向上布局窗口小部件,请使用 GtkBox。这样我们就有一个输入和渲染都可以操作的窗口小部件树。在实践中,这意味着大部分情况下要摆脱窗口小部件内部使用的所有小工具以及独立的 GtkCssNode 实例。

迭代子窗口小部件

虽然在 GTK+3 中,每个容器都必须实现 GtkContainer::forall,并且可以使用 gtk_container_foreach()gtk_container_forall() 轻松地迭代所有子窗口小部件,但在 GTK+4 中,每个 GtkWidget 都可以拥有子窗口小部件,因此每个窗口小部件都可能有一个我们需要绘制、测量、大小分配等的子列表。在窗口小部件层次结构中,可以通过以下方式访问这些子项和兄弟窗口小部件
  • gtk_widget_get_first_child()
  • gtk_widget_get_last_child()
  • gtk_widget_get_prev_sibling()
  • gtk_widget_get_next_sibling()
因此,迭代给定 GtkWidget 的子部件最简单的方法(在 C 语言中)是:
GtkWidget *widget;
GtkWidget *child;
for (child = gtk_widget_get_first_child (widget);
     child != NULL;
     child = gtk_widget_get_next_sibling (child))
  {
    /* Do stuff with @child */
    g_assert (gtk_widget_get_parent (child) == widget);
  }

将部件添加到非容器父部件

GTK+(3 和 4)中的每个部件都保存一个指向其父部件的指针。可以使用 gtk_widget_set_parent() 设置此父部件,并且所有 GtkContainer::add 的实现最终都必须使用此函数来设置给定子部件的父部件。

在 GTK+4 中,gtk_widget_set_parent() 仍然有效,并将部件添加到父部件的子部件列表的末尾。但是,我们显然也希望管理子部件的顺序,以及在列表中添加新子部件的位置,所以我们有:

  • gtk_widget_insert_before()
  • gtk_widget_insert_after()
用于在父部件列表中已有的子部件之前或之后添加新的子部件。这些也可以通过传递已设置给定父部件的子部件来重新排序子部件。
由于 GTK+ 中的许多部件目前使用 XML 形式的复合部件模板,GtkWidget 现在也有自己的 GtkBuildable::add_child() 实现来支持此用例。例如,GtkFileChooserWidget 就使用了该实现,它几乎完全在 XML 中定义。

部件 CSS 名称

由于我们经常需要为任意部件使用任意的 CSS 节点名称,GtkWidget 现在有一个名为 GtkWidget:css-name 的构造时属性,如果指定了该属性,它将用作 CSS 节点名称。如果未指定,则将使用传递给部件的 gtk_widget_class_set_css_name() 调用的名称。如果这两个都没有使用,则 CSS 节点将简单地称为“widget”。

已转换部件的示例

当前 master 分支(和侧分支)中已经有一些部件从使用各种 GtkCssGadgetGdkWindowPangoLayout 转换为使用实际的部件。当然,最终目标是尽可能多地重用部件,从而减少代码大小和维护负担。

GtkSwitch

在 GTK+3 中,GtkSwitchGtkWidget 的直接子类(好耶!),它使用 GdkWindow 进行输入(单击开关将启用/禁用它),一个 GtkCssGadget 用于部件本身,两个 PangoLayout 用于 ON/OFF 文本,另一个 GtkCssGadget 用于滑块。
在 GTK+ master 分支中,开关仍然具有其部件级别的 GtkCssGadget,因此它支持 min-width/min-height CSS 属性和 CSS 边距,但滑块小工具已替换为 GtkButton,并且两个 PangoLayout 已替换为 GtkLabel。这样,我们可以节省大约 300 行 gtkswitch.c 代码。理论上,我们还拥有更多功能,例如可以使用 GtkLabel 支持的文本装饰 CSS 属性的有限支持,但我只是怀疑这是否非常有用。

GtkSpinButton

如前所述,GtkSpinButton 可以轻松地使用实际的 GtkButton 作为向上/向下区域,并且它在 GTK+ master 分支(将在某个时候成为 GTK+4)中这样做。这消除了 gtkspinbutton.c 中的另外 300 行代码。通过使用 GtkButton,旧的图标辅助小工具也变成了实际的 GtkImage 实例。不幸的是,我们必须在这里实现一些 GtkGesture 的魔术,因为 GtkSpinButton 还支持在其按钮上进行中间和右键单击,而 GtkButton::clicked 仅对单个主鼠标按钮单击做出反应。

GtkLevelBar

GtkLevelBar 管理一组块,并为其分配不同的样式类。在 GTK+3 中,这些块都是 GtkCssGadget 实例。它们都是“哑”的,从某种意义上说,它们不做任何特殊的事情——它们只是 CSS 框,就是这样。这就是为什么将其转换为对所有块使用 GtkWidget 并未获得文件大小的太多减少的原因。

 

GtkProgressBar

GtkProgressBar 使用小工具来表示槽和进度节点。它还使用 PangoLayout 来显示百分比或用户给定的字符串。

在 master 分支中,槽和进度都是部件,PangoLayout 当然是 GtkLabel。不必监听 GtkWidget::style-changed(这对于部件会自动完成)并且不必自己绘制 PangoLayout(现在由 GtkLabel 负责),这可以节省大约 200 行代码的大小。

 

GtkExpander

GtkExpander 比看起来更复杂。在 GTK+3 中,它由 2 个 GtkBoxGadget(类似于 GtkBox,但不是部件……)、一个用于标题部件左侧箭头的部件、标题部件和实际内容部件组成。在 master 分支中,这是使用实际的 GtkBox 和一个用于箭头的 GtkIcon(一个内部部件)完成的。我不确定这是否是表达 GtkExpander 功能的最佳方式,例如,我们也可以使用 GtkButton 作为箭头+标题部件的组合。
由于 GtkBoxGadget 几乎已经是完美的 GtkBox 克隆,因此此处的代码节省不是很有趣,但是不必再次监听 GtkWidget::direction-changed 可以节省大约 30 行代码。

意外的 GtkBox 和 GtkButton 子类

GTK+3 包含很多部件,这些部件继承自另一个部件,仅仅是为了看起来和行为像它们一样。这里的问题是,这些部件还会继承父类的所有 API,而这很少是想要的。
对于 GtkBox,GTK+3 中几乎所有子类都是“偶然的”,从某种意义上说,实际将它们用作 GtkBox 没有任何意义,人们通常不会这样做,但是它们必须是 GtkBox 的子类,以满足 GTK+ 的 GtkContainer 要求。此类部件的一个示例是 GtkFileChooserWidget。这已经是存在过的最复杂的部件之一,但是您是否曾经考虑过使用 gtk_container_add()gtk_box_pack_{start,end}() 来向其中添加部件?这没有多大意义。它是一个具有自己 API 的封闭实体。因此,在 GTK+4 中,它将是一个直接的 GtkWidget 子类,其中包含一个 GtkBox。或者可能不是。这只是您不必关心的实现细节。(顺便说一句有趣的事实:GtkFileChooserButton 在 GTK+3 中是一个 GtkBox
同样的情况也适用于 GtkButton。在 GTK+3 中,GtkButton 有许多子类,它们继承了所有 GtkButton 的 API,但实际上并不支持它。 如果你从 GtkLinkButton 中移除子部件会发生什么?如果你设置 GtkFontButtonGtkButton:label 属性会发生什么? 同样,这些都是封闭的实体,它们有自己的 API 来设置和获取各种数据,并根据这些数据来改变行为和/或外观,但这并不意味着它们支持所有 GtkButton/GtkContainer 的那些小技巧。

通用重构规则和未来

对于这项重构工作,我们尝试保持 CSS 节点结构与 GTK+3 中的一致,即我们尽量不破坏目前在 testsuite/css/nodes.c 中的 CSS 节点测试。
GTK+ 内部的一些更复杂的部件仍然严重依赖小工具(gadget),将它们移植为仅使用实际部件将需要大量工作。GtkRange 在历史上是 GTK+ 中最复杂的非容器部件之一。它同时用于滚动条和滑块,因此将其移植到部件可能需要先进行另一轮重构。
另一个有趣的例子是 GtkNotebook,它结合了小工具和部件的使用。例如,在这里我们可以使用一个真正的 GtkStack 来在页面之间切换,并轻松支持页面切换过渡效果。
当然,展望未来的另一个令人兴奋的方向是 Carlos 的 wip/carlosg/event-delivery 分支,它摆脱了大量的 GdkWindow 实例,并使部件输入比以往任何时候都更容易。