(转载)Emacs Widget 库:批评与案例研究

A deep dive into Emacs’s widget library: what it does well, where it falls apart, and a step-by-step case study of building a table widget with editable cells. Includes working code and hard-won lessons about state management, layout hacks, and cursor position preservation.

深入探索 Emacs 的小部件库:它做得好的地方,它失败的地方,以及构建具有可编辑单元格的表格小部件的逐步案例研究。包含可运行的代码和关于状态管理、布局技巧以及光标位置保留的宝贵经验。

The Emacs widget library (widget.el and wid-edit.el) has been part of Emacs since 1996, when Per Abrahamsen wrote it to power the Customize interface. Nearly three decades later, it remains the foundation for M-x customize and appears in various packages that need form-like interfaces. It’s also largely unchanged, rarely discussed, and - when you actually try to build something non-trivial with it - surprisingly painful to work with.

Emacs 小部件库( widget.elwid-edit.el )自 1996 年 Per Abrahamsen 编写它以驱动 Customize 界面以来一直是 Emacs 的一部分。近三十年后,它仍然是 M-x customize 的基础,并出现在需要表单式界面的各种包中。它也基本未变,很少被讨论,而且——当你实际尝试用它构建一些非平凡的东西时——令人惊讶地难以使用。

This post is a critique born from experience. I’ve built complex UIs using the widget library, including a table widget with editable cells that reflows dynamically. The code lives in widget-extra, a library I wrote to extend the built-in widget system. It works. It was hard. The process revealed both the library’s hidden power and its fundamental limitations.

这篇帖子源于我的经验。我使用小部件库构建过复杂的 UI,包括一个具有可编辑单元格且能动态重排的表格小部件。代码保存在 widget-extra 中,这是一个我编写以扩展内置小部件系统的库。它有效。它很困难。这个过程揭示了库的隐藏力量和其根本局限性。

What It Does Well

它做得好的地方

Before the critique, credit where it’s due. 在批评之前,应给予应有的认可。

Deep Integration with Emacs

与 Emacs 深度集成

Widgets are text. They live in buffers, use overlays and text properties, and work identically in GUI and terminal Emacs. This is philosophically aligned with Emacs’s core principle: everything is a buffer. You can use standard navigation, search the buffer, even run keyboard macros across widget forms.

小部件是文本。它们存在于缓冲区中,使用覆盖层和文本属性,在 GUI 和终端 Emacs 中工作方式完全相同。这在哲学上与 Emacs 的核心原则相一致:所有东西都是缓冲区。你可以使用标准导航,搜索缓冲区,甚至跨小部件形式运行键盘宏。

Performance

性能

A buffer with hundreds of widgets remains snappy. There are no heavy GUI objects, no separate rendering pipeline - just text with properties. The Customize interface, with its deeply nested groups and countless options, demonstrates this well.

一个包含数百个小部件的缓冲区仍然保持流畅。没有沉重的 GUI 对象,没有独立的渲染流程——只是带有属性的文本。带有深度嵌套分组和无数选项的 Customize 界面很好地展示了这一点。

Type Hierarchy

类型层次结构

The library excels at defining what widgets are. You can create new widget types that inherit from existing ones, override specific behaviours, and build a taxonomy of components. A bounded-int-field can inherit from int-field which inherits from field which inherits from default. This is genuinely powerful for building families of related widgets.

该库在定义小部件方面表现出色。您可以创建新的、继承自现有小部件的类型,覆盖特定行为,并构建组件的分类体系。一个 bounded-int-field 可以继承自 int-field ,而 int-field 又继承自 fieldfield 再继承自 default 。这对于构建一系列相关小部件来说确实非常强大。

That said, this is a classical inheritance approach, and the game development community moved away from deep inheritance hierarchies years ago. The Entity-Component-System pattern, popularised by Unity and others, favours composition over inheritance: instead of an entity being a subclass of multiple base classes, it has components that define its behaviours. An entity with physics, visuals, and AI isn’t a PhysicsVisualAIEntity subclass - it’s just an entity with three components attached.

话说回来,这是一种经典的继承方法,而游戏开发社区多年前就已经摒弃了深层继承层次结构。由 Unity 和其他框架推广的实体-组件-系统模式更倾向于组合而非继承:一个实体不再是从多个基类派生的子类,而是拥有定义其行为的组件。一个拥有物理、视觉和人工智能的实体不是某个 PhysicsVisualAIEntity 子类——它只是附加了三个组件的普通实体。

The widget library’s type hierarchy works well when your widgets fit neatly into an “is-a” relationship. It becomes awkward when you need a widget that combines multiple orthogonal behaviours - editable, validated, formatted, linked to external state. You end up either creating deep hierarchies or manually composing behaviours through property combinations. Not a fatal flaw, but worth noting that the approach shows its age.

当你的组件能够完美地契合"是一个"的关系时,组件库的类型层次结构工作得很好。当你需要一种结合多种正交行为的组件时——可编辑、可验证、格式化、与外部状态关联——你就会陷入创建深层层次结构或通过属性组合手动组合行为的困境。这不是一个致命缺陷,但值得注意的是,这种方法显示出它的时代局限性。

A Reasonable Set of Primitives

一套合理的原语

The library provides what you’d expect: links, buttons, editable fields, checkboxes, radio buttons, dropdown menus, and editable lists. For a simple configuration screen or a linear questionnaire, widgets work fine.

该库提供了你期望的功能:链接、按钮、可编辑字段、复选框、单选按钮、下拉菜单和可编辑列表。对于简单的配置界面或线性的问卷,小部件工作得很好。

Where It Falls Apart

问题所在

A confession before I start criticising: I might be wrong about some of this.

在开始批评之前,我必须承认:我可能在某些方面是错的。

The documentation didn’t work for me. The code was hard to navigate. I spent a lot of time confused. If there are better patterns I missed, I’d genuinely like to know - leave a comment or reach out. I’ll happily update this post with corrections.

文档对我来说不起作用。代码难以导航。我花了大量时间感到困惑。如果存在我遗漏的更好模式,我真心希望知道——留言或联系我。我会乐意更新这篇帖子以修正内容。

Also, I should mention: I’m primarily a server developer. I know little about building UIs. This might explain why I kept banging my head against walls that UI people would have walked around. On the other hand, it also means I approached the library without preconceptions, which occasionally has value. Make of that what you will.

此外,我应当说明:我主要是一名服务器开发者。我对构建用户界面知之甚少。这可能解释了为什么我一直在那些用户界面专家早已绕过的问题上碰壁。另一方面,这也意味着我带着没有先入为主的观念来接触这个库,这偶尔也有其价值。你如何看待就如何看吧。

Hierarchy Without Layout

无布局的层级结构

Here’s the core confusion: the widget library is excellent at defining widget types (the “what”) but offers almost nothing for widget layout (the “where”).

这里的核心问题在于:组件库在定义组件类型(即“是什么”)方面非常出色,但在组件布局(即“在哪里”)方面几乎毫无作为。

When you define a new widget type, you’re specifying its behaviour, validation, appearance, and relationship to other types. This is well-supported. But when you want to arrange widgets spatially - put these three in a row, align those labels, create a grid - you’re on your own.

当你定义一个新的部件类型时,你正在指定它的行为、验证、外观以及与其他类型的关联。这一点得到了很好的支持。但是当你想要在空间上排列部件——将这三个部件排成一行、对齐那些标签、创建一个网格——你就只能靠自己了。

The library’s composition primitives are minimal and poorly explained. You can nest widgets inside other widgets, but there’s no layout engine. You insert text and widgets sequentially into a buffer, calculating positions manually. Want columns? Count characters. Want alignment? Pad with spaces. Want reflow when content changes? Rebuild everything.

库的组合原语非常简陋且解释得不好。你可以将部件嵌套在其他部件内部,但没有布局引擎。你可以将文本和部件按顺序插入到缓冲区中,手动计算位置。想要列?数字符。想要对齐?用空格填充。想要内容变化时自动重排?重建所有内容。

This confusion between type hierarchy and spatial composition is never clearly addressed in the documentation, leaving developers to discover it painfully.

类型层次和空间组合之间的这种混淆在文档中从未得到明确说明,让开发者不得不痛苦地自己去发现。

No State Management

没有状态管理

This is the fundamental architectural gap. 这是基本架构上的差距。

Modern UI development has converged on patterns for managing state: unidirectional data flow, reactive bindings, declarative state containers. The widget library offers none of this. When widget A’s action needs to update widget B, you must:

现代 UI 开发已经集中在管理状态的模式上:单向数据流、响应式绑定、声明式状态容器。小部件库提供这些功能。当小部件 A 的操作需要更新小部件 B 时,你必须:

  1. Store references to both widgets in buffer-local variables

将两个小部件的引用存储在缓冲区局部变量中

  1. Write a :notify callback on widget A

在小部件 A 上编写一个 :notify 回调函数

  1. Manually call widget-value-set on widget B

手动调用 widget B 的 widget-value-set

  1. Call widget-setup to re-enable editing

调用 widget-setup 以重新启用编辑

  1. Hope you haven’t broken anything

希望你没有破坏任何东西

For a form with three interdependent fields, this is tedious. For a form with twenty, it’s a maintenance nightmare. There’s no concept of derived state, no way to declare “this widget’s options depend on that widget’s value,” no subscription mechanism. Everything is imperative side effects, manually threaded through callback functions.

对于一个包含三个相互依赖字段的表单,这很繁琐。对于一个包含二十个字段的表单,它是一场维护噩梦。没有派生状态的概念,没有办法声明"这个控件的选项取决于那个控件的值",也没有订阅机制。一切都是命令式的副作用,通过回调函数手动传递。

No Widget Tree

没有组件树

Most UI toolkits provide a parent-child hierarchy. This gives you automatic layout propagation, event bubbling, scoped state, and declarative nesting.

大多数 UI 工具包提供父子层级结构。这让你获得自动布局传播、事件冒泡、作用域状态和声明式嵌套。

The widget library is flat. Widgets are inserted into a buffer sequentially. Yes, composite widgets like editable-list have a :parent property for their items, but this isn’t a general-purpose tree. You cannot nest arbitrary widgets inside a container and treat them as a unit.

组件库是扁平的。组件按顺序插入到缓冲区中。是的,像 editable-list 这样的复合组件有 :parent 属性用于其子项,但这并不是一个通用树。你不能将任意组件嵌套在容器中并视它们为一个整体。

The Simplicity Paradox

简单性悖论

Here’s the irony: the widget library’s performance comes from the same architectural simplicity that makes it hard to use - not despite it.

这里有个讽刺之处:这个组件库的性能源于其架构上的简单性,这种简单性也正是它难以使用的原因——并非尽管如此。

Widgets are just text with properties. No widget tree means no tree traversal overhead. No reactive state system means no dependency tracking cost. No layout engine means no layout calculations. The buffer is the UI, rendered by Emacs’s extremely optimised text display machinery.

组件不过是带属性的文本。没有组件树意味着没有树遍历的开销。没有响应式状态系统意味着没有依赖跟踪的成本。没有布局引擎意味着没有布局计算。缓冲区就是 UI,由 Emacs 极其优化的文本显示机制渲染。

This is genuinely elegant for the Customize interface, where performance matters and the UI is fundamentally linear. The simplicity is a feature when your requirements match the design.

这对于 Customize 界面来说确实很优雅,因为性能很重要,而且 UI 本质上就是线性的。当你的需求与设计匹配时,这种简单性就是一种特性。

But that same simplicity becomes a burden when you want interdependent widgets, spatial layouts, or dynamic composition.

但当你想使用相互依赖的组件、空间布局或动态组合时,这种同样的简单性就会变成一种负担。

Case Study: Building Layout Widgets

案例研究:构建布局组件

Enough critique. Let’s build something and see what we learn.

足够的批评了。让我们来构建一些东西,看看我们能学到什么。

(This is the part where a proper UI developer would probably reach for an existing solution. As a server developer with more arrogance than sense, I naturally decided to implement everything from scratch.)

(在这个阶段,一个专业的 UI 开发者可能会选择一个现有的解决方案。作为一个比聪明更有傲慢的服务器开发者,我自然决定从头实现所有功能。)

All the code shown here is available in widget-extra, a library I wrote to extend the built-in widget system with additional components: labels, fields, buttons, and layout widgets. You can use it directly or study it as a reference.

这里展示的所有代码都可以在 widget-extra 中找到,这是一个我编写的库,用于扩展内置的 widget 系统,添加额外的组件:标签、字段、按钮和布局组件。你可以直接使用它,或者将其作为参考来研究。

Warm-up: A Fields Group with Aligned Tags

热身:一个带有对齐标签的字段组

Before tackling tables, let’s solve a simpler problem: displaying multiple fields with their tags aligned.

在处理表格之前,让我们先解决一个更简单的问题:显示多个字段并使其标签对齐。

Name:   Boris
Age:    30
Email:  [email protected]

The challenge: each field has a tag of different length, but we want the values to line up. The widget library provides no alignment primitives, so we calculate padding manually.

挑战:每个字段的标签长度不同,但我们希望值能对齐。组件库不提供对齐原语,因此我们需要手动计算填充。

(define-widget 'fields-group 'default
  "Group multiple fields with automatic tag alignment."
  :convert-widget #'widget-types-convert-widget
  :copy #'widget-types-copy
  :format "%v"
  :extra-offset 1
  :value-create #'widget-fields-group-value-create)

The :value-create function measures all tags, finds the maximum length, then adds appropriate padding to each field:

:value-create 函数测量所有标签,找到最大长度,然后为每个字段添加适当的填充:

(defun widget-fields-group-value-create (widget)
  "Create children with aligned tags."
  (let* ((args (widget-get widget :args))
         (max-tag-length (seq-max
                          (seq-map
                           (lambda (x) (length (or (widget-get x :tag) "")))
                           args))))
    (dolist (arg args)
      (widget-fields-group-add-item widget arg max-tag-length))))

(defun widget-fields-group-add-item (widget item max-tag-length)
  "Add ITEM to WIDGET with padding based on MAX-TAG-LENGTH."
  (let* ((tag (widget-get item :tag))
         (tag-length (if tag (length tag) 0))
         (offset (+ (widget-get widget :extra-offset)
                    (- max-tag-length tag-length)))
         (format (or (widget-get item :format) "%T%[%v%]"))
         (format (if (s-ends-with-p "\n" format)
                     format
                   (concat format "\n"))))
    (widget-put item :format format)
    (widget-put item :offset offset)
    (widget-create-child widget item)))

Usage:

用法:

(widget-create
 'fields-group
 (list 'field :tag "Name:" :value "Boris")
 (list 'int-field :tag "Age:" :value 30)
 (list 'field :tag "Email:" :value "[email protected]"))

This is the pattern: measure first, then render with calculated offsets. No layout engine - just arithmetic and string padding.

这是模式:先测量,然后用计算出的偏移量渲染。没有布局引擎——只是算术和字符串填充。

Note also that we’re modifying each child’s :offset property before creation. The base field widget (also defined in widget-extra) supports a custom %T format escape that renders the tag with configurable spacing. This kind of cooperation between parent and child widgets requires planning the property protocol in advance.

还要注意的是,我们在创建之前会修改每个子元素的 :offset 属性。基础 field 小部件(也在 widget-extra 中定义)支持自定义 %T 格式转义符,该转义符使用可配置的间距渲染标签。这种父级和子级小部件之间的协作需要在事先规划属性协议。

The Hard Part: A Table with Editable Cells

难点:带可编辑单元格的表格

Now let’s tackle something genuinely difficult.

现在让我们来处理一个真正困难的问题。

A table with editable cells sounds simple: rows and columns, maybe some separators, widgets in each cell. But the requirements quickly compound:

一个带有可编辑单元格的表格听起来很简单:行和列,可能有些分隔符,每个单元格中有小部件。但要求很快就会叠加:

  1. Columns must align - cells in the same column should have equal width

列必须对齐 - 同一列中的单元格应有相等的宽度

  1. When a cell’s value changes length, the column must resize

当单元格的值改变长度时,列必须调整大小

  1. When the table redraws, the cursor must stay in the same logical position

当表格重绘时,光标必须保持在相同的逻辑位置

  1. The whole table should be a single widget that can be created and manipulated atomically

整个表格应该是一个单一的部件,可以原子性地创建和操作

None of this is provided. All of it is possible.

这些都没有提供。所有这些都是可能的。

Step 1: Define the Structure

第一步:定义结构

A table takes rows as arguments. Each row is either a horizontal line or a data row containing widgets:

一个表格以行为参数。每一行要么是水平线,要么是包含小部件的数据行:

(define-widget 'table 'default
  "A table widget with rows, columns, and separators."
  :convert-widget #'widget-types-convert-widget
  :copy #'widget-types-copy
  :format "%v"
  :row-conj " | "
  :hline-conj "-+-"
  :hline-content ?-
  :padding ?\s
  :value-create #'widget-table-value-create
  :notify #'widget-table-notify)

Usage will look like:

使用方式如下:

(widget-create
 'table
 '(row (label :value "Name") (label :value "Age"))
 '(hline)
 '(row (field :value "Boris") (int-field :value 30)))

The :value-create function does the heavy lifting. The :notify function handles state changes.

:value-create 函数负责繁重的任务。 :notify 函数处理状态变化。

Step 2: Calculate Column Widths

第二步:计算列宽

Here’s the first hack: to know how wide each column should be, we need to know how wide each cell’s content is. But widgets don’t have a “width” property - they’re just text that gets inserted.

这是第一个小技巧:要知道每列应该有多宽,我们需要知道每个单元格的内容有多宽。但小部件没有"宽度"属性——它们只是被插入的文本。

Solution: create each widget in a temporary buffer, measure the resulting text, then discard it:

解决方案:在临时缓冲区中创建每个部件,测量生成的文本,然后丢弃它:

(let* ((args (widget-get widget :args))
       (cols (apply #'max (mapcar (lambda (row)
                                    (length (widget-get row :args)))
                                  args)))
       (widths
        (->> (-iota cols)
             ;; Transpose: group by column instead of row
             (-map (lambda (i)
                     (-map (-partial #'nth i)
                           (--map (widget-get it :args) args))))
             ;; Measure each cell
             (--map-indexed
              (--map (when it
                       (with-temp-buffer
                         (widget-create it)
                         (- (point) 1)))
                     it)))))
  ;; widths is now a list of lists: ((col0-row0 col0-row1 ...) (col1-row0 ...))
  ...)

This is expensive - we create every widget twice. But it works, and for reasonable table sizes, it’s fast enough.

这很昂贵——我们为每个部件创建了两次。但它有效,对于合理的表格大小,它足够快。

Step 3: Render with Padding

步骤 3:带填充渲染

Now we iterate through rows, rendering each cell with appropriate padding:

现在我们遍历行,为每个单元格渲染适当的填充:

(defun widget-table-value-create (widget)
  (let* ((args (widget-get widget :args))
         (widths (widget-table--calculate-widths widget))
         (max-widths (-map #'-max widths))
         (children))
    (-each-indexed args
      (lambda (row-index row)
        (pcase (car row)
          (`row
           (widget-insert (widget-get widget :row-start))
           (-each-indexed (widget-get row :args)
             (lambda (col-index col)
               (unless (= 0 col-index)
                 (widget-insert (widget-get widget :row-conj)))
               (let* ((w (nth row-index (nth col-index widths)))
                      (mw (nth col-index max-widths))
                      (pad (- mw w))
                      (child (widget-create-child widget col)))
                 ;; Track position for state management
                 (widget-put child :row-index row-index)
                 (widget-put child :col-index col-index)
                 (push child children)
                 ;; Add padding to align columns
                 (when (> pad 0)
                   (widget-insert (make-string pad ?\s))))))
           (widget-insert "\n"))

          (`hline
           ;; Draw separator line
           (--each (-iota (length max-widths))
             (unless (= it 0)
               (widget-insert (widget-get widget :hline-conj)))
             (widget-insert
              (make-string (nth it max-widths) ?-)))
           (widget-insert "\n")))))

    (widget-put widget :children (reverse children))))

The key insight: we store :row-index and :col-index on each child widget. This lets us find them again after a redraw.

关键洞察:我们在每个子部件上存储 :row-index:col-index 。这使我们能够在重绘后再次找到它们。

Step 4: Handle State Changes (The Hard Part)

步骤 4:处理状态变化(难点)

When a cell’s value changes, we need to: 当单元格的值发生变化时,我们需要:

  1. Update our internal representation (:args)

更新内部表示( :args

  1. Redraw the entire table (column widths may have changed)

重绘整个表格(列宽可能已改变)

  1. Put the cursor back where it was

将光标移回原位

The :notify callback receives the child widget that changed:

:notify 回调函数接收发生变化的子部件:

(defun widget-table-notify (widget child &optional _event)
  (let* ((row-index (widget-get child :row-index))
         (col-index (widget-get child :col-index))
         ;; Remember cursor position relative to widget start
         (child-from (marker-position (widget-get child :from)))
         (delta (when child-from (- (point) child-from)))
         (new-value (widget-value child)))

    ;; Update the spec in :args with new value
    (let* ((row (nth row-index (widget-get widget :args)))
           (original-spec (nth col-index (widget-get row :args)))
           (updated-spec (widget-table--update-spec-value
                          original-spec new-value)))
      (widget-put widget :args
                  (--update-at row-index
                               (progn
                                 (widget-put it :args
                                             (-replace-at col-index
                                                          updated-spec
                                                          (widget-get it :args)))
                                 it)
                               (widget-get widget :args))))

    ;; Redraw the entire table
    (widget-default-value-set widget (widget-get widget :value))

    ;; Restore cursor position
    (when-let ((child (--find (and (= row-index (widget-get it :row-index))
                                   (= col-index (widget-get it :col-index)))
                              (widget-get widget :children))))
      (when delta
        (goto-char (+ (widget-get child :from) delta))))))

This is the critical piece. We:

这是关键之作。我们:

  1. Capture the cursor’s offset from the widget’s start before redrawing

在重新绘制前捕获光标相对于组件起始位置的偏移量

  1. Modify :args to reflect the new value

:args 修改为反映新值

  1. Trigger a full redraw via widget-default-value-set

通过 widget-default-value-set 触发完整重绘

  1. Find the same cell again by row/column indices

通过行列索引再次找到相同单元格

  1. Restore the cursor to the same offset

将光标恢复到相同的偏移量

Without step 5, editing would be maddening - every keystroke would jump the cursor somewhere unexpected.

如果没有第 5 步,编辑将会令人抓狂——每个按键都会让光标跳到意想不到的位置。

Step 5: Update Specs Without Corruption

第 5 步:在不破坏的情况下更新规格

One subtle bug: widget specs in :args are often shared structures. If you modify them directly, you corrupt the original definitions. Deep copy is essential:

一个微妙的问题: :args 中的 widget 规格通常是共享结构。如果你直接修改它们,会破坏原始定义。必须进行深拷贝:

(defun widget-table--update-spec-value (spec new-value)
  "Return a copy of widget SPEC with :value set to NEW-VALUE."
  (let ((copy (copy-tree spec)))
    (if (plist-member (cdr copy) :value)
        (plist-put (cdr copy) :value new-value)
      (setcdr copy (cons :value (cons new-value (cdr copy)))))
    ;; Special case: menu-choice needs :tag updated too
    (when (eq (car copy) 'menu-choice)
      (plist-put (cdr copy) :tag new-value))
    copy))

The Result 结果

After all this, we have a table that:

经过所有这些,我们得到了一个表格,它:

  • Aligns columns automatically

自动对齐列

  • Reflows when cell content changes

当单元格内容变化时重新布局

  • Preserves cursor position through redraws

在重绘过程中保持光标位置

  • Works with various widget types as cells

作为不同类型控件的单元格使用

(widget-create
 'table
 '(row (label :value "Name") (label :value "Score"))
 '(hline)
 '(row (field :value "Alice") (int-field :value 95))
 '(row (field :value "Bob") (int-field :value 87)))

Renders as: 渲染为:

Name  | Score
------+------
Alice |    95
Bob   |    87

Edit “Alice” to “Alexandria” and watch the first column widen. The cursor stays in the cell you were editing.

将"Alice"编辑为"Alexandria",观察第一列变宽。光标将停留在您正在编辑的单元格中。

What This Teaches Us 这教会了我们什么

Building this table required:

构建这个表格需要:

  • Measuring widgets by creating them in temporary buffers - there’s no introspection API for “how wide would this be?”

通过在临时缓冲区中创建部件来测量部件尺寸——没有用于"这个部件有多宽"的内置自省 API

  • Manual coordinate tracking - storing row/column indices because there’s no widget tree to traverse

手动坐标跟踪——存储行/列索引,因为没有部件树可供遍历

  • Full redraw on any change - no incremental updates, no dirty-region tracking

任何更改时进行全重绘——没有增量更新,没有脏区域跟踪

  • Cursor position surgery - capturing and restoring offsets because the library doesn’t preserve context through redraws

光标位置手术——捕获和恢复偏移量,因为库不会通过重绘保留上下文

  • Deep copying specs - because shared structures will bite you

深度复制规格——因为共享结构会给你带来麻烦

None of this is documented. All of it is discoverable only by building something and hitting walls.

所有这些都没有文档记录。所有这些只有在构建某物并遇到障碍时才能发现。

And yet - it works. The underlying primitives (text properties, overlays, markers) are solid. The performance is good. You can build sophisticated UIs if you’re willing to pay the complexity tax.

然而——它确实能工作。底层的原语(文本属性、覆盖层、标记)非常稳固。性能良好。如果你愿意付出复杂性的代价,可以构建出复杂的 UI。

The Modern Landscape

现代格局

For keyboard-driven command menus, Transient (from Magit) has become the standard. It’s well-documented, actively maintained, and designed around a coherent model of transient state.

对于基于键盘的命令菜单,Transient(来自 Magit)已经成为标准。它有完善的文档、活跃的维护,并且围绕一个连贯的瞬态状态模型进行设计。

For complex interactive UIs, there isn’t a clear answer. The widget library occupies an awkward middle ground: too complex for simple needs, too limited for complex ones.

对于复杂的交互式 UI,目前还没有明确的答案。Widget 库处于一个尴尬的中间地带:对于简单需求来说太复杂,对于复杂需求来说又太有限。

What’s Next

接下来是什么

While building Emacs tools for Barberry Garden - my wine tasting management system - I’ve been pushing the widget library to its limits. The brb package includes event planning interfaces, tasting score entry forms, and various administrative views. Tables with editable cells. Dynamic forms that reconfigure based on selections. Nested groups that expand and collapse.

在为 Barberry Garden(我的葡萄酒品鉴管理系统)构建 Emacs 工具时,我已将小部件库推向极限。brb 包包含活动规划界面、品鉴评分录入表单以及各种管理视图。可编辑单元格的表格。根据选择动态重新配置的表单。可展开和折叠的嵌套组。

It works, but the friction is constant. Every feature requires fighting the architecture. The cognitive overhead of manual state management, cursor preservation, and layout calculation adds up.

它确实能工作,但摩擦始终存在。每个功能都需要与架构抗争。手动状态管理、光标保留和布局计算的认知负担不断累积。

So I’ve started designing something new: a UI layer that uses widget.el under the hood but provides higher-level abstractions. Not a full reactive framework - Emacs doesn’t need that complexity - but a thin system that handles:

因此我开始设计新的事物:一个使用 widget.el 作为底层技术但提供更高级抽象的 UI 层。不是完整的响应式框架——Emacs 不需要这种复杂性——而是一个处理以下内容的薄系统:

  • Declarative composition: describe what you want, not how to build it

声明式组合:描述你想要什么,而不是如何构建它

  • Automatic state propagation: when this changes, update that

自动状态传播:当这个变化时,更新那个

  • Cursor-aware redraws: preserve editing context through updates

光标感知的重绘:通过更新保留编辑上下文

  • Layout primitives: rows, columns, groups that just work

布局原语:只需工作的行、列、组

The goal isn’t to replace widget.el but to tame it. Keep the performance, hide the ceremony. Respect the fundamental constraints of Emacs UI - the criticality of cursor position, the two-dimensional nature of the buffer. We don’t have a proper DOM and CSS, and that’s actually fine.

目标不是取代 widget.el ,而是驯服它。保留性能,隐藏仪式。尊重 Emacs UI 的基本约束——光标位置的重要性,缓冲区的二维特性。我们没有合适的 DOM 和 CSS,这其实很好。

No promises on timeline, but I may share the design document soon. One more React-inspired UI library for Emacs? Perhaps. But sometimes you need to build the tools that let you build what you actually want.

没有时间表上的承诺,但可能很快会分享设计文档。又一个受 React 启发的 Emacs UI 库?也许吧。但有时你需要构建那些能让你构建真正想要的工具。

In the meantime, widget-extra is available and working. The widgets described in this post - labels, fields, buttons, fields-group, table, and more - are all there. Use it if it helps. Study it if you’re curious. And if I do build the new UI system, widget-extra will likely be superseded - but until then, it’s a reasonable way to build widget-based interfaces without starting from scratch.

与此同时,widget-extra 已经可用且在运行。这篇帖子中描述的部件——标签、字段、按钮、 fields-grouptable 等——都包含在内。如果它有帮助,就使用它。如果你感兴趣,就研究它。如果我确实构建了新的 UI 系统, widget-extra 可能会被取代——但在此之前,它是构建基于部件的界面的一个合理方式,而无需从零开始。

Conclusion 结论

The Emacs widget library is more powerful than its documentation suggests and more painful than it should be. The type hierarchy is genuinely elegant. The layout story is essentially absent. State management is your problem.

Emacs 小部件库比其文档所暗示的更强大,也比它应有的更令人痛苦。类型层次结构确实非常优雅。布局方面基本上缺失。状态管理是你的问题。

Its performance comes from simplicity - the same simplicity that makes complex UIs difficult. There’s no free lunch.

它的性能来源于简单性——这种简单性也让复杂的用户界面变得困难。没有免费的午餐。

If you’re building something simple, widgets work fine. If you’re building something complex, budget time for archaeology. Read the source. Build small experiments. Accept that cursor position preservation will haunt your dreams.

如果你在构建简单的东西,小部件工作得很好。如果你在构建复杂的东西,预算时间进行考古挖掘。阅读源代码。构建小实验。接受光标位置保存将困扰你的梦。

I hope this post serves as a useful introduction to what you’re getting into with widget.el - and perhaps widget-extra can save you some of the pain I went through. The library isn’t comprehensive documentation of the widget system (that would require a book), but between this walkthrough and the source code, you should have enough to get started.

我希望这篇文章能为你介绍 widget.el 中你所涉及的内容——或许 widget-extra 能帮你避免我经历的一些痛苦。这个库并不是对 widget 系统的全面文档(那需要一本书),但通过这篇指南和源代码,你应该有足够的内容开始使用了。

And when you hit walls - because you will - know that you’re not alone. You’re joining a long tradition of Emacs hackers who got their widgets working and then immediately wanted to forget everything about wid-edit.el.

当你遇到障碍——因为一定会遇到——要知道你并不孤单。你正加入一个悠久的 Emacs 黑客传统,他们让他们的部件工作起来,然后立刻就想忘记关于 wid-edit.el 的一切。

原文: The Emacs Widget Library: A Critique and Case Study - Boris Buliga

3 个赞