What I learned about widget.el while building TextUI 0.2–0.5TextUI has moved from 0.2 to 0.5.1. The…
انتشار: 2026/08/11 10:07 UTCدریافت: 2026/08/13 05:00 UTCآخرین مشاهده: 2026/08/13 05:00 UTC
What I learned about widget.el while building TextUI 0.2–0.5TextUI has moved from 0.2 to 0.5.1. The releases happened close together because most of the work was driven by focused experiments: reproduce a real UI, find the missing capability, measure it, and only then decide whether it belongs in the framework.The main lesson has become clearer:>TextUI should improve the layout, refresh and runtime behavior around Emacs widget.el, rather than replace it with another widget system.Repository: [github.com/yibie/textui](github.com/yibie/textui)# 0.2: state, lifecycle and partial refreshThe first versions of TextUI were mostly about responsive Flex and Grid layout. By 0.2, the layout engine was already useful, but building a live interface still required too much repeated coordination code.Version 0.2 added a small buffer-level runtime:* textui-state, an ordinary buffer-local Lisp value* textui-update and textui-set-state* coalescing several state changes into one refresh* lifecycle effects for timers, processes and cleanup* async callbacks that ignore results from expired effects* named complete-line refresh regionsAt that time, TextUI also introduced textui-route-state. A render function could declare that certain top-level plist keys affected a particular region.That was fast. In the original 50-process btop fixture, one detail update improved from a median of 11.65 ms for a full refresh to 5.67 ms through state-to-region routing.But it introduced a new responsibility: the package developer had to maintain a second dependency graph.If a routed key later started affecting another region, the surrounding layout, or a lifecycle effect, forgetting to update the route could leave stale content in the buffer.The framework could not detect that mistake because the route existed specifically to skip the complete render function.# 0.3: finding where widget.el actually costs timeA thoughtful response from the author of vui pushed me to investigate a more fundamental question:>Is good refresh performance only possible after replacing widget.el controls with a separate widget implementation?Instead of guessing, I profiled TextUI with hundreds and thousands of real widget.el controls.A large part of the cost came from creating a widget to measure it, inserting the final layout, and then creating the widget again in the real buffer. Editable fields were especially expensive because of their overlays, markers and teardown behavior.Version 0.3 introduced an optional fast path:1. Produce the exact single-line presentation without creating the final widget.2. Let the layout engine place that text.3. Attach normal widget.el behavior directly to the already inserted range.The generic widget path remained available for every widget type that did not declare the optimization.In a fixed fixture containing 3,000 native controls:* Generic native-widget path: 596.55 ms* Attached fast path: 238.58 msThat is about a 2.5× improvement while retaining normal widget actions, notifications, validation, keymaps and type identity.The performance programs are retained under test/performance/, including experiments that did not improve performance. I found the negative results useful because they ruled out several attractive but incorrect explanations.Version 0.3 also fixed several layout details exposed by these tests:* editable fields and display glyphs remain inside bordered widths* Knuth–Plass uses real ideal, shrink and stretch spacing* narrow paragraphs fall back to natural wrapping instead of overflowingTextUI’s paragraph justification is based on the algorithm from emacs-kp.# 0.4: turning the optimization into a widget.el extension seamThe 0.3 fast path initially worked mainly with TextUI’s own experimental widget types.Version 0.4 made it a public, inherited widget.el protocol: :textui-measure :textui-attachA package can add these properties to its existing define-widget form: (require 'textui-widgets) (define-widget 'my-package-button