<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>开发日志 on Tsic&#39;s Blog</title>
    <link>https://tsic.top/categories/%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/</link>
    <description>Recent content in 开发日志 on Tsic&#39;s Blog</description>
    <generator>Hugo -- 0.154.0</generator>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 15 Mar 2024 17:45:17 +0800</lastBuildDate>
    <atom:link href="https://tsic.top/categories/%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>linux 下使用wayland合成器将插件分进程</title>
      <link>https://tsic.top/post/ui%E6%8F%92%E4%BB%B6%E5%88%86%E8%BF%9B%E7%A8%8B%E6%96%B9%E6%A1%88/</link>
      <pubDate>Fri, 15 Mar 2024 17:45:17 +0800</pubDate>
      <guid>https://tsic.top/post/ui%E6%8F%92%E4%BB%B6%E5%88%86%E8%BF%9B%E7%A8%8B%E6%96%B9%E6%A1%88/</guid>
      <description>&lt;p&gt;介绍如何使用wayland合成器实现UI插件分进程的实现。&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>介绍如何使用wayland合成器实现UI插件分进程的实现。</p>
<h2 id="背景">背景</h2>
<ol>
<li>
<p>为什么要将插件分进程</p>
<p>原有的dde-dock经常因为插件问题导致假死崩溃卡顿等问题，插件与dock在相同进程就避免不了因为插件代码质量等导致的同生共死，所以有将插件独立进程出去将插件的影响范围降低到最小，只影响自己。</p>
</li>
<li>
<p>分进程有什么好处与坏处</p>
<p>分进程后带来的好处可以从以下几个方面体现：</p>
<pre><code> 1. 兼容性：只要确定了插件进程与dock进程通信的方式，可以使用这种通信方式都可以被dock当作插件加载上来。无论widget，qml，qt5/6还是其他语言等。

 2. 流畅性：dock自身更加专注于dock本身的业务逻辑，不需要发生变化时去更新插件的状态，也不会被插件阻塞住。

 3. 稳定性：dock进程只有dock自身，不再受插件的影响。
</code></pre>
<p>缺点方面：</p>
<pre><code> 最主要的一点就是总体的系统资源占用会比之前稍高。
 开发复杂度和维护复杂度都会上升。
</code></pre>
</li>
<li>
<p>怎么去分进程</p>
<p>插件的主要作用：</p>
<pre><code> 1. 用于显示，显示插件的UI部分。
 2. 获取输入，获取用户的鼠标或者键盘输入，并作出对应的响应。
</code></pre>
<p>确定一种通信方式可以让 dock 和插件通信，直接使用 socket 方式不能通信UI相关的信息，共享 buffer 来实现显示 = 换个类似的上层封装，可以接受 UI 相关 &mdash;“显示服务器！”</p>
<p>linux上的显示服务器有xorg，wayland等。采用 wayland 的原因是现代化，方便扩展，并且 Qt 做好了 qt-wayland 的上层封装很方便开发者去开发Wayland相关。</p>
</li>
</ol>
<h2 id="wayland-简单介绍">wayland 简单介绍</h2>
<ol>
<li>
<p>wayland 是什么</p>
<p>wayland 是x11的取代品，wayland是一套应用程序用于获取用户输入和显示的协议。<a href="https://wayland.freedesktop.org/">wayland</a></p>
<p>正好与插件的需求相切合，用于显示和获取输入。</p>
<p>显示和输入的两方面问题</p>
<p>wayland中如何显示？        surface</p>
<p>如何暴露插件属性和插件交互？ shell-surface</p>
</li>
<li>
<p>wayland 有什么好处</p>
<ol>
<li>更现代化的设计</li>
<li>更低的延迟</li>
<li>更好的性能</li>
<li>更好的安全性</li>
<li>更简单的代码库</li>
</ol>
</li>
<li>
<p>shell-surface介绍</p>
<p>显示比较好理解，surface 是wayland中需要渲染的一片区域。</p>
<p>那 shell-surface 是什么呢？</p>
<p>shell-surface 是具有元数据属性的 surface。它可以具有各种属性比如窗管中定义的窗口标题，窗口类型，窗口状态等，还具有布局和排列的能力和交互响应等。</p>
</li>
</ol>
<h2 id="dock-插件接口介绍">dock 插件接口介绍</h2>
<ol>
<li>
<p>dock 提供给插件的接口</p>
<p><a href="https://github.com/linuxdeepin/dde-dock/blob/32bd005492009aaa7e9ea2d59237fa8bf9a1f75c/interfaces/pluginproxyinterface.h">pluginproxyinterface.h</a></p>
</li>
<li>
<p>插件提供给dock的接口</p>
<p><a href="https://github.com/linuxdeepin/dde-dock/blob/32bd005492009aaa7e9ea2d59237fa8bf9a1f75c/interfaces/pluginsiteminterface.h">pluginsiteminterface.h</a></p>
</li>
<li>
<p>之前的工作流程</p>
<p>dock 通过 pluginloader 将插件加载到 pluginmanager，然后对插件进行初始化。</p>
<p>dock 再根据 pluginmanager 加载进来的插件，去加载对应类型区域的部分。</p>
<p>比如：</p>
<ul>
<li>工作区预览（Fixed类型插件） 将显示在 dock 定义的 Fixed 区域</li>
<li>电源按键（system类型插件）显示在 dock 定义的 System 区域</li>
<li>回收站（Tools类型插件）显示在 dock 定义的 Tool 区域</li>
<li>托盘（tray类型）显示在 dock 定义的 Tray 区域</li>
<li>快捷面板将被快捷面板加载，获取到各个插件的快捷面板里面的UI。</li>
</ul>
</li>
<li>
<p>shell-surafce 与 插件的相似之处</p>
<p>插件需要按照一定的顺序显示，shell-surface 可以根据窗口的属性进行布局和排列。</p>
<p>插件需要响应用户的输入，shell-surface 具有交互性和事件处理。</p>
<p>所以接下来需要定一个自定义类型的 shell surface 来实现dock 插件管理的需求。</p>
</li>
</ol>
<h2 id="通过-shell-surface-实现插件加载和通信流程">通过 shell surface 实现插件加载和通信流程</h2>
<ol>
<li>
<p>wayland 协议介绍</p>
<p>wayland 是一套应用程序用于获取用户输入和显示的协议。<a href="https://wayland.freedesktop.org/">wayland</a></p>
<p>现有的wayland显示协议都不满足dock插件管理的要求，所以定义一套dock的插件管理协议，用于dock插件的显示和响应。</p>
</li>
<li>
<p>确定对应的shell-surface和对应的wayland协议</p>
<p>根据上面的内容，输入转换</p>
<pre><code> 插件向dock的接口    -&gt;      wayland request
 dock向插件的接口    -&gt;      wayland event
</code></pre>
<p>得出下面一份简单的 <a href="https://github.com/linuxdeepin/dde-shell/blob/7d3437492afc37c2d5a6bd908fb7a7fce501581f/panels/dock/dockplugin/protocol/dock-plugin-manager-v1.xml">Wayland 协议</a>，仍有需要补充部分</p>
</li>
<li>
<p>制定协议后的工作流程</p>
<ol>
<li>
<p>加载流程</p>
<ol>
<li>
<p>widget 插件由 widget 的loader进行初始化</p>
</li>
<li>
<p>实现一个 widgetplugin 将初始化后的插件和协议的具体实现连接起来。</p>
</li>
<li>
<p>将 每个widget 初始化称独立窗口，使用窗口的显示和隐藏控制 shell-surface 的创建和隐藏达到控制插件的注册后的显示与否。</p>
</li>
</ol>
</li>
<li>
<p>通信流程</p>
<ol>
<li>
<p>插件通过 wayland 的request 向 dock 发送插件提供的接口</p>
</li>
<li>
<p>dock 通过 wayland 的 event 向插件发送 dock 提供的能力</p>
</li>
</ol>
<p>比如点击事件：</p>
<pre><code>  1. 点击 dock 上面的 item， item 通过 wayland 产生一个自定的 event

  2. 该 event 通过 wayland 发送到协议的客户端实现，调用对应的event 处理接口，将事件发送到 widgetplugin实现中。

  3. widgetplugin收到转发后的 点击事件，调用插件对应的点击接口

  4. 插件响应对应的点击行为。
</code></pre>
</li>
<li>
<p>通过 wayland 后的插件时序图</p>
</li>
</ol>
<p><img alt="时序图" loading="lazy" src="https://s2.loli.net/2024/03/18/bVyNdEwWLQBtZTo.png"></p>
</li>
</ol>
<h2 id="使用qt-wayland-实现协议">使用Qt wayland 实现协议</h2>
<ol start="0">
<li>
<p>工具 qtwayland-scanner 根据协议生成胶水代码</p>
<p>1.1 客户端自动填充好 request 相关接口的调用逻辑，需要实现收到对应 event 后的处理和 request 如何暴露对外的方式。</p>
<p>对于 dock 插件来说， event 一般就是 dock 的状态变更，将其关联到对应的信号处理即可，将协议定义的 request 与旧插件接口相关的调用关联起来即可。</p>
<p>1.2 服务端自动填充好 event 相关接口的调用逻辑，需要实现收到对应的 reqeust 后的处理逻辑和 event 暴露对外的方式。</p>
</li>
<li>
<p>server端（也就是dock本体的合成器）实现</p>
<p>server 是作为一个 QWaylandCompositorExtension 实现，需要将PluginManager暴露到QML中用于注册到 wayland 合成器中作为受支持的协议。并且需要关联 dock 对应的属性到上面，用于发送对应的 dock 状态变化后的事件。</p>
<p>通过 Qt的 QML_ELEMENT 和 Q_COMPOSITOR_DECLARE_QUICK_EXTENSION_CLASS()
宏注册到 QML 中作为QWaylandCompositorExtension</p>
<p>PluginSurface 是由 PluginManager 控制创建，不需要 QML 中创建暴露，但是 PluginSurface 需要响应对应的输入行为，所以一些输入行为接口需要设置成 Q_INVOKABLE 由 QML 直接调用。</p>
<p>细节：</p>
<ol>
<li>
<p>实现 DockPluginManager</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DockPluginManager</span> <span class="o">:</span> <span class="k">public</span> <span class="n">QWaylandCompositorExtensionTemplate</span><span class="o">&lt;</span><span class="n">DockPluginManager</span><span class="o">&gt;</span><span class="p">,</span> <span class="k">public</span> <span class="n">QtWaylandServer</span><span class="o">::</span><span class="n">dock_plugin_manager_v1</span>
</span></span></code></pre></div></li>
<li>
<p>实现胶水代码中的虚函数，暴露属性并关联到 event 上，用于向插件通知 dock 的状态变更。</p>
</li>
<li>
<p>实现 surface 创建流程，根据 QWaylandSurface 和 QWaylandResource 创建 PluginSurface。</p>
</li>
<li>
<p>实现 PluginSurface</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PluginSurface</span> <span class="o">:</span> <span class="k">public</span> <span class="n">QWaylandShellSurfaceTemplate</span><span class="o">&lt;</span><span class="n">PluginSurface</span><span class="o">&gt;</span><span class="p">,</span> <span class="k">public</span> <span class="n">QtWaylandServer</span><span class="o">::</span><span class="n">dock_plugin_surface</span>
</span></span></code></pre></div></li>
<li>
<p>定义对应的属性和对外暴露，用于接收插件上报的属性。</p>
</li>
<li>
<p>定义对应的信号，用于插件发生对应的 request 后转发给 dock。</p>
</li>
<li>
<p>实现对外的接口，将输入事件转成对应的 event 发送给插件。</p>
</li>
</ol>
</li>
<li>
<p>client端（也就是插件）实现</p>
<p>由于 widget 插件是使用 Qt5 编写，所以注册方式采用 Qt5 的方式。</p>
<p>将 PluginManager 通过 QWaylandShellIntegration 和 QWaylandShellIntegrationPlugin 注册到 wayland 的createSurface上。</p>
<p>当有对应类型的 surface 创建时，便会调用里面的接口。实现 PluginSurface 的创建和 wayland 合成器端的bingding。</p>
<p>widget 插件将通过 客户端封装的接口来调用 实现后的request 和接受对应的 event。</p>
<p>细节：</p>
<ol>
<li>
<p>作为一个 wayland-shell-integration 插件实现</p>
</li>
<li>
<p>实现一个 QWaylandShellIntegrationPlugin 和 QWaylandShellIntegration 将 DockPluginManager 的创建和 PluginSurface 的创建注册到 wayland 中</p>
</li>
<li>
<p>定义 DockPlugin 作为 QWindow 和 shell-surafce 的桥梁。QWindow， DockPlugin 和 DockPluginSurface 一一对应。</p>
</li>
<li>
<p>实现 DockPluginManager</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DockPluginManager</span> <span class="o">:</span> <span class="k">public</span> <span class="n">QObject</span><span class="p">,</span> <span class="k">public</span> <span class="n">QtWayland</span><span class="o">::</span><span class="n">dock_plugin_manager_v1</span>
</span></span></code></pre></div></li>
<li>
<p>将 event 转成对应的信号对外暴露，实现属性关联 request。</p>
</li>
<li>
<p>实现 createPluginSurface 接口，创建DockPluginSurface</p>
</li>
<li>
<p>实现 DockPluginSurface 并根据 DockPlugin 将对应的 QWindow 关连起来。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c++" data-lang="c++"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DockPluginSurface</span> <span class="o">:</span> <span class="k">public</span> <span class="n">QtWaylandClient</span><span class="o">::</span><span class="n">QWaylandShellSurface</span><span class="p">,</span> <span class="k">public</span> <span class="n">QtWayland</span><span class="o">::</span><span class="n">dock_plugin_surface</span>
</span></span></code></pre></div></li>
<li>
<p>将 event 转成对应的信号暴露给 DockPlugin，并将 DockPlugin 的信号关联到 DockPluginSurface 调用 request 的槽上。</p>
</li>
</ol>
</li>
<li>
<p>widget 插件的接口转到实现的协议上</p>
<p>widget 插件是一个旧接口对接到新协议的中间类。他通过实现 proxy 的接口来让插件认为它就是 dock 本体。又通过和 dock 插件管理协议的关联，将插件的请求通过 wayland 转发给 dock。</p>
<p>在 main 中通过 PluginLoader 将插件进行加载，成功加载后构造对应的widgetplugin 对象， widgetplugin 将旧接口 pluginsiteminterface 和 pluginproxyinterface 通过 DockPlugin 接口类与协议关联起来，实现将对应的 wayland event/request 转成 dock 的接口相关。</p>
</li>
<li>
<p>dock 又是如何&quot;抓取&quot;插件</p>
<p>插件注册到 wayland 合成器后，在dock 的 DockCompositoer 中根据 surface 中定义的 type 属性，将 surface 存入到不同的 Model 中。然后 dock 对应的部分（比如tray部分）将对应的Model（traySurfaceModel）用于 tray 区域的显示，并使用 Qt 提供的 ShellSurfaceItem 将对应的  surface 显示出来。在 dock 上的布局和排列就可以按照 shell-surface 上定义的属性来实现。</p>
<p>但是对于插件的 tooltip 和 popup 处理特殊一下，主要有两种思路：</p>
<ol>
<li>
<p>tooltip 和 popup 按需提供，需要将对应的 hover 事件转到客户端，然后调用插件接口产生 tooltip/popup 的窗口出来，然后将该窗口贴到 tooltip 和 popup 上去。</p>
</li>
<li>
<p>tooltip 和popup 上来就注册到wayland中，dock 按需将对应的 item 贴到 dock 的 tooltip/popup 上去。</p>
</li>
</ol>
<p>目前采用的是第二种方案，但是对于网络插件自身有判断，不能同时产生 tooltip 和 popup，两者只可存在一个，导致目前网络插件没有对应的 tooltip 产生。</p>
</li>
</ol>
<h2 id="实现后的效果">实现后的效果</h2>
<p>dde-shell 中插件与dock分进程，目前托盘插件正常显示和响应对应输入，tooltip和popup正常处理。</p>
<p>所有插件各自独立进程
<img alt="效果图" loading="lazy" src="https://s2.loli.net/2024/03/18/inZueY643Cha1HE.png"></p>
<h2 id="畅想和待做">畅想和待做</h2>
<ol>
<li>
<p>对于有输入插件（比如网络插件，输入情况的支持需要确认，应该是需要添加输入法协议的支持）</p>
</li>
<li>
<p>不同类型的插件需要准备多种loader</p>
</li>
</ol>
<p>待做：</p>
<ol>
<li>实现一个 QML loader 并完善 QML的支持，用于编写 QML 插件。</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
