到目前为止,我们已经成功地在顶级应用程序窗口中显示了一些内容,但XDG shell的功能远不止于此。即使是最简单的应用程序也应该正确地实现配置生命周期,而xdg-shell
为更复杂的应用程序提供了有用的功能。xdg-shell功能的完整范围包括窗口大小、多窗口层次结构、客户端装饰和上下文菜单等窗口的语义定位的客户端/服务器协商。
10.1 配置与生命周期
以前,我们创建了一个我们选择固定大小为640x480的窗口。然而,合成器通常会对我们的窗口应该采用什么大小有自己的看法,我们也可能想要传达我们的偏好。如果不这样做,通常会导致不良行为,比如你的窗口的一部分被一个试图告诉你让你的表面变小的合成器切断了。
合成器可以向应用程序提供关于其显示上下文的额外线索。它可以让你知道你的应用程序是否最大化或全屏,是否在一个或多个方面与其他窗口或显示器的边缘对齐,是否聚焦或空闲等等。由于wl_surface
用于从客户端到服务器的原子通信,因此xdg_surface
接口提供了以下两条消息,供合成器建议更改和客户端确认它们。
<request name="ack_configure">
<arg name="serial" type="uint" />
</request>
<event name="configure">
<arg name="serial" type="uint" />
</event>
仅凭这些消息本身意义不大。然而,xdg_surface
的每个子类(xdg_toplevel
和xdg_popup
)都有额外的事件,服务器可以在“configure”之前发送这些事件,以使上述所有建议都有意义。服务器将发送所有状态;最大化、聚焦、建议的大小;然后是一个带有序列号的configure事件。当客户端假定与这些建议一致的状态时,它会发送具有相同序列号的ack_configure
请求来表示这一点。在关联的wl_surface
的下一个提交时,合成器将认为状态是一致的。
XDG Top-level 生命周期
我们第七章的示例代码可以运行,但它现在不是桌面环境的最佳成员。它没有采用合成器推荐的尺寸,如果用户试图关闭窗口,它也不会消失。响应这些由合成器提供的事件涉及到两个Wayland事件:configure和close。
<event name="configure">
<arg name="width" type="int"/>
<arg name="height" type="int"/>
<arg name="states" type="array"/>
</event>
<event name="close" />
宽度和高度是合成器为窗口推荐的尺寸,而states是一个包含以下值的数组:
<enum name="state">
<entry name="maximized" />
<entry name="fullscreen" />
<entry name="resizing" />
<entry name="activated" />
<entry name="tiled_left" />
<entry name="tiled_right" />
<entry name="tiled_top" />
<entry name="tiled_bottom" />
</enum>
close事件可以被忽略,一个典型的原因是向用户显示确认以保存他们未保存的工作。我们第七章的示例代码可以很容易地更新以支持这些事件:
diff --git a/client.c b/client.c
--- a/client.c
+++ b/client.c
@@ -70,9 +70,10 @@ struct client_state {
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
/* State */
- bool closed;
float offset;
uint32_t last_frame;
+ int width, height;
+ bool closed;
};
static void wl_buffer_release(void *data, struct wl_buffer *wl_buffer) {
@@ -86,7 +87,7 @@ static const struct wl_buffer_listener wl_buffer_listener = {
static struct wl_buffer *
draw_frame(struct client_state *state)
{
- const int width = 640, height = 480;
+ int width = state->width, height = state->height;
int stride = width * 4;
int size = stride * height;
@@ -124,6 +125,32 @@ draw_frame(struct client_state *state)
return buffer;
}
+static void
+xdg_toplevel_configure(void *data,
+ struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height,
+ struct wl_array *states)
+{
+ struct client_state *state = data;
+ if (width == 0 || height == 0) {
+ /* Compositor is deferring to us */
+ return;
+ }
+ state->width = width;
+ state->height = height;
+}
+
+static void
+xdg_toplevel_close(void *data, struct xdg_toplevel *toplevel)
+{
+ struct client_state *state = data;
+ state->closed = true;
+}
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+ .configure = xdg_toplevel_configure,
+ .close = xdg_toplevel_close,
+};
+
static void
xdg_surface_configure(void *data,
struct xdg_surface *xdg_surface, uint32_t serial)
@@ -163,7 +190,7 @@ wl_surface_frame_done(void *data, struct wl_callback *cb, uint32_t time)
cb = wl_surface_frame(state->wl_surface);
wl_callback_add_listener(cb, &wl_surface_frame_listener, state);
- /* Update scroll amount at 8 pixels per second */
+ /* Update scroll amount at 24 pixels per second */
if (state->last_frame != 0) {
int elapsed = time - state->last_frame;
state->offset += elapsed / 1000.0 * 24;
@@ -217,6 +244,8 @@ int
main(int argc, char *argv[])
{
struct client_state state = { 0 };
+ state.width = 640;
+ state.height = 480;
state.wl_display = wl_display_connect(NULL);
state.wl_registry = wl_display_get_registry(state.wl_display);
wl_registry_add_listener(state.wl_registry, &wl_registry_listener, &state);
@@ -227,6 +256,8 @@ main(int argc, char *argv[])
state.xdg_wm_base, state.wl_surface);
xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, &state);
state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);
+ xdg_toplevel_add_listener(state.xdg_toplevel,
+ &xdg_toplevel_listener, &state);
xdg_toplevel_set_title(state.xdg_toplevel, "Example client");
wl_surface_commit(state.wl_surface);
如果你再次编译并运行这个客户端,你会注意到它比以前表现得更好。
请求状态更改
客户端还可以请求合成器将客户端置于这些状态之一,或者对窗口的大小施加约束。
<request name="set_max_size">
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<request name="set_min_size">
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<request name="set_maximized" />
<request name="unset_maximized" />
<request name="set_fullscreen" />
<arg name="output"
type="object"
interface="wl_output"
allow-null="true"/>
</request>
<request name="unset_fullscreen" />
<request name="set_minimized" />
合成器通过发送相应的configure事件来表示对请求的确认。
1 这考虑到了客户端通过set_window_geometry请求发送的窗口几何形
状。建议的大小只包括由窗口几何形状表示的空间。
10.2 Popup和父窗口
弹窗
当设计利用应用程序窗口的软件时,很多情况下都会使用较小的次级表面用于各种目的。一些示例包括在右键单击时出现的上下文菜单、从多个选项中选择值的下拉框、当你将鼠标悬停在UI元素上时显示的上下文提示,或沿着窗口顶部和底部的菜单和工具栏。这些通常会被嵌套,例如,通过遵循类似于“文件→最近文档→Example.odt”的路径。
对于Wayland,XDG shell为管理这些窗口提供了设施:弹出窗口。我们之前看过xdg_surface的“get_toplevel”请求,用于创建顶级应用程序窗口。对于弹出窗口,则会使用“get_popup”请求。
<request name="get_popup">
<arg name="id" type="new_id" interface="xdg_popup"/>
<arg name="parent" type="object" interface="xdg_surface" allow-null="true"/>
<arg name="positioner" type="object" interface="xdg_positioner"/>
</request>
前两个参数相当直观,但第三个参数引入了一个新概念:定位器。定位器的目的是,如其名称所示,定位新的弹出窗口。这允许合成器利用其特权信息参与弹出窗口的定位,例如避免弹出窗口延伸到显示器边缘。我们将在第10.4章讨论定位器。现在,你可以简单地创建一个并传递它,而无需进一步配置即可实现相当合理的默认行为,利用适当的xdg_wm_base请求:
<request name="create_positioner">
<arg name="id" type="new_id" interface="xdg_positioner"/>
</request>
所以,简而言之,我们可以:
- 创建一个新的wl_surface
- 为其获取一个xdg_surface
- 创建一个新的xdg_positioner,为第10.4章保存其配置
- 从我们的XDG表面和XDG定位器创建一个xdg_popup,将其父级分配给之前创建的xdg_toplevel。
然后我们可以渲染并将缓冲区附加到我们的弹出窗口表面,与之前讨论的生命周期相同。我们还可以访问一些其他特定于弹出窗口的功能。
配置
与XDG顶级配置事件一样,合成器也有一个事件,它可以使用该事件建议您的弹出窗口应假定的大小。然而,与顶级窗口不同,这还包括一个定位事件,该事件通知客户端相对于其父级表面的弹出窗口的位置。
<event name="configure">
<arg name="x" type="int"
summary="x position relative to parent surface window geometry"/>
<arg name="y" type="int"
summary="y position relative to parent surface window geometry"/>
<arg name="width" type="int" summary="window geometry width"/>
<arg name="height" type="int" summary="window geometry height"/>
</event>
客户端可以使用XDG定位器来影响这些值,这将在第10.4章中讨论。
弹出窗口抓取
弹出窗口通常希望“抓取”所有输入,例如允许用户使用箭头键选择不同的菜单项。这可以通过“抓取”请求实现:
<request name="grab">
<arg name="seat" type="object" interface="wl_seat" />
<arg name="serial" type="uint" />
</request>
此请求的先决条件是已经接收了符合条件的输入事件,例如右键单击。该输入事件的序列号应在此请求中使用。这些语义在第9章中有详细介绍。合成器稍后可以取消此抓取,例如当用户按下“Esc”键或单击你的弹出窗口之外时。
取消
在这些情况下,如果合成器取消了你的弹出窗口,例如按下“Esc”键时,将发送以下事件:
<event name="popup_done" />
为了避免竞争条件,合成器将弹出窗口结构保存在内存中,并在其被取消后继续为它们提供服务。有关对象生命周期和竞争条件的更多详细信息,请参阅第2.4章。
销毁弹出窗口
客户端销毁一个弹出窗口相当简单:
<request name="destroy" type="destructor" />
但是,有一个细节值得注意:你必须从顶部开始销毁所有弹出窗口。在任何给定时刻,你唯一可以销毁的弹出窗口是最顶层的那个。如果你不这样做,你将因协议错误而被断开连接。
10.3 交互式移动和调整大小
move 和 resize
许多应用程序窗口具有用户可以用来拖动或调整窗口大小的交互式UI元素。许多Wayland客户端默认情况下期望负责自己的窗口装饰以提供这些交互元素。在X11上,应用程序窗口可以独立定位在屏幕上的任何位置,并利用这一点来促进这些交互。
然而,Wayland的故意设计特性使应用程序窗口不知道它们在屏幕上或与其他窗口的相对位置。这个决定赋予Wayland组合器更大的灵活性-窗口可以同时显示在几个地方,安排在VR场景的3D空间中,或以任何其他新颖的方式呈现。Wayland旨在是通用的,并广泛适用于许多设备和外形因素。
为了平衡这两个设计需求,XDG顶级层提供了两个请求,可以用来要求组合器开始交互式移动或调整大小操作。界面的相关部分是:
<request name="move">
<arg name="seat" type="object" interface="wl_seat" />
<arg name="serial" type="uint" />
</request>
与前一章中解释的弹出窗口创建请求一样,您需要提供输入事件的序列以开始交互操作。例如,当您收到鼠标按钮事件时,您可以使用该事件的序列开始交互移动操作。合成器将接管此操作,并在其内部坐标空间中开始对您的窗口进行交互操作。
由于需要指定窗口的哪些边缘或角参与操作,因此调整大小要复杂一些。
<enum name="resize_edge">
<entry name="none" value="0"/>
<entry name="top" value="1"/>
<entry name="bottom" value="2"/>
<entry name="left" value="4"/>
<entry name="top_left" value="5"/>
<entry name="bottom_left" value="6"/>
<entry name="right" value="8"/>
<entry name="top_right" value="9"/>
<entry name="bottom_right" value="10"/>
</enum>
<request name="resize">
<arg name="seat" type="object" interface="wl_seat" />
<arg name="serial" type="uint" />
<arg name="edges" type="uint" />
</request>
但是,除此之外,它的功能大致相同。如果用户点击并拖动窗口的左下角,您可能想要发送一个带有相应座位和序列的交互式调整大小请求,并将边缘参数设置为bottom_left。
对于完全实现交互式客户端窗口装饰,客户端还需要发送一个额外的请求:
<request name="show_window_menu">
<arg name="seat" type="object" interface="wl_seat" />
<arg name="serial" type="uint" />
<arg name="x" type="int" />
<arg name="y" type="int" />
</request>
一个上下文菜单,提供窗口操作,如关闭或最小化窗口,通常在点击窗口装饰时出现。对于由客户端管理窗口装饰的客户端,这有助于将客户端驱动的交互与合成器驱动的元操作(如最小化窗口)联系起来。如果您的客户端使用客户端装饰,您可以使用此请求来实现此目的。
xdg-decoration
在讨论客户端装饰的行为时,最后需要提及的细节是管理其使用的协议。不同的Wayland客户端和服务器可能对使用服务器端或客户端窗口装饰有不同的偏好。为了表达这些意图,使用了一个协议扩展:xdg-decoration。它可以在wayland-protocols中找到。该协议提供了一个全局:
<interface name="zxdg_decoration_manager_v1" version="1">
<request name="destroy" type="destructor" />
<request name="get_toplevel_decoration">
<arg name="id" type="new_id" interface="zxdg_toplevel_decoration_v1"/>
<arg name="toplevel" type="object" interface="xdg_toplevel"/>
</request>
</interface>
您可以将您的xdg_toplevel对象传递到get_toplevel_decoration请求中,以获取具有以下接口的对象:
<interface name="zxdg_toplevel_decoration_v1" version="1">
<request name="destroy" type="destructor" />
<enum name="mode">
<entry name="client_side" value="1" />
<entry name="server_side" value="2" />
</enum>
<request name="set_mode">
<arg name="mode" type="uint" enum="mode" />
</request>
<request name="unset_mode" />
<event name="configure">
<arg name="mode" type="uint" enum="mode" />
</event>
</interface>
set_mode请求用于表达客户端的偏好,unset_mode用于表达无偏好。合成器然后使用配置事件告诉客户端是否使用客户端装饰。有关更多详细信息,请参阅完整的XML。
10.4 Positioners
当我们几页前介绍弹出窗口时,我们注意到在创建弹出窗口时,必须提供一个定位器对象。我们要求您不要担心,只需使用默认值即可,因为它是一个复杂的接口,并且与主题无关。现在,我们将深入探讨这个复杂的接口。
当您打开一个弹出窗口时,它会显示在一个窗口系统中,该系统具有您的客户端不了解的约束。例如,Wayland客户端不知道其窗口在屏幕上的显示位置。因此,如果您右键单击一个窗口,客户端不具备必要的信息来确定由此产生的弹出窗口最终可能会从屏幕边缘运行。定位器的设计是为了解决这些问题,它允许客户端指定在如何移动或调整弹出窗口大小时的某些约束,然后由合成器完全掌握事实,最终决定如何适应。
基础知识
<request name="destroy" type="destructor"></request>
这会在您完成时销毁定位器。您可以在弹出窗口创建后调用此方法。
<request name="set_size">
<arg name="width" type="int" />
<arg name="height" type="int" />
</request>
set_size请求用于设置正在创建的弹出窗口的大小。
使用定位器的所有客户端都将使用这两个请求。现在,让我们来看看有趣的部分。