Makepad 本周进展报告 #20250421

作者: AlexZhang

基于 4 月 16 日到 4 月 20 日的讨论和一周内PR 进行总结。

本周是富有成效的一周,本周 Makepad 团队在多个方面取得了进展,包括平台特性、核心控件的修复与改进、性能优化以及底层渲染架构的调整。

关键进展包括解决了 iOS 平台上的手势交互难题、修复了 Dock 状态加载的核心 Bug、推进了 Android XR 的底层支持。同时,团队也在持续进行性能分析与优化(文本、字体栈),并修复各种控件和平台相关的问题。对 CI 的规划也预示着未来开发流程的改进。一些已知的小问题(如 PortalList 警告、Html 链接解析)已被识别并等待后续处理。

进展报告

一、 平台特性与修复

  1. iOS 长按手势支持 (#715):
    • Kevin 成功为 iOS 添加了长按手势识别功能。
    • 期间遇到了一个挑战:iOS 的 UIGestureRecognizer 在识别手势后会“接管”事件流,阻止底层视图(Makepad 的 MTKView)接收后续的触摸事件(如 FingerUp),直到手势完全结束(手指抬起)。这给在手势进行中(手指未抬起但长按已满足条件)触发 Makepad 内部事件带来了困难。
    • 最终通过设置 cancelsTouchesInView 属性为 false 解决了这个问题,允许底层视图在手势识别后继续接收触摸事件。
  2. Android XR 支持推进 (Rik):
    • Rik 在 Android XR 支持方面取得了进展,这涉及到将渲染后端升级到 OpenGL ES 3.0。
    • 为了满足 XR 渲染(特别是多视图渲染)的需求,Rik 开始为 OpenGL 后端添加统一缓冲区 (Uniform Buffers) 的支持。这预计也能提升非 XR 场景下的渲染性能。
  3. 窗口管理改进 (#693):
    • Alan 贡献了跨平台调整窗口大小/位置的功能改进。
  4. 构建与平台修复:
    • 修复了 Linux X11 平台的构建问题 (#710)。
    • 修复了 Android 上长按事件坐标未使用 DPI 因子的问题 (#708)。
    • 添加了与 IME(输入法编辑器)相关的底层支持 (#696)。

二、 核心控件修复与改进

  1. Dock 状态加载 Bug 修复 (#711):

    • Alan 发现并修复了一个关键问题:当加载之前保存的 Dock 布局状态时,由于新应用实例中唯一 LiveId 计数器重置,可能导致动态创建的 Splitter 或 Tab 的 ID 与加载的 ID 冲突,从而覆盖旧布局。
    • 解决方案是在 load_state 时,将加载进来的、原先是唯一 ID 的 LiveId 转换为基于字符串的新 LiveId,避免了冲突。Kevin 请求 Rik 对此方案进行了审查。
  2. PortalList / DrawList 问题:

    • smooth_scroll_to_end 函数得到改进,现在内部调用 smooth_scroll_to (#691)。
    • Julian 报告了一个关于 DrawListId 生成索引错误的警告,该警告在使用不同模板渲染相同 item_idPortalList 时出现。虽然目前看起来不影响功能,但团队讨论了调试方法(panic、#[track_caller])以追踪来源。
  3. Button 状态 Bug 修复 (#718):

    • Kevin 修复了一个 Button 的小 Bug:当鼠标悬停在按钮上时按钮被禁用,鼠标移出后 hover 状态未被正确处理的问题。
  4. CommandTextInput 改进:

    • AlexZhang 修复了 DSL 中的一个小错误 (#709)。
    • 区分了鼠标模式和键盘模式,提升了交互体验 (#707)。
  5. Html Widget 链接解析问题 (Kevin):

    • 发现 <Html> 控件在处理嵌套在其他标签(如 <code>)内的自定义子控件(如 <a>)时,链接内容无法正确解析和渲染。初步判断是 <Html> 控件对子控件内容处理方式的问题,而非底层 HTML 解析器的问题。

三、 性能与底层

  1. 文本栈性能 (Rik):
    • Rik 提到发现并正在修复文本栈(TextStack)中的一些性能退步问题(描述为“愚蠢的 bug”)。
  2. 字体栈性能 (Eddy):
    • Eddy 指出新的字体栈在判断 glyph 是否已在 atlas 中时存在一个已知的性能问题(需要创建中间数据结构),并计划在本周修复。
  3. OpenGL ES 3.0 与 Uniform Buffers (Rik):
    • 为支持 Android XR,正在将 OpenGL 后端升级至 ES 3.0,并添加 Uniform Buffer 支持,这有望提升整体渲染性能。

四、 基础设施与其他

  1. CI 计划 (Rik):
    • Rik 计划设置基础的持续集成 (CI),至少确保 PR 在所有平台上都能通过 cargo makepad check all 检查。
  2. 讨论:
    • 团队就事件命名("pointer" vs "finger")和 NIH(非我发明)综合症进行了简短讨论。
    • 强调了拥有源代码(如 Android)相比于闭源平台(如 Apple)在解决底层问题时的优势。

拓展阅读

事件命名讨论总结 ("pointer" vs "finger")

Makepad 的事件系统(如 Hit 枚举)中使用了 FingerDownFingerUpFingerHoverIn 等术语来描述触摸和鼠标交互。

讨论点:  

Rik 提到,未来可能需要考虑非人类用户(比如 AI 或“长着触手的异形霸主” - 这是一种幽默的说法)与 UI 交互的可能性。在这种情况下,“finger”(手指)这个词可能就不太合适了。Rik 指出,Web 标准制定组织 W3C 使用了更通用的术语 "pointer"(指针)来涵盖鼠标、触摸、笔等各种输入方式。他认为 W3C 在这方面考虑得更周全。

结论 (推测): 

虽然没有明确决定立即修改,但讨论表明团队意识到了 "finger" 的局限性,并认可 "pointer" 作为更通用、更具未来兼容性的术语的价值。这可能意味着未来 Makepad 的事件系统可能会向 "pointer" 事件迁移,以更好地适应多样化的输入和交互方式。

NIH 综合症 (Not Invented Here Syndrome)

NIH 综合症,即“非我发明综合症”,是一种文化或组织现象,指的是开发者或组织倾向于避免使用、信任或购买来自外部(第三方)的现有产品、研究、标准或知识,即使它们是现成可用且可能更优的解决方案,而倾向于自己重新开发或“重新发明轮子”

讨论中的体现:

Kevin 在处理 iOS 手势识别器时遇到了困难,因为苹果提供的 UIGestureRecognizer 行为复杂且有些“霸道”(会阻止事件传递给底层视图)。他担心可能需要自己从头实现一个手势识别器来获得所需的控制权。

Rik 表达了对必须自己实现底层功能的担忧(“the hard cost of NIH”),但也强调了 Makepad 的一个优势:如果现有的(苹果的)方案无法满足需求,他们 可以 选择自己实现(“atleast we CAN choose”, “cherish the thought that we CAN”)。他将此与 Web 开发进行对比,Web 开发者通常受限于浏览器提供的 API,无法自行修改底层行为(“on web you cant choose”)。

 Kevin 承认自己也有 NIH 倾向(“I too suffer from NIH syndrome”),因为他过去曾开发过自己的操作系统。但他认为手势识别这种相对高层的功能 不应该 属于必须自己重新发明的范畴,暗示苹果应该提供更好的 API 或定制性。

这段讨论反映了开发者在面对外部平台或库的限制时的常见困境:

  • 优点: 自己实现可以获得完全的控制权,确保满足特定需求,避免外部依赖带来的问题。
  • 缺点: 自己实现耗时耗力,可能引入新的 bug,并且错过了利用现有成熟方案的机会。

Makepad 的立场 (推测): 

讨论表明 Makepad 团队倾向于在可能的情况下利用平台提供的功能,但保留在必要时进行底层定制甚至重新实现的能力,以确保最终的用户体验和框架的灵活性。他们认识到 NIH 的成本,但也将“能够选择自研”视为一种优势。

总的来说,NIH 综合症描述了一种“闭门造车”的心态,而 Makepad 团队在讨论中展现了对这种心态的警惕,同时也珍视在必要时进行自主开发的自由度。

苹果UIGestureRecognizer 的核心机制

苹果的 UIGestureRecognizer 是一套强大的框架,用于识别用户在触摸屏上的各种手势,如点击 (Tap)、长按 (Long Press)、平移 (Pan)、捏合缩放 (Pinch)、旋转 (Rotation) 和轻扫 (Swipe)。

它的核心思想是:

  1. 附加到视图 (View): 你可以将一个或多个手势识别器附加到一个 UIView 上。
  2. 监听触摸事件: 手势识别器会监听附加到的视图及其子视图接收到的原始触摸事件 (UITouch 对象,包含触摸的开始、移动、结束、取消等信息)。
  3. 状态机: 每个手势识别器内部维护一个状态机。它根据接收到的触摸事件序列来判断手势是否正在进行、是否已经成功识别、是否失败或被取消。主要状态包括:
    • Possible: 初始状态,手势可能发生。
    • Began: 手势已经开始(例如,长按已达到足够时间,或平移已移动足够距离)。
    • Changed: 手势正在进行中且状态发生变化(例如,手指正在平移或捏合)。
    • Ended / Recognized: 手势成功完成(例如,手指抬起完成了一次点击或平移)。
    • Cancelled: 手势被系统或其他手势识别器取消。
    • Failed: 触摸序列不符合手势要求。
  4. 触发动作 (Action): 当手势识别器的状态发生改变(特别是识别成功或进行中)时,它可以触发你预先设定的目标-动作 (Target-Action) 方法或闭包。

“接管事件流”的行为 (默认情况)

对话中提到的关键行为是 UIGestureRecognizer 默认会“接管”事件流。具体来说:

  1. 触摸事件传递: 通常情况下,触摸事件会先传递给手势识别器,然后再(可能)传递给视图本身(通过 touchesBegan:, touchesMoved:, touchesEnded: 等方法)。
  2. 识别成功后的拦截: 一旦某个手势识别器成功识别了手势(状态变为 BeganRecognized/Ended),默认情况下,它会阻止后续的原始触摸事件传递给该视图及其子视图。 这就是 Kevin 提到的“take over the event flow and do not allow the underlying view to receive future events”。
  3. 目的: 这种设计的目的是为了避免冲突和提供清晰的交互模型。例如,如果一个视图同时有单击手势和拖动手势,一旦系统识别出用户正在拖动,就不应该再将后续的触摸移动事件解释为单击的一部分,也不应该让视图自己处理这些拖动事件(因为手势识别器已经在处理了)。

cancelsTouchesInView 属性的作用

Kevin 后来发现的 cancelsTouchesInView 属性是解决上述问题的关键。

  • cancelsTouchesInView = true (默认值): 当手势被识别时,所有当前正在进行的触摸事件会被标记为“取消”(发送 touchesCancelled: 消息给视图),并且后续的触摸事件将不再传递给视图,直到这个手势结束。这就是导致 Makepad 底层视图收不到 FingerUp 等事件的原因。
  • cancelsTouchesInView = false: 当手势被识别时,触摸事件仍然会继续传递给视图。这意味着视图的 touchesBegan:, touchesMoved:, touchesEnded: 等方法仍然会被调用,即使手势识别器也在同时处理这些触摸。

为什么设置 cancelsTouchesInView = false 对 Makepad 很重要?

Makepad 有自己的事件处理系统,它需要接收完整的触摸事件序列(按下、移动、抬起)来驱动其内部的 Hit 检测和 Widget 事件分发逻辑。如果 iOS 的手势识别器在识别手勢(如长按)后就拦截了后续的 touchesEnded 事件,Makepad 就无法知道手指何时抬起,导致无法触发对应的 FingerUp 事件,进而可能导致 UI 状态不一致或交互中断。

通过将 cancelsTouchesInView 设置为 false,Kevin 允许 Makepad 的底层视图(MTKView)即使在长按手势被 iOS 识别后,也能继续接收到原始的触摸事件(包括手指抬起的事件),从而能够正确地生成和处理 Makepad 内部的 FingerUp 事件。

苹果的 UIGestureRecognizer 是一个强大的状态机,用于识别复杂手势。其默认行为是在识别手势后“接管”并取消传递给视图的原始触摸事件,以简化交互逻辑。然而,对于像 Makepad 这样拥有自己完整事件系统的框架,这种默认行为会造成干扰。通过设置 cancelsTouchesInView = false,可以允许原始触摸事件继续传递给底层视图,使得 Makepad 能够接收并处理完整的用户交互序列。