Hosts
A Host is the View node that owns an Ebox node: the unit that Ebox lays out, styles, paints and hit-tests. Every visual part of a user interface is a Host, and everything else in a View exists to compose, repeat or replace Hosts. This chapter defines the eight Host kinds, the properties they accept, how properties from several sources are combined, how Host metadata becomes selector subjects, and the functions that resolve Host references and read committed geometry.
The View grammar, the evaluation of property expressions and the identity of Host nodes are defined in Views. Components, :use Behaviors and caller attributes belong to the Component and Behavior chapters; how style properties are declared and cascaded belongs to the style chapters. This chapter is the reference for the Host layer itself.
The only View node with an Ebox node
Section titled “The only View node with an Ebox node”A View is a tree of nodes. When a View is mounted or rendered, ETAF lowers it to a retained tree of segments; a Host segment is the only one that owns an Ebox node.
- A Host node is written with a Host name —
text,box,fragment,row,column,flex,gridorimage— its properties and its children. - A Host segment owns exactly one Ebox node, or none for
fragment, whose children join the enclosing layout directly. - When a View produces several roots, the renderer adds one column wrapper of its own; that wrapper is not a Host and carries no application properties.
- Only Hosts carry style properties, host references and accessibility metadata, and only Hosts can be measured by the geometry queries.
A Host is also the unit of identity in a View: reconciliation matches Host children by their name and :key, as Views describes, and a matched Host keeps its Ebox node and its retained state.
The eight Host kinds
Section titled “The eight Host kinds”| Host | Content | Default role in layout |
|---|---|---|
text |
exactly one text payload | text laid out by its parent |
box |
children | block-level flow container |
fragment |
children | no node; children join the parent |
row |
children | one horizontal line, no wrapping |
column |
children | vertical stack |
flex |
children | flex container |
grid |
children | grid with automatic tracks |
image |
none | fixed :columns × :rows character cell |
Every Host accepts semantic properties, event properties and pointer affordances; a fragment has no node to carry them. Style properties are accepted by kind:
| Host kinds | Accepted style properties |
|---|---|
text |
the typography and paint set: :color, :background-color, :font-family, :font-size, :font-weight, :font-style, :text-decoration-line, :text-decoration-color, :text-decoration-style |
box |
that set, plus :text-align, :wrap-mode, and the box model and geometry set |
row, column |
the Box set, plus :cross-align and :item-gap |
flex |
the Box set, plus :flex-direction, :flex-wrap, :flex-flow, :justify-content, :align-items, :align-content, :row-gap, :column-gap and :gap |
grid |
the Box set, plus :grid-template-columns, :grid-template-rows, :grid-auto-columns, :grid-auto-rows, :grid-auto-flow, :justify-items, :justify-content, :align-items, :align-content, :row-gap, :column-gap and :gap |
image |
none; its source and size come from :src, :columns, :rows, :adapter and :inset |
fragment |
none; it has no node |
A child of a flex or grid Host may also declare the item properties (:order, :flex-grow, :flex-shrink, :flex-basis, :flex, :align-self, :justify-self, :grid-column, :grid-row, :grid-column-span, :grid-row-span), which affect how its parent places it. Style properties start at the schema defaults — no padding, margin or border, inherited color, normal font weight and style, and the layout defaults named with each kind — so a Host with no style properties is already laid out. Style Properties lists every property with its value grammar, initial value and inheritance.
Inline figure: The eight Host kinds; fragment is the one without an Ebox node
(etaf-view (column :width '(ch 44) :item-gap '(lh 0) (grid :width '(ch 44) :grid-template-columns '((fr 1) (fr 1) (fr 1) (fr 1)) :gap '((lh 0) (ch 1)) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "text")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "box")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "row")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "column")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "flex")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "grid")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-panel-bg "#f6f8fa") (text "image")) (box :padding '((lh 0) (ch 1)) :background-color (etaf-theme-token :ui-disabled-bg "#e5e7eb") (text :color (etaf-theme-token :ui-disabled-fg "#687386") "fragment"))) (text :color (etaf-theme-token :ui-muted-fg "#526174") "fragment 不产生节点;其余七种各绑定一个 Ebox 节点。")))Host kinds
Section titled “Host kinds”Host: text
Section titled “Host: text”A Host showing one run of text; the leaf of most interfaces.
Semantics.
- The content is exactly one payload: a string, or an
exprwhose result resolves to text. A second child is a render error, and an empty string is valid. - The renderer builds it as one Ebox text node. Ebox lays it out inside its parent: a
textHost inside aboxparticipates in a flow paragraph, and one inside arowbecomes a one-line item. - It accepts the typography and paint properties. Layout properties such as
:padding,:width,:wrap-modeand:text-alignbelong to the container that lays the text out; the text Host inherits typography, color and:text-alignfrom its ancestors. - Its content is not interactive by itself: a click on it is handled by the nearest activating ancestor unless the
textHost declares an interaction property.
Errors. error→ etaf-renderer-error when the payload is not text, for example a number in a structural position or a payload that resolves to a non-string.
Example. (text :font-weight 'bold "Unread (3)")
See also. the box, row and image Hosts.
Host: box
Section titled “Host: box”A block-level flow container; the general-purpose layout Host.
Semantics.
- Children are laid out in normal flow. Consecutive inline children (typically
textHosts) become one anonymous paragraph, so a sentence written as several text nodes wraps as a whole; block children start a new line. - It accepts the full box model and geometry set: padding, margin, border, width and height with their min/max forms,
:box-sizing,:position,:left/:top,:z-index,:layer,:anchor,:placement,:visibility,:outer,:overflow,:scroll-anchor,:border-radius,:wrap-mode,:text-align, and the item properties when the box is a flex or grid item. :overflow scrollor:autowith a bounded height makes the box a scroller that Scrolling describes.
Example. (box :background-color "gray15" (text "Ready")) is already a usable panel; (box :padding '(px 4) :border-radius '(px 1) ...) adds breathing room and a rounded corner.
See also. the column and row Hosts for the common stacking cases, and Boxes and Normal Flow for the layout rules.
fragment
Section titled “fragment”Host: fragment
Section titled “Host: fragment”A grouping Host with no Ebox node.
Semantics.
- The renderer inserts its children into the enclosing layout in order, so a fragment adds no visual box, no spacing and no style target. It is how a Component returns several roots, as Views describes.
- A fragment accepts
:keyand the compiler directives (:if,:else-if,:else,:for). Other Host properties have no node to act on and are disregarded.
Example. (fragment (text "left") (text "right")) places two text nodes next to each other in the parent.
See also. Fragments.
Host: row
Section titled “Host: row”A container that lays its children out in one horizontal line.
Semantics.
- Children are placed left to right and do not wrap; a child wider than the remaining space is shrunk to it, and the row's height is the tallest child.
:cross-align(initialstretch) aligns children on the vertical axis;:item-gap(initial zero) sets the horizontal spacing between them.- It accepts the Box set plus
:cross-alignand:item-gap. Use aflexHost when the content must wrap or reverse; userowfor toolbars, button strips, breadcrumbs and single-line status bars.
Example. (row :item-gap '(ch 1) (text "Name:") (text (expr name)))
See also. column, which stacks children vertically with the same cross-axis and gap properties.
column
Section titled “column”Host: column
Section titled “Host: column”A container that stacks its children vertically.
Semantics.
- Children are laid out top to bottom in order;
:cross-alignaligns them on the horizontal axis and:item-gapsets the vertical spacing. - It accepts the Box set plus
:cross-alignand:item-gap. It is the usual root of a page, a form or a list of rows.
Example. (column :item-gap '(lh 1) ...heading... ...rows...)
See also. row.
Host: flex
Section titled “Host: flex”A full flex container.
Semantics.
- Direction, wrapping, main-axis distribution and cross-axis alignment come from the flex container set:
:flex-direction(initialrow),:flex-wrap(initialnowrap),:flex-flow,:justify-content(initialflex-start),:align-itemsand:align-content(initialstretch), plus:row-gap,:column-gapand the:gapshorthand. - Unlike
row, a flex container wraps, reverses, and honors the item properties of its children, so a toolbar that overflows can wrap to the next line. - It accepts the Box set plus the flex container set.
Example. (flex :flex-wrap 'wrap :gap '((lh 0) (px 2)) ...cards...) lays cards out in a wrapping strip.
See also. Flex Layout.
Host: grid
Section titled “Host: grid”A grid container with explicit or automatic tracks.
Semantics.
:grid-template-columnsand:grid-template-rowsdefine tracks; left at their initial nil the grid creates automatic tracks.:grid-auto-columns,:grid-auto-rowsand:grid-auto-flowcontrol tracks and placement outside the explicit template;:justify-items(initialstretch),:justify-content,:align-itemsand:align-contentalign items and the track group;:row-gap,:column-gapand:gapset spacing.- Children place themselves with the item properties
:grid-column,:grid-row,:grid-column-span,:grid-row-span,:justify-selfand:align-self. - It accepts the Box set plus the grid container set.
Example. (grid :grid-template-columns '((fr 1) (fr 2)) ...cells...) gives a two-column form whose second column is twice as wide.
See also. Grid Layout.
Host: image
Section titled “Host: image”A fixed-size local picture.
Semantics.
:srcis a local file path or an Emacs image descriptor(image ... :type TYPE :file FILE)or:data DATA.:columnsand:rowsare the positive integer character-cell size of the picture.:adapterselects how the source becomes cells:auto(initial) tries a graphics image on the current frame, then the registered converters, then a monochrome ASCII placeholder;textskips graphics;placeholderalways makes the ASCII form. A missing or undecodable source becomes a placeholder rather than an error.:insetis nil or(TOP . BOTTOM), the pixels of vertical room a graphics image leaves empty inside its rows.- An image Host takes no children; it is a leaf, and style properties such as
:paddingor:colorare disregarded because the picture fills its cell.
Errors. error→ etaf-renderer-error when :src is not a path or a descriptor with :type and :file/:data, when :columns or :rows is not a positive integer, when :adapter is not auto, text or placeholder, when :inset is not nil or a non-negative pair, or when the Host is given children.
Example. (image :src "/tmp/chart.png" :columns 40 :rows 10 :adapter 'placeholder)
See also. Images.
Semantic properties
Section titled “Semantic properties”Semantic properties are ETAF's own Host metadata: identity, accessibility, references, behaviors, pointer feedback, keyboard operation and layout persistence. They do not change a node's visual style; they reach the runtime, the Ebox node, or both, and this chapter defines them all.
A semantic property is validated when its Host commits. A successful commit keeps the previous state; a failing one publishes nothing for that Host, so a Host never carries a half-applied property set. Event properties (:on-*) are the third domain; Events defines them, and Actions, Behaviors and Error Boundaries defines how they compose. :on-direction is a keyboard property and is defined in the Keyboard chapter.
Values and conflicts
Section titled “Values and conflicts”A Host can receive properties from three places. Inline properties are written on the Host itself. Fallthrough attributes are the attributes a caller passes to a Component call whose root is a Host; they are merged into that root Host. :use attaches Behaviors, whose attributes are merged into the Host as well. The theme and component styles add Ebox-level inputs, not Host properties: a theme's Host defaults and a Component's style rules are applied by the Ebox cascade underneath the Host's own declarations.
The merge keeps each domain coherent:
| Property | When several sources provide it |
|---|---|
| style property | a non-nil value from the outer source replaces the inner one; nil leaves the inner value in place; a Behavior's style value fills only when the Host has none |
:class |
the values merge into one stable union, inner tokens first, duplicates removed |
:id |
the outer value replaces the inner one |
| event property | the callbacks compose, inner first, and the inner callback's return value is preserved |
:disabled |
the values combine with or |
:use |
the Behavior source lists concatenate |
:keys |
the binding lists merge; the Host's own entries take precedence |
| other semantic properties | the outer value replaces the inner one; a Behavior fills only when the Host has none |
:role and most :aria-* |
owned semantics: two different values are a conflict, not an override |
A conflict between owned semantics is an error rather than a silent override, because changing a control's role or accessible state behind the caller's back would break both the accessibility contract and the selectors that matched it. :aria-label and :aria-description are the exceptions: they describe rather than identify, so a caller may replace them.
Errors. error→ etaf-component-call-error when a fallthrough or Behavior value conflicts with an owned semantic property of the root Host; the message names the Component or Behavior, the Host kind and the property.
See also. the Component chapter for fallthrough attributes, and Actions, Behaviors and Error Boundaries for :use.
Roles and ARIA
Section titled “Roles and ARIA”A Host's accessible metadata is its role attribute and its aria-* attributes on the Ebox node. They are ordinary selector subjects, so stylesheets and tests can select a Host by role or accessible state, and they are the facts the keyboard and focus layers read.
:rolewrites the node'sroleattribute. Roles such asbutton,option,checkboxandslidercarry conventional behavior in the Focus and Keyboard chapters.- Every
:aria-*property writes one attribute with the name after the colon. Explicitly passing nil still writes the attribute with a nil value, which is different from omitting it: a nil attribute is present, so a selector that tests the attribute matches, and Ebox's inherited-value blocking sees the difference. - The values are strings, numbers or symbols; numbers and symbols are converted to their printed form. A list or other value is rejected with
error→ etaf-renderer-error. :roleand the identifyingaria-*attributes are owned semantic properties as described above;:aria-labeland:aria-descriptionare descriptive and may be replaced by a caller.
Example. (box :role "progressbar" :aria-valuenow 62 :aria-valuemin 0 :aria-valuemax 100 ...)
See also. the Focus and Keyboard chapters for role-specific behavior.
Identity and metadata
Section titled “Identity and metadata”Property: :class
Section titled “Property: :class”One or more class names that selectors and component styles match.
Arguments. A string, a symbol, or a proper list of strings and symbols. A string is split on whitespace and a list is flattened, so "task done" and '(task done) both give the classes task and done. The default is nil, no classes. Any other value is rejected.
Semantics. Classes are written to the Ebox node and become selector subjects: a rule such as .task matches the Host, and Component Styles and Themes explains how a Component's own rules use them. When a caller attribute or a Behavior contributes :class, the two token sets merge into a stable union with the inner Host's tokens first and duplicates removed, so a caller can add a class without dropping the Host's own.
Example. (text :class "status error" "Deployment failed")
Errors. error→ ebox-error when the value is not a string, symbol or list of them and the node is built; error→ etaf-component-call-error when a merged value is invalid.
See also. Selector subjects and Component Styles and Themes.
Property: :id
Section titled “Property: :id”The identifier of a Host, matched by #name selectors.
Arguments. A string, symbol or number used as a token value; nil means no id.
Semantics. The value is written to the Ebox node as its id and becomes a selector subject. A caller attribute replaces the root Host's id; it does not merge.
Example. (box :id "results" ...) with a rule #results { ... }.
See also. Selector subjects.
Accessibility and focus
Section titled “Accessibility and focus”Property: :role
Section titled “Property: :role”The accessible role of a Host.
Arguments. A symbol or a string, or nil. Values are not restricted to the ARIA vocabulary; ETAF only refuses values that are neither a symbol nor a string.
Semantics. A non-nil role is written to the node as its role attribute and becomes a selector subject. The Focus and Keyboard chapters define how roles such as button, slider, separator and option participate in focus order, arrows and activation. :role is owned semantics: once the Host declares it, a caller or Behavior cannot replace it with a different value.
Example. (text :role 'button :tab-index 0 :on-press #'retry "Retry")
Errors. error→ etaf-renderer-error when the value is neither a symbol, a string nor nil.
See also. :aria-*, Roles and ARIA.
Property: :aria-*
Section titled “Property: :aria-*”An attribute in the ARIA namespace; :aria-* stands for every property whose name begins with :aria-.
Arguments. nil, a string, a number or a symbol. nil is a supplied value: it writes a present nil attribute rather than omitting the attribute.
Semantics. The property writes one attribute whose name is the property name after the colon, so :aria-label writes aria-label and :aria-selected writes aria-selected. The attribute is a selector subject, and Ebox reads some attributes as state (aria-selected, aria-checked, aria-current) when a focus group chooses its entry. Explicit nil being present matters twice: a selector written for the attribute matches, and an inner nil blocks an ancestor's value in the Ebox inheritance rules.
Example. (box :role "listbox" :aria-label "Open tabs" ...)
Errors. error→ etaf-renderer-error when the value is not nil, a string, a number or a symbol.
See also. :role, Roles and ARIA.
Property: :tab-index
Section titled “Property: :tab-index”The position of a Host in the Tab order.
Arguments. nil or an integer. The default is nil, which makes the Host no focus stop.
Semantics. A non-negative integer makes the Host a focus stop: focus navigation can enter it with Tab and leave it with Tab. Stops are ordered by the integer, lower first, and stops with equal indices keep tree order, so a region's controls are visited together. A Host without a tab index can still be activated by pointer and can still contain focus stops of its own. The Focus chapter defines the rest of the navigation model.
Example. (row :tab-index 0 :on-press (lambda () (open item)) (text (expr item)))
Errors. error→ etaf-renderer-error when the value is not an integer or nil.
See also. the Focus chapter.
Property: :disabled
Section titled “Property: :disabled”Mark a Host as unavailable without removing it.
Arguments. nil or t. Any other value is rejected. The default is nil.
Semantics. A disabled Host does not run its event callbacks or :keys; its Behaviors are not installed; Ebox gives it an empty keymap, so a disabled control blocks an inherited keymap instead of falling through to it; and its node derives the disabled selector state, which stylesheets can match with :disabled. The Host stays visible and keeps its place in the layout.
Example. (text :role 'button :tab-index 0 :disabled (not (etaf-value valid)) :on-press #'save "Save")
Errors. error→ etaf-renderer-error when the value is not nil or t.
See also. Events for activation and Focus for focus.
Property: :focus-group
Section titled “Property: :focus-group”Make a container's focus stops one stop of the enclosing navigation.
Arguments. nil, vertical, horizontal or grid. The default is nil.
Semantics. A container with a focus group contributes only its entry to Tab order: Tab enters the group at the remembered member, or at the member whose aria-selected, aria-current or aria-checked is true, or at the first member. Inside the group, arrows move between members in the declared direction, and leaving an edge moves to the nearest stop outside. Nested groups belong to their nearest ancestor group.
Example. (column :focus-group 'vertical ...rows...)
Errors. error→ etaf-renderer-error when the value is not nil, vertical, horizontal or grid.
See also. the Focus chapter.
Property: :focus-scope
Section titled “Property: :focus-scope”Confine keyboard operation to a subtree.
Arguments. nil or modal. The default is nil.
Semantics. While a modal Host is visible, Tab, arrows and :keys resolve inside it first; the innermost visible modal scope wins. Outer :keys are unreachable, and printing keys that nothing inside the scope binds are ignored instead of reaching the buffer's keymaps. This is how a dialog keeps focus while it is open.
Example. (box :focus-scope 'modal :role "dialog" ...)
Errors. error→ etaf-renderer-error when the value is not nil or modal.
See also. the Focus and Keyboard chapters.
Property: :region
Section titled “Property: :region”Declare a user-addressable functional region on a Host.
Arguments. A property plist whose keys are :id (required, a non-nil symbol that identifies the region), :label (a display name), :key (one hint character other than ?), :enter (the default entry Host reference) and :neighbors (a plist mapping :left, :right, :up and :down to region ids). Unknown keys are rejected.
Semantics. Regions nest by Host ancestry, so a pane inside a window is found inside its parent region. The Region Navigation chapter defines the selector that jumps between regions, the entry memory and how adjacency orders the movement.
Example. (box :region (list :id 'editor :label "Editor" :key ?e :neighbors (list :right 'files)) ...)
Errors. error→ etaf-renderer-error for a malformed plist, an unknown key, a missing or non-symbol :id, an invalid :key, or a neighbour direction outside the four directions.
See also. Region Navigation.
References and Behaviors
Section titled “References and Behaviors”Property: :ref
Section titled “Property: :ref”The application's handle to a Host.
Arguments. nil, a symbol or a function. Other values are rejected.
Semantics. An explicit reference registers the committed Host under that value, and the geometry queries, focus functions and event functions accept it. A symbol is the common form; an instance-scoped opaque handle comes from etaf-local-ref. A function is accepted as an opaque handle and is never called by ETAF. A Host that carries no non-nil semantic property is not registered and cannot be found by a reference; :ref itself is such a property, so an explicit ref always registers. ETAF also gives every Host an internal generated reference that applications cannot reach.
When two committed Hosts carry the same explicit reference, the later commit wins; if that Host leaves, the reference resolves to nothing even though the other Host still carries it. An explicit reference is therefore a name for one Host at a time, not a shared alias.
Example. (box :ref 'search-results ...), then (etaf-host-ref-bounds runtime 'search-results).
Errors. error→ etaf-renderer-error when the value is not nil, a symbol or a function.
See also. Host references and etaf-local-ref.
Property: :use
Section titled “Property: :use”Attach one or more Behaviors to a Host.
Arguments. A Behavior source: a symbol naming a zero-argument constructor, a Behavior spec created with etaf-behavior-create, or a proper list of such sources. nil is accepted and attaches nothing.
Semantics. Each source resolves to a Behavior whose attributes are merged into the Host as Values and conflicts describes: event callbacks and :disabled compose, style values fill only where the Host is silent, and owned semantic properties conflict rather than override. A disabled Host installs no Behaviors. Actions, Behaviors and Error Boundaries defines constructors, the optional installer and cleanup.
Example. (text :use 'etaf-focusable "Open")
Errors. error→ etaf-renderer-error when the value is not a Behavior source or a proper list of them; error→ etaf-behavior-error for an unknown constructor, a constructor that returns something else, or two Behaviors with the same name on one Host.
See also. Actions, Behaviors and Error Boundaries.
Presentation and pointer feedback
Section titled “Presentation and pointer feedback”Property: :help-echo
Section titled “Property: :help-echo”The help text shown for a Host.
Arguments. A string, a native three-argument help-echo function, or nil.
Semantics. Ebox paints it as the node's help-echo text property, so Emacs shows the text (or calls the function) when the pointer rests on the Host. It does not create an activation boundary, a keymap or an ETAF hover event. A present nil blocks an ancestor's value.
Example. (text :help-echo "Reload from disk" :on-press #'reload "Reload")
See also. :hover-style and :hover-content for visual feedback.
Property: :pointer
Section titled “Property: :pointer”The mouse pointer shape for a Host.
Arguments. An Ebox pointer symbol such as hand, or nil.
Semantics. The pointer is painted as the node's pointer text property. A Host with a press or drag callback receives the hand pointer automatically unless it supplies :pointer; an explicit nil keeps the default pointer and blocks an ancestor's value.
Example. (text :pointer 'hand :on-press #'open "Read more")
See also. the pointer affordances described with :help-echo and :hover-style.
Property: :hover-style
Section titled “Property: :hover-style”The paint a Host shows while the pointer is over it.
Arguments. An even plist of paint properties: :color, :background-color, :font-weight, :font-style, :text-decoration-line, :text-decoration-style and :text-decoration-color. Theme tokens are resolved like any other style value.
Semantics. Ebox turns the plist into a mouse face that covers the Host while the pointer is over it; the face disappears when the pointer leaves. An unsupported key or an odd plist is an error when the node paints. A present nil removes an inherited hover face.
Example. (row :hover-style (list :background-color "gray25") ...menu item...)
Errors. error→ ebox-error for a property outside the paint list or an odd plist.
See also. :hover-content.
Property: :hover-content
Section titled “Property: :hover-content”Text shown in place of a Host's content while it is hovered.
Arguments. A string, or a zero-argument function returning a string; other values are accepted but show nothing. nil means no replacement.
Semantics. While the pointer is over the Host — or keyboard focus enters it, since focus shows the same affordance as hovering — ETAF replaces the text of the Host's first text descendant with the value, and restores the original text when the pointer or focus leaves. This is how a compact cell reveals a full label, a short path reveals a longer one, or a status shows its details on demand. Properties of the Host remain in effect; only the text changes.
Example. (text :hover-content "src/etaf-runtime.el (1420 lines)" "runtime.el")
See also. :hover-group, which switches several Hosts together.
Property: :drag-lock-modifier
Section titled “Property: :drag-lock-modifier”Keep a drag gesture alive across a button release.
Arguments. One of shift, control, meta, super or hyper, or nil. The default is nil, where releasing the button ends the gesture.
Semantics. A drag that starts with the lock modifier set stays active after the button is released, until the next press; the application can therefore implement click-then-drag sequences without holding the button. The property requires a drag callback: a lock modifier on a Host with no drag gesture is an error when the Host mounts, because there is no gesture to keep alive.
Example. (box :on-drag (lambda (gesture) (move-panel gesture)) :drag-lock-modifier 'shift ...)
Errors. error→ error when the modifier is unsupported, or when :drag-lock-modifier is given without a drag callback on the same Host.
See also. Dragging and Gestures.
Property: :hover-group
Section titled “Property: :hover-group”Make several Hosts share one hover state.
Arguments. Any value usable as a group key; nil gives the Host its own private group.
Semantics. Every Host that has both :hover-content and the same group key switches its hover content together: hovering or focusing any member shows the content of all members. The members need not be siblings, and a group with one member behaves like a plain :hover-content.
Example. (row (text :hover-group 'toolbar :hover-content "Bold (C-b)" "B") (text :hover-group 'toolbar :hover-content "Italic (C-i)" "I")) shows both full labels when either button is hovered.
See also. :hover-content.
Layout persistence
Section titled “Layout persistence”Property: :sticky
Section titled “Property: :sticky”Keep a Host visible while its buffer scrolls.
Arguments. nil or top. Any other value is rejected. The default is nil.
Semantics. A top Host stays reachable like a sticky header. Once a window's start reaches the Host, ETAF mirrors the Host's first row into that window's header line and makes the window body start after the Host, so the row is neither duplicated nor shifted; while point is inside the Host, the Host shows in place instead. A primary click on a mirrored cell runs that cell's press binding at its buffer position. The Runtime owns the buffer's header-line-format while a sticky Host is mounted, and one Host per Runtime is sticky: the last committed top Host wins.
Sticky is a row-level affordance: keep the Host to a single row, as in a toolbar or a section header, rather than a whole panel.
Example. A build log whose header stays visible while the log scrolls.
Runnable example
(etaf-view (column (row :sticky 'top :background-color "gray20" (text :font-weight 'bold "Build log")) (text :for (line '("12:01 configure" "12:02 compile" "12:03 link" "12:04 test" "12:05 package" "12:06 publish")) :key line (expr line))))Errors. error→ etaf-renderer-error when the value is not nil or top.
See also. Viewport and Scrolling.
Keyboard operation
Section titled “Keyboard operation”Property: :keys
Section titled “Property: :keys”Declare Host key bindings that run while the Host has focus or contains point.
Arguments. A list of (KEY COMMAND [LABEL]) entries: KEY is a key or key sequence as a kbd string or a vector, COMMAND is a zero-argument function, and the optional LABEL is a string that names the command in the ETAF help palette. nil declares no bindings.
Semantics. A binding works while focus, or point when nothing is focused, is inside the Host. The nearest Host that binds a key wins; a disabled Host contributes nothing; Host bindings take precedence over the buffer's own keymaps, including application emulation maps. A command runs like a keymap binding, so it may move focus or begin input on a Host it just revealed; wrap several writes in etaf-batch to publish them once. Keys and regions coexist: the framework reserves arrows, Tab, ?, Escape, M-g and C-g for focus, help and region navigation, and a binding may not collide with a reserved key or with another sequence elsewhere in the same Runtime. Keyboard defines the full model, including prefixes, reserved keys and diagnostics.
Example. (row :tab-index 0 :keys (list (list "RET" (lambda () (open item)) "Open")) ...)
Errors. error→ etaf-renderer-error from the keyboard layer at Host commit: a malformed entry, an empty key, a sequence that starts with a printing key, a framework-reserved key, or a collision with another binding in the same Runtime.
See also. Keyboard, and :region for region-level navigation.
Selector subjects
Section titled “Selector subjects”A Host's identity and accessible metadata are the facts Ebox selector queries match:
:classtokens become the node's classes;:idbecomes its id.- A non-nil
:rolebecomes theroleattribute. - Every
:aria-*property, including one explicitly set to nil, becomes an attribute with the name after the colon. - The
disabledstate comes from the same:disabledinput that protects activation, so a Host disabled at commit is matched by:disabled.
Selector matching follows the rendered Host tree, not the View tree, and comparison uses ECSS value formatting. The adapter facts :selector-attributes and :selector-state are how Ebox's library users attach extra subjects; they cannot be authored as View properties, and ETAF rejects them with error→ etaf-renderer-error. This is the boundary that lets component styles and application stylesheets select Hosts by meaning rather than by position; Component Styles and Themes builds on it.
Host references
Section titled “Host references”Function: etaf-local-ref (name)
Section titled “Function: etaf-local-ref (name)”Obtain a stable Host reference scoped to one Component instance.
Arguments. NAME is a non-nil symbol or string naming the reference within the instance.
Semantics.
- Call it while the Component is being set up, from
:bindingsor:setup. Equal names in one instance return the same opaque reference; different instances never share one, and a new instance gets a new reference. Pass it to a Host's:ref, or to the focus, event and geometry functions that take a Host reference. - The reference is a name, not state and not a key: it identifies the Host it is attached to, not the current value of anything.
- Only a committed Host resolves a reference. If publication fails, the previous target stays; if the Host with the reference is removed, the geometry queries return nil; a subsequent instance never inherits the old handle.
Returns. An opaque non-nil reference value.
Errors. error→ etaf-component-definition-error when called outside Component setup, or when NAME is not a non-nil symbol or string.
Example. A panel keeps a handle to its body so callers can measure or scroll it.
(etaf-define-component demo-panel () :bindings ((body (etaf-local-ref 'body))) :view (box :ref body ...))See also. :ref, Host references and the Component chapter.
Uniqueness and lifetime
Section titled “Uniqueness and lifetime”A reference resolves to at most one committed Host. That is why the old name for a removed Host returns nil instead of falling back to another Host that still carries it, and why an application should give each Host a distinct reference. etaf-local-ref maintains that property per instance; a hand-written symbol is the application's own responsibility. The generated references ETAF assigns to every Host are internal: they are not returned by the geometry queries and cannot be passed to them by an application.
Geometry queries
Section titled “Geometry queries”The geometry queries read the committed geometry of a mounted Runtime: what Ebox last laid out and painted. They are read-only, they do not publish or re-layout, and they signal when the Runtime is not mounted. Query after a change has drained — after a write's publication, or after etaf-runtime-flush — to see the new geometry, as Reactivity describes.
Function: etaf-host-ref-bounds (runtime host-ref)
Section titled “Function: etaf-host-ref-bounds (runtime host-ref)”Return the live buffer span of a Host.
Arguments. RUNTIME is a mounted Runtime. HOST-REF is an explicit reference registered in it.
Semantics. The result is a (START . END) cons from the Host's first visible painted cell to the end of its last painted row. A :visibility hidden Host in flow reports its blank slot; a Host that owns no cells, or whose reference is not registered, returns nil.
Returns. A cons of buffer positions, or nil.
Errors. error→ etaf-runtime-error when RUNTIME is not mounted.
Example. After layout, scroll a row into view from its bounds, or align a capture region to a panel.
See also. etaf-host-ref-position, etaf-host-ref-size and Scrolling.
Function: etaf-host-ref-at (runtime &optional position predicate)
Section titled “Function: etaf-host-ref-at (runtime &optional position predicate)”Find the explicit reference of the Host painted at a buffer position.
Arguments. RUNTIME is a mounted Runtime. POSITION defaults to point. PREDICATE, when non-nil, is a function of one reference that decides which Host is acceptable.
Semantics. The lookup starts at the innermost Host painted at POSITION and walks outwards through its committed ancestors, returning the first explicit reference — or, with PREDICATE, the first reference PREDICATE accepts. Generated references are never returned, so a Host the application did not reference is invisible to this query. This is what lets a key handler map point to the row it is on; etaf-host-ref-position is its inverse.
Returns. A reference value, or nil when no matching Host encloses POSITION.
Errors. error→ etaf-runtime-error when RUNTIME is not mounted.
Example. (etaf-host-ref-at runtime (point) (lambda (ref) (string-prefix-p "row-" (symbol-name ref)))) stops at the innermost ancestor whose reference names a row.
See also. etaf-host-ref-position and Keyboard.
Function: etaf-host-ref-size (runtime host-ref)
Section titled “Function: etaf-host-ref-size (runtime host-ref)”Return the painted size of a Host.
Arguments. RUNTIME is a mounted Runtime. HOST-REF is an explicit reference registered in it.
Semantics. The result is the committed border-box size of the Host's last layout: (WIDTH-PIXELS . HEIGHT-ROWS). Margins and neighbouring columns are excluded, clipping reduces the reported size, and an absent or wholly hidden Host returns nil. Read it after layout to prepare resources — for example to decide whether a preview fits — not per frame.
Returns. (WIDTH . HEIGHT), both positive, or nil.
Errors. error→ etaf-runtime-error when RUNTIME is not mounted.
Example. (etaf-host-ref-size runtime 'preview) decides whether the preview pane can show a picture.
See also. etaf-host-ref-bounds.
Function: etaf-host-ref-position (runtime host-ref)
Section titled “Function: etaf-host-ref-position (runtime host-ref)”Return the first live buffer position of a Host.
Arguments. RUNTIME is a mounted Runtime. HOST-REF is an explicit reference registered in it.
Semantics. The result is the position where the Host starts in the buffer. A Host that is not painted, or whose reference is not registered, returns nil.
Returns. A buffer position, or nil.
Errors. error→ etaf-runtime-error when RUNTIME is not mounted.
Example. (goto-char (etaf-host-ref-position runtime 'message-log)) moves point to a log's first line.
See also. etaf-host-ref-at, which maps a position back to a reference.
Reading geometry
Section titled “Reading geometry”The queries share one contract:
- They resolve references against committed state, so they never see a Host that has been removed or a property update that has not drained.
- They do not publish: calling them inside a watch or an effect does not cause another render, and they must not be used to poll per frame. Prepare resources after a layout change instead.
- They are safe to call from ordinary application code, including keyboard handlers, after checking that the Runtime is mounted.
- All four refuse an unmounted Runtime with
error→ etaf-runtime-error.
Semantic details
Section titled “Semantic details”Hosts and their Ebox nodes
Section titled “Hosts and their Ebox nodes”Each Host kind lowers to one Ebox node:
| Host kind | Ebox node |
|---|---|
text |
a text node with the resolved payload |
box, row, column, flex, grid |
a container node holding the children's nodes |
image |
a leaf node holding the prepared image |
fragment |
nothing; its children's nodes are inserted into the parent's child list |
The mapping is one Host to one node, and the node moves with its Host: reconciliation matches segments by the Host's identity, and a matched Host keeps its node and updates its properties and children in place. A removed Host releases its node.
Property resolution and cost
Section titled “Property resolution and cost”Property resolution is per Host: when a Host's segment initializes or its properties change, ETAF evaluates the Host's deferred property expressions once, resolves theme tokens, merges Behaviors, and computes the Ebox property list. The node is only updated when that list differs from the committed one.
- A Host whose properties and Theme are unchanged costs one identity comparison at update time.
- A Host whose plain style values changed follows a lighter path: the changed values are applied to the retained node without recomputing its metadata, Behaviors or children.
- Changing keys, semantics or the Theme takes the full path: the Host re-resolves its properties, re-registers its reference, and reconciles its Behaviors.
- The resolved property values reach the node for style properties; identity, accessibility and interaction facts are split off according to the table in Selector subjects and the interaction boundary Events describes. ETAF keeps its own copy of the semantic properties for its queries and for event dispatch.
The cost of a Host update is therefore proportional to that Host's own properties, not to the size of the View or the number of siblings; a Host that only reads static values costs nothing after it is first bound.
Sticky Hosts
Section titled “Sticky Hosts”The sticky mechanism is owned by the Runtime, not by the layout. When a Host with :sticky top commits, the Runtime records it; when a window's start reaches the Host's bounds, ETAF mirrors the Host's first row into that window's header line, adjusts the window's start so the body begins after the Host, and removes the mirror when the window scrolls back above the Host or point is inside it. Because the header line belongs to the window, the mechanism works per window: every window showing the buffer can have its own state. The mirror is a copy for display; it is refreshed when the Host is repainted, and clicking a mirrored cell runs the press binding of the cell at its original buffer position.
Errors and recovery
Section titled “Errors and recovery”Host problems fall into distinct groups, by the layer that detects them.
| Failure | Condition | What remains |
|---|---|---|
A style property the Host kind does not accept, or an invalid :class value |
error→ ebox-error when the node is built |
Nothing is published for that Host; a mounted previous state stays |
An invalid semantic value, or an Ebox-only property such as :content or :selector-attributes |
error→ etaf-renderer-error before the Host commits |
The previous committed properties and reference stay in effect |
| A conflicting owned semantic property from a caller or Behavior | error→ etaf-component-call-error during the merge |
The previous committed properties stay; the caller must resolve the conflict |
| An invalid Behavior source or a duplicate Behavior | error→ etaf-behavior-error during resolution |
The Host keeps its previous properties and Behaviors |
etaf-local-ref outside setup or with an invalid name |
error→ etaf-component-definition-error at the call |
No reference is created; the component instance is unaffected |
| A geometry query on an unmounted Runtime | error→ etaf-runtime-error at the call |
The query returns no value and no state changes |
The recovery principle is the same throughout: a Host commits as one unit. A failure leaves the previous committed Host in place — its properties, its node, its reference and its Behaviors — and the failing work is retried by the next drain if it was caused by data, or must be fixed in the source if it was caused by a malformed property. A Host that is removed simply stops existing for queries: its reference resolves to nothing and the geometry queries return nil.