Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
724 lines
32 KiB
Markdown
724 lines
32 KiB
Markdown
# Ebox user guide
|
||
|
||
[中文](ebox-user-guide.zh.md)
|
||
|
||
Ebox is the low-level Text/Box layout and buffer-rendering package. This guide
|
||
uses one public author grammar and keeps framework-integration details separate.
|
||
See the [API reference](ebox-api-reference.en.md) for the complete function
|
||
inventory.
|
||
|
||
## 1. Load Ebox
|
||
|
||
```elisp
|
||
(require 'ebox)
|
||
```
|
||
|
||
Loading Ebox does not create a buffer, enable a mode in the current buffer, or
|
||
build Rust.
|
||
|
||
## 2. Learn one author model
|
||
|
||
An Ebox document contains Text and Box nodes. The author grammar has exactly
|
||
seven entries:
|
||
|
||
| Entry | Meaning |
|
||
| --- | --- |
|
||
| `"text"` | Short form for one Text node. |
|
||
| `(text ... "text")` | Text with explicit text properties. |
|
||
| `(box ... CHILD...)` | A normal visual Box. |
|
||
| `(row ... CHILD...)` | A Box with simple horizontal child layout. |
|
||
| `(column ... CHILD...)` | A Box with simple vertical child layout. |
|
||
| `(flex ... CHILD...)` | A Box with Flex child layout. |
|
||
| `(grid ... CHILD...)` | A Box with Grid child layout. |
|
||
|
||
Children are always nested directly. A layout form is a Box with a selected
|
||
child-layout algorithm, not a different kind of visual object.
|
||
|
||
```elisp
|
||
(defvar ebox-guide-input
|
||
(ebox-build
|
||
'(column :padding ((lh 1) (ch 2))
|
||
:border ((px 1) solid "#8A93A6")
|
||
(text :color "#263244" "Research notes")
|
||
(row :item-gap (ch 1)
|
||
(box :id "status" :background-color "#F4F6FB" "Inbox")
|
||
(box :background-color "#EEF2FF" "Archive")))))
|
||
```
|
||
|
||
`ebox-build` returns an opaque `CanonicalEboxInput`. It keeps the canonical
|
||
forest and its source generation together. Pass that value unchanged to the
|
||
render and publication functions; ordinary author code does not extract or
|
||
reassemble its internal nodes.
|
||
|
||
Use `box` with geometry but no child when an empty rectangular area is needed.
|
||
No extra node type is necessary.
|
||
|
||
### Prefer defaults and inheritance
|
||
|
||
Write only what changes the intended result. Omit redundant default values,
|
||
place shared text styles on an existing common parent, and keep only child
|
||
overrides. Reference panels still spell out the property they teach, even
|
||
when its value is the default.
|
||
|
||
Ebox inherits `:color`, `:font-family`, `:font-size`, `:font-weight`,
|
||
`:font-style`, `:text-align`, `:wrap-mode`, and `:visibility`. Geometry and
|
||
background colors do not inherit; an unpainted child can show its parent's
|
||
background. For example:
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(column :width (ch 40) :color "#1F2328" :bgcolor "#FAF7F0"
|
||
(box "Shared text color")
|
||
(box :color "#B45309" "Local accent")
|
||
(box :height (lh 1))))
|
||
```
|
||
|
||
Inheritance does not expand where a property may be authored. For example,
|
||
`:text-align` is declared on `box`; it cannot be moved onto a `row`, `column`,
|
||
`flex`, or `grid` merely because its value can inherit.
|
||
|
||
In this ordinary Column, the separator fills the available width and explicitly
|
||
reserves one line with `:height (lh 1)`. No width or repeated background is needed.
|
||
An otherwise empty Box with `auto` height has zero content height; use
|
||
`:height (lh 1)` when one blank line is intended. Padding and borders can still
|
||
add their own outer extent.
|
||
This is context-dependent: an auto-width child in a Row uses intrinsic width,
|
||
and Flex/Grid have their own item sizing rules. Do not replace `stretch` with
|
||
`auto` everywhere, or remove a default-looking value that overrides inherited
|
||
styles, a stylesheet, or another shorthand. Zero sides can often be omitted
|
||
with `:padding-inline` or `:padding-block` when no other declaration sets them.
|
||
|
||
## 3. Choose the layout that states the intent
|
||
|
||
Use `box` for ordinary content, `row` or `column` for direct one-axis
|
||
composition, `flex` when free space or wrapping matters, and `grid` for
|
||
two-dimensional tracks or explicit placement.
|
||
|
||
### Row and column
|
||
|
||
`row` and `column` accept `:item-gap` and `:cross-align`. Their children remain
|
||
ordinary Text or Box nodes.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(row :item-gap (ch 2) :cross-align center
|
||
(box :width (ch 12) "Left")
|
||
(box :width (ch 12) "Right")))
|
||
```
|
||
|
||
### Flex
|
||
|
||
Flex container properties belong to `flex`. Participation properties belong
|
||
directly to a child `box` because they describe the parent-child relationship.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(flex :width (px 480)
|
||
:flex-flow (row wrap)
|
||
:gap ((lh 1) (px 12))
|
||
(box :flex (1 1 auto) "Primary")
|
||
(box :flex-grow 2 "Secondary")))
|
||
```
|
||
|
||
### Grid
|
||
|
||
Grid placement is one-based. Tracks may be fixed, `auto`, fractional,
|
||
`minmax`, or `repeat` values. Placement properties also belong directly to a
|
||
child `box`.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(grid :width (px 640)
|
||
:grid-template-columns ((px 200) (fr 1) (fr 1))
|
||
:grid-template-rows ((lh 1) (lh 1))
|
||
:gap ((lh 1) (px 12))
|
||
(box :grid-column (1 :span 3) "Header")
|
||
(box :grid-column 1 :grid-row 2 "Navigation")
|
||
(box :grid-column 2 :grid-row 2 "Main")
|
||
(box :grid-column 3 :grid-row 2 "Aside")))
|
||
```
|
||
|
||
## 4. Use geometry and paint properties
|
||
|
||
Box geometry uses explicit `(unit number)` values. The same syntax applies to
|
||
width, height, their minimum/maximum bounds, padding, margins, gaps, borders,
|
||
Flex basis, and fixed Grid tracks. Ebox implements a CSS sizing subset with an
|
||
Elisp data representation, not CSS strings such as `"80ch"`.
|
||
|
||
| Unit | Meaning |
|
||
| --- | --- |
|
||
| `(px 240)` | 240 pixels. |
|
||
| `(% 50)` | 50% of the property's containing-block reference size. |
|
||
| `(vw 100)` | 100% of the viewport width. |
|
||
| `(vh 100)` | 100% of the viewport height. |
|
||
| `(ch 80)` | 80 times the effective font's advance width for `0`. |
|
||
| `(lh 3)` | Three effective line heights. |
|
||
|
||
`ch` does not count arbitrary characters or CJK glyphs.
|
||
`1vw` and `1vh` each mean 1% of the corresponding viewport axis. A displayed
|
||
Ebox viewport is the target window's usable body area, excluding its mode line
|
||
and header line; a Playground preview therefore uses the preview window.
|
||
|
||
<a id="size-unit-constraints"></a>
|
||
|
||
### Unit constraints by property
|
||
|
||
The following table is the unit contract for all Ebox author entry points.
|
||
Inline means horizontal and block means vertical in Ebox's supported writing
|
||
mode. These axis restrictions deliberately narrow CSS's general length model.
|
||
|
||
| Value context | Allowed units | Properties |
|
||
| --- | --- | --- |
|
||
| Inline geometry | `px`, `ch`, `vw`, `%` | `width`, `min-width`, `max-width`; left/right and inline padding/margin; `column-gap`; Row `item-gap`; Grid column tracks. |
|
||
| Block geometry | `lh`, `vh`, `%` | `height`, `min-height`, `max-height`; top/bottom and block padding/margin; `row-gap`; Column `item-gap`; Grid row tracks. |
|
||
| Positioned offsets | Inline units for `left`; block units for `top` | Signed lengths and size expressions; `%` refers to the paint host's corresponding content axis. |
|
||
| Flex main size | The parent Flex's main-axis units | `flex-basis` and the basis in `(grow shrink basis)`; row directions use inline units, column directions use block units. |
|
||
| Border paint thickness | `px`, `ch`, `lh`, `vw`, `vh` | `border-width`, each side's border width, and the width component of border shorthands; percentages are forbidden. |
|
||
|
||
The same restrictions apply recursively to every branch of `calc`, `min`,
|
||
`max`, and `clamp`, and to nested Grid track functions. A disallowed unit is
|
||
an error when `ebox-build` constructs the tree, even if arithmetic would
|
||
cancel it or another branch would win. For example, `:height (px 24)`,
|
||
`:width (lh 3)`, and `:height (min (lh 2) (px 24))` are rejected.
|
||
Border thickness is a separate paint value, so `:border ((px 1) solid "red")`
|
||
is valid on all four sides.
|
||
|
||
Stylesheets and dynamic updates follow the same contract: after computed styles
|
||
are known and before layout, Ebox rechecks the parent Flex direction and child
|
||
basis. Invalid updates do not publish to an existing buffer. Numeric `:flex 1`
|
||
uses an implicit `(% 0)` basis, valid on either main axis; explicit bases still
|
||
follow the parent direction.
|
||
|
||
Shorthands are checked after expansion onto their sides or axes. A one-value
|
||
`padding`, `margin`, or `gap` must therefore be valid on both axes: use `%`
|
||
for such a shared length, or spell out the two axes, for example
|
||
`:padding ((lh 1) (ch 2))` and `:gap ((lh 1) (px 12))`.
|
||
Use `:padding-inline (px 12)` or `:padding-block (lh 1)` to set only one axis.
|
||
|
||
Percent widths refer to containing-block width; percent heights require a
|
||
definite containing-block height and otherwise follow the property's
|
||
indefinite-size rule. Percentage padding and margins on every side refer to
|
||
containing-block width. Border widths do not accept percentages.
|
||
Negative margins and `auto` margins are outside this subset; the current
|
||
buffer backend does not implement overlapping margin geometry.
|
||
|
||
| Property | Keywords | Default |
|
||
| --- | --- | --- |
|
||
| `width`, `height` | `auto`, `min-content`, `max-content`, `fit-content`, `stretch` | `auto` |
|
||
| `min-width`, `min-height` | `auto`, `min-content`, `max-content`, `fit-content`, `stretch` | `auto` |
|
||
| `max-width`, `max-height` | `none`, `min-content`, `max-content`, `fit-content`, `stretch` | `none` |
|
||
|
||
For a normal block root with an available viewport, `auto` width fills the
|
||
available space and `auto` height follows content. `min-content` and
|
||
`max-content` request intrinsic sizes. `fit-content` fits available space
|
||
between those intrinsic bounds; `stretch` fills available space with the
|
||
margin box. `none` removes a maximum-size constraint. Automatic minimums
|
||
depend on layout; an explicit `(px 0)` minimum allows an item to shrink below
|
||
its automatic content minimum on the inline axis. Use `(lh 0)` for the block
|
||
minimum. An empty Box with no padding, border, or explicit height has zero
|
||
height; `(box :height (lh 1))` explicitly reserves one blank line.
|
||
|
||
The four size functions compose units without evaluating Lisp during layout:
|
||
|
||
```elisp
|
||
'(column :width (min (% 100) (ch 80))
|
||
:height (calc (- (vh 100) (lh 1)))
|
||
(box :width (max (px 120) (% 25)) "Sidebar")
|
||
(box :width (clamp (ch 20) (% 50) (ch 60)) "Body"))
|
||
```
|
||
|
||
`calc` accepts one arithmetic expression; `+` and `-` combine lengths, `*`
|
||
multiplies a length by a scalar, and `/` divides it by a nonzero scalar.
|
||
`min` and `max` choose from one or more permitted lengths. `clamp` takes
|
||
minimum, preferred, and maximum lengths. Functions may nest. Bare numbers inside math
|
||
are scalars, never implicit `ch` or `lh` lengths.
|
||
|
||
Units and functions remain data until layout, so viewport and containing-block
|
||
changes are resolved again. Floating-point intermediate results are preserved:
|
||
`33vw` of an 853-pixel viewport is 281.49 pixels before display quantization.
|
||
The buffer backend materializes block geometry in whole lines; fractional
|
||
line targets are quantized at that boundary. Vertical `px` lengths are not
|
||
part of the author contract. `:overflow hidden` clips excess vertical lines and
|
||
limits each content line to the measured content width. It retains the fitting
|
||
prefix without splitting supported text clusters; a glyph or image that cannot
|
||
fit is omitted as a whole. Fixed display spaces can be shortened to the exact
|
||
remaining pixel width. Any remaining gap is filled with box content space, so
|
||
padding and borders retain their positions. This is pixel-width truncation,
|
||
not partial-glyph/image masking or automatic ellipsis. Omitted child text does
|
||
not leave its help, hover, pointer, or keymap active in the filler.
|
||
|
||
For example, `(box :width (ch 3) :wrap-mode none :overflow hidden "ABCDEFGHIJ")`
|
||
keeps only the prefix that fits three zero-glyph advances; with `visible`,
|
||
overflow may extend outside the computed width. Set `hidden` on the container
|
||
whose content boundary should be enforced. It applies to that container's
|
||
composed child output as well as its direct text. `scroll` provides vertical
|
||
scrolling and does not add horizontal scrolling.
|
||
|
||
Bare geometry numbers, one-element pixel lists, `viewport`, `viewport-height`,
|
||
`contain`, and parameterized `fit-content` are rejected. Even zero needs an
|
||
axis-appropriate unit: `(px 0)` inline or `(lh 0)` block. Grid `(fr n)` remains a track fraction; Grid placement and
|
||
Flex growth/shrink factors remain dimensionless numbers.
|
||
|
||
Text style declarations accept only font, foreground/background, and
|
||
text-decoration properties. Text also accepts the four native node capabilities
|
||
described below.
|
||
Padding, margin,
|
||
border, size, `:outer`, overflow, visibility, and wrapping policy belong only
|
||
to Box. Font and color on Box may feed inherited Text facts, but never give
|
||
Text Box geometry.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(box :width (px 420)
|
||
:padding ((lh 1) (ch 2))
|
||
:margin ((lh 0) (ch 1))
|
||
:border ((px 1) solid "#8A93A6")
|
||
:color "#263244"
|
||
:background-color "#FFFFFF"
|
||
"A readable panel"))
|
||
```
|
||
|
||
Use `:outer inline` or `:outer block` to state how a Box participates in its
|
||
parent. The child-layout algorithm still comes from the form name.
|
||
|
||
<a id="retained-layers"></a>
|
||
|
||
### Retained layers
|
||
|
||
Use positioning properties on existing Box forms to overlap content. Give the
|
||
host an explicit height when absolute children need a reserved canvas: they do
|
||
not increase its automatic flow size.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(box :width (ch 12) :height (lh 3)
|
||
"ABCDEFGHIJKL\nabcdefghijkl\n0123456789ab"
|
||
(box :position absolute :left (ch 2) :top (lh 1)
|
||
:width (ch 5) :height (lh 1)
|
||
:background-color "#334155" :color "#FFFFFF"
|
||
"Panel")))
|
||
```
|
||
|
||
`:position relative` keeps the child's ordinary slot and moves only its paint.
|
||
`:position absolute` removes the child from normal, Row, Column, Flex, or Grid
|
||
flow and places it from the parent content origin. Signed `:left` uses inline
|
||
units; signed `:top` uses block units and is floored to whole host rows at final
|
||
placement. Both accept size expressions. A local host clips its composed output
|
||
to its canvas. Nested local groups are automatic: a child's high `:z-index`
|
||
cannot jump above a sibling of its host. Integer depths paint low to high;
|
||
normal content precedes positioned content at equal depth, and equal-depth
|
||
positioned boxes follow document order.
|
||
|
||
The upper box covers its content, padding, and border, including blanks, while
|
||
its margin leaves the lower content visible. Visible text retains its native
|
||
help, pointer, hover, and keymap properties; covered text contributes none of
|
||
those interactions. The retained lower node and its semantic ID still exist.
|
||
Updates to it become visible with their latest content and properties when
|
||
the covering box moves or becomes hidden.
|
||
|
||
Use a root layer to place a panel beyond a clipped local container while
|
||
keeping its logical parent and inherited styles:
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(box :width (ch 20) :height (lh 4)
|
||
(box :height (lh 1) :overflow hidden
|
||
(box :id "trigger" :width (ch 6) "Open")
|
||
(box :id "panel" :position absolute :layer root
|
||
:anchor "trigger" :placement bottom-start
|
||
:width (ch 8) :height (lh 2)
|
||
"Choice A\nChoice B"))))
|
||
```
|
||
|
||
`:layer root` selects the root content canvas as the paint target. It does not
|
||
reparent the panel for selectors, inheritance, or region updates. `:anchor`
|
||
uses a unique semantic ID in the paint host's logical subtree. Both properties
|
||
require absolute positioning. Placements are `bottom-start`, `bottom-end`,
|
||
`top-start`, and `top-end`; offsets adjust the requested origin. Panels flip
|
||
vertically if the opposite side fits, then shift within the host canvas.
|
||
Missing or invisible anchors do not paint a panel; ambiguous IDs and placement
|
||
dependency cycles are errors.
|
||
|
||
Composition uses horizontal pixels and one shared vertical text-row grid.
|
||
Positioned subtrees must have the same effective font line height as their
|
||
host. Whole supported clusters survive cuts; partially covered glyphs/images
|
||
are replaced with neutral space, and fixed display spaces can be shortened.
|
||
There is no alpha blending, half-glyph masking, or child frame. Layered trees
|
||
fall back to Elisp when native reflow cannot represent them.
|
||
|
||
Publication remains a TP transaction, including hidden-node updates and
|
||
rollback. Fixed-footprint local changes can recompose only the affected host;
|
||
geometry changes and root-layer dependencies may require broader work. No
|
||
per-update latency or universal local-update guarantee is implied. Run
|
||
`make layer-tests` for the focused contract. The separate `ebox-playground`
|
||
package provides `layer-minimal.ebox` and `layer-reference.ebox`; batch tests
|
||
do not establish GUI visual parity or remote CI success.
|
||
|
||
### Wrapping and the optional EKP dependency
|
||
|
||
`:wrap-mode word` wraps ordinary words and permits character breaks for CJK;
|
||
`char` breaks at the common text-cluster boundaries supported by Ebox; `none`
|
||
keeps explicit newlines without adding soft line breaks. `kp` delegates
|
||
paragraph layout to the independent [EKP package](https://github.com/Kinneyzhang/emacs-kp)
|
||
using the Knuth–Plass algorithm. The mode belongs to a Box and is inherited by
|
||
its text content.
|
||
|
||
Install EKP 1.0.0 or newer, or add its source directory to `load-path`, before
|
||
using `:wrap-mode kp`. It is an optional dependency loaded on demand; ordinary
|
||
Ebox layouts require only ECSS and TP. Choosing `kp` without a compatible EKP
|
||
signals an installation or update error; install EKP or explicitly select
|
||
another mode. Ebox never silently switches to word wrapping. The EKP accelerator and the Ebox Rust
|
||
reflow module are separate optional modules; enabling one does not install the
|
||
other.
|
||
|
||
```elisp
|
||
(add-to-list 'load-path "/path/to/ekp")
|
||
(ebox-render
|
||
(ebox-build '(box :width (ch 40) :wrap-mode kp :overflow hidden
|
||
"Knuth–Plass considers the paragraph when choosing line breaks.")))
|
||
```
|
||
|
||
Common combining marks, variation selectors, emoji modifiers, ZWJ sequences,
|
||
and regional-indicator pairs are kept together by Ebox's text-cluster handling.
|
||
This is not complete Unicode grapheme segmentation or browser-level
|
||
bidirectional layout. Font shaping and glyph availability remain dependent on
|
||
Emacs, the selected fonts, and the window system.
|
||
|
||
<a id="native-node-interaction"></a>
|
||
### Native node interaction
|
||
|
||
Text and every Box form accept `:help-echo`, `:pointer`, `:hover-style`, and
|
||
`:keymap`. These are explicit node capabilities outside the CSS cascade.
|
||
For example, render a Box with dynamic help, a hand pointer, hover paint, and
|
||
one callback for both mouse and keyboard activation:
|
||
|
||
```elisp
|
||
(ebox-render-to-buffer
|
||
"*Ebox Interaction*"
|
||
(ebox-build
|
||
`(box :id "action" :padding ((lh 1) (ch 2))
|
||
:border ((px 1) solid "#5893A3")
|
||
:help-echo ,(ebox-help-create
|
||
(lambda ()
|
||
(format-time-string "Help requested at %H:%M:%S")))
|
||
:pointer hand
|
||
:hover-style (:color "#FFFFFF" :background-color "#286477"
|
||
:text-decoration-line underline)
|
||
:keymap ,(ebox-keymap-create :activate #'describe-mode)
|
||
"Click or press RET / SPC here to describe this buffer's mode.")))
|
||
```
|
||
|
||
`:help-echo` accepts a string, a native function with arguments
|
||
`(WINDOW OBJECT POSITION)`, or `nil`. Prefer `ebox-help-create` to adapt a
|
||
zero-argument business function returning a string or `nil`; Ebox supplies the
|
||
hovered buffer's context. Emacs calls the function when requesting help; the DSL
|
||
does not call it while building the tree. `:pointer` accepts
|
||
`text`, `arrow`, `vdrag`, `modeline`, `hand`, `hdrag`, `nhdrag`, `hourglass`,
|
||
or `nil`. Help display and exact pointer appearance depend on the user's Emacs
|
||
settings and window system.
|
||
|
||
`:hover-style` accepts `nil` or an Ebox paint plist containing only `:color`,
|
||
`:background-color`, `:text-decoration-line`, `:text-decoration-color`, and
|
||
`:text-decoration-style`. Within a rendered line, one declaring node's text
|
||
and padding share a native `mouse-face`, with unspecified attributes taken from
|
||
that node's base style. Differently colored child text covered by this hover
|
||
uses the same hover base. Physical left/right borders are excluded from hover;
|
||
horizontal border strokes remain intact. Font metrics and geometry do not change.
|
||
The [API reference](ebox-api-reference.en.md#native-node-capabilities)
|
||
defines the accepted values. Ebox compiles this plist to native `mouse-face`;
|
||
there is no arbitrary native-property passthrough.
|
||
|
||
Box help, pointer, and keymap cover its rendered content, padding, and border,
|
||
excluding that Box's margin and structural newlines; hover follows the rule
|
||
above. A nested Text or Box can replace
|
||
each capability with an explicit value. Explicit `nil` blocks an enclosing
|
||
value; an omitted property leaves the enclosing surface coverage in place.
|
||
|
||
`:keymap` accepts a native Emacs keymap or `nil`. Prefer `ebox-keymap-create`:
|
||
its `:activate` callback takes no arguments and handles `RET`, `[return]`, `SPC`,
|
||
and `[mouse-1]`. Use `:bindings` for an alist of key-description strings or event
|
||
vectors paired with zero-argument callbacks. The helper uses the event window's
|
||
buffer for mouse callbacks, so a callback can directly update a semantic ID
|
||
with `(ebox-region-update "action" :help-echo "Activated")`.
|
||
|
||
Move point into the surface to use keyboard bindings. Optional commands
|
||
`ebox-next-interaction` and `ebox-previous-interaction` move between declaring
|
||
nodes that currently render a usable keymap. Each owner contributes one stop,
|
||
even if its map covers padding or several lines; explicit child maps are
|
||
separate stops. Empty maps, explicit `nil`, and invisible or clipped-away
|
||
content do not add stops. Navigation follows the current committed buffer, so
|
||
updates do not require a separate refresh. At a boundary it reports a user error
|
||
without wrapping or moving point. These commands add no bindings automatically.
|
||
For example, opt in for buffers using `ebox-buffer-mode`:
|
||
|
||
```elisp
|
||
(keymap-set ebox-scroll-map "TAB" #'ebox-next-interaction)
|
||
(keymap-set ebox-scroll-map "<backtab>" #'ebox-previous-interaction)
|
||
```
|
||
|
||
Raw native keymaps are also accepted; their mouse commands must handle the
|
||
event's target buffer themselves. Ebox uses native command dispatch and does
|
||
not create an application state or focus-management system. A hand pointer
|
||
alone does not bind a click command.
|
||
|
||
The Playground's [interaction lab](../../../ebox-playground/README.md#native-interaction-lab)
|
||
keeps its business functions and state in `interaction-reference.el`, with its
|
||
layout in `interaction-reference.ebox`. It demonstrates callbacks, nested
|
||
overrides, and replacing or removing all four capabilities through public
|
||
region updates.
|
||
|
||
## 5. Render and publish
|
||
|
||
`ebox-render` returns propertized text without changing a live buffer.
|
||
`ebox-render-to-buffer` mounts a retained TP surface and returns its buffer.
|
||
Both functions, and `ebox-display-buffer`, accept the opaque value returned by
|
||
`ebox-build`.
|
||
|
||
```elisp
|
||
(ebox-render ebox-guide-input)
|
||
(ebox-render-to-buffer "*Ebox Guide*" ebox-guide-input)
|
||
```
|
||
|
||
To display the result as well, use `ebox-display-buffer`. It finishes rendering
|
||
before asking Emacs to place the buffer, respects `display-buffer-alist`, and
|
||
accepts the same optional action argument as native `display-buffer`:
|
||
|
||
```elisp
|
||
(ebox-display-buffer "*Ebox Guide*" ebox-guide-input
|
||
'(display-buffer-pop-up-window))
|
||
```
|
||
|
||
It returns the buffer and does not select the result or delete other windows.
|
||
A rendering error leaves the window layout untouched. A custom display action
|
||
can still have its own window effects; if it fails after publication, the
|
||
successfully rendered buffer remains available. Use `ebox-render-to-buffer`
|
||
when the caller owns display placement entirely.
|
||
|
||
Build a fresh canonical input and use `ebox-commit` for an atomic update:
|
||
|
||
```elisp
|
||
(ebox-commit
|
||
"*Ebox Guide*"
|
||
(ebox-build
|
||
'(column :padding ((lh 1) (ch 2))
|
||
(text "Updated notes")
|
||
(box :key body "The new input is caller-owned."))))
|
||
```
|
||
|
||
Validation, rendering, or publication failure leaves the previous buffer and
|
||
runtime state intact. `ebox-buffer-update-report` returns a defensive copy of
|
||
the last successful update report.
|
||
|
||
## 6. Query and update a mounted surface
|
||
|
||
`:id`, `:class`, and `:key` are author metadata. Selectors use ECSS semantics.
|
||
In the target mounted buffer, pass the semantic `:id` directly:
|
||
|
||
```elisp
|
||
(with-current-buffer "*Ebox Guide*"
|
||
(ebox-region-update "status" :color "#166534"))
|
||
```
|
||
|
||
For an explicit buffer address, resolve an `:id` to an opaque, surface-scoped
|
||
handle:
|
||
|
||
```elisp
|
||
(let ((handle (ebox-region-resolve "*Ebox Guide*" "status")))
|
||
(ebox-region-update handle :color "#166534"))
|
||
```
|
||
|
||
The same API replaces native capabilities on Text or Box regions. For the
|
||
interaction example above:
|
||
|
||
```elisp
|
||
(with-current-buffer "*Ebox Interaction*"
|
||
(ebox-region-update "action" :help-echo "Updated help" :pointer 'arrow
|
||
:hover-style '(:background-color "#F6D6AB"))
|
||
(ebox-region-update "action" :help-echo nil :pointer nil
|
||
:hover-style nil :keymap nil))
|
||
```
|
||
|
||
Update arguments are evaluated Elisp, so literal symbols and plists need
|
||
quotes, unlike properties inside already quoted DSL data. Omitted update
|
||
properties stay unchanged. Explicit `nil` clears that capability and blocks
|
||
an enclosing value.
|
||
Ebox snapshots keymaps; replace `:keymap` to publish new bindings instead of
|
||
mutating the original map. Clearing a node keymap leaves ordinary buffer and
|
||
global bindings available.
|
||
|
||
`ebox-selector-query-buffer` returns document-ordered matches from a mounted
|
||
buffer. `ebox-selector-update-buffer` applies one style update to all editable
|
||
matches. Numeric region ids are diagnostic render metadata, not stable update
|
||
handles.
|
||
|
||
## 7. Resize and scroll
|
||
|
||
When content exceeds a finite height, `:overflow scroll` creates an internal
|
||
scroll window. Keyboard scrolling targets point; wheel and trackpad scrolling
|
||
target the mouse position carried by the event. Remaining distance passes
|
||
through enclosing scroll owners from inner to outer, then to ordinary Emacs
|
||
scrolling. Events over another column, outside a box, or on the mode line do
|
||
not select an unrelated scroll box elsewhere on the page.
|
||
|
||
```elisp
|
||
(ebox-build
|
||
'(box :id log :width (px 420) :height (lh 8) :overflow scroll
|
||
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9"))
|
||
```
|
||
|
||
Visible mounted buffers follow one chosen display window. Each mounted buffer
|
||
owns one viewport layout: showing the same buffer in two windows with different
|
||
widths does not provide two independent layouts. Mount the same canonical input
|
||
in separate buffers when each view needs its own width. Integrations may apply
|
||
an explicit viewport, measured in pixels horizontally and lines vertically,
|
||
with:
|
||
|
||
```elisp
|
||
(ebox-rerender-buffer-with-context
|
||
(get-buffer "*Ebox Guide*") 800 30)
|
||
```
|
||
|
||
## 8. Standalone `.ebox` files
|
||
|
||
A `.ebox` file contains one ordinary Elisp expression whose value is the layout
|
||
data passed to `ebox-build`. Quote the whole list for a static layout:
|
||
|
||
```elisp
|
||
'(column :padding ((lh 1) (ch 2))
|
||
:cross-align center
|
||
(text :color "#263244" "Title")
|
||
(box :width (px 240) "Body"))
|
||
```
|
||
|
||
The sibling `ebox-playground` reads exactly one expression, evaluates it with
|
||
lexical bindings, and passes the resulting data to `ebox-build`. It does not
|
||
evaluate individual properties or children. The expression is exactly what you
|
||
would write as the argument to `(ebox-build ...)` in an `.el` file; omit that
|
||
outer call in `.ebox`.
|
||
|
||
Put necessary business functions and variables in a same-basename `.el` file
|
||
beside the layout. For `notes.ebox`, the Playground loads `notes.el` when it
|
||
exists, before evaluating the layout on every preview. This applies to
|
||
`C-c C-c`, `ebox-playground-open-file`, and file render entry points. It loads
|
||
the exact `.el` source, so saving changes is enough for the next preview;
|
||
there is no `require` cache or preference for a stale `.elc`. If the companion
|
||
is absent, the standalone `.ebox` still works. Companion load errors stop the
|
||
render before the layout is evaluated.
|
||
|
||
For example, `notes.el` owns the data and action:
|
||
|
||
```elisp
|
||
;;; notes.el --- Notes example behavior -*- lexical-binding: t; -*-
|
||
(defvar notes-body "A long article.")
|
||
|
||
(defun notes-help ()
|
||
"Return business help for the article."
|
||
(format "Article: %s" notes-body))
|
||
|
||
(defun notes-activate ()
|
||
"Mark the article in the current Ebox buffer."
|
||
(ebox-region-update "article" :color "#166534"))
|
||
```
|
||
|
||
`notes.ebox` stays focused on the layout:
|
||
|
||
```elisp
|
||
`(column :padding ((lh 1) (ch 2))
|
||
(box :id "article" :width (px 480)
|
||
:help-echo ,(ebox-help-create #'notes-help)
|
||
:pointer hand
|
||
:keymap ,(ebox-keymap-create :activate #'notes-activate)
|
||
,notes-body))
|
||
```
|
||
|
||
Backquote keeps layout data literal, `,` inserts a value, and `,@` inserts a
|
||
list of children. Avoid helpers whose only purpose is hiding repeated DSL
|
||
property lists. Each preview loads the companion and then evaluates the layout
|
||
once; ordinary Elisp variable definitions determine whether application state
|
||
is initialized, retained, or reset.
|
||
|
||
Migration from the old property-evaluation format is explicit: add a quote to
|
||
the whole static layout and remove the quotes around its list and symbol
|
||
properties. For dynamic layouts, use backquote and commas at the values to
|
||
evaluate. For example, old `:padding '(1 2)` becomes
|
||
`:padding ((lh 1) (ch 2))` inside a quoted layout, and a computed pixel width
|
||
becomes `:width (px ,(+ 200 40))` inside a backquoted layout. Bare structural forms
|
||
are no longer interpreted as DSL automatically; there is no legacy-format
|
||
auto-detection.
|
||
|
||
## 9. Typed integration API
|
||
|
||
Frameworks that already normalize author input may bypass the list DSL. This
|
||
is an integration API, not a second author grammar. One source builder owns
|
||
the complete source generation. Each TextNode or BoxNode receives an opaque
|
||
handle from that builder and node-owned facts projected from the same
|
||
normalized declarations. The framework then seals the builder and transports
|
||
the forest and source index together as one `CanonicalEboxInput`.
|
||
|
||
```elisp
|
||
(let* ((builder (ebox-source-builder-create))
|
||
(root-declarations nil)
|
||
(left-declarations nil)
|
||
(right-declarations nil)
|
||
(root-handle
|
||
(ebox-source-builder-bind
|
||
builder :declarations root-declarations))
|
||
(left-handle
|
||
(ebox-source-builder-bind
|
||
builder :declarations left-declarations))
|
||
(right-handle
|
||
(ebox-source-builder-bind
|
||
builder :declarations right-declarations))
|
||
(left
|
||
(ebox-text-create
|
||
:value "Left"
|
||
:source-handle left-handle
|
||
:owned-facts
|
||
(ebox-canonical-facts-from-declarations
|
||
'text left-declarations)))
|
||
(right
|
||
(ebox-text-create
|
||
:value "Right"
|
||
:source-handle right-handle
|
||
:owned-facts
|
||
(ebox-canonical-facts-from-declarations
|
||
'text right-declarations)))
|
||
(root
|
||
(ebox-box-create
|
||
:layout (ebox-row-layout-create
|
||
:item-gap '(ch 1) :cross-align 'center)
|
||
:children (list left right)
|
||
:source-handle root-handle
|
||
:owned-facts
|
||
(ebox-canonical-facts-from-declarations
|
||
'row root-declarations))))
|
||
(ebox-canonical-input-create
|
||
(list root)
|
||
(ebox-source-builder-finish builder)))
|
||
```
|
||
|
||
`ebox-source-builder-create`, `ebox-source-builder-bind`,
|
||
`ebox-source-builder-finish`, `ebox-canonical-facts-from-declarations`, the
|
||
typed node/layout constructors, and `ebox-canonical-input-create` belong to
|
||
this integration boundary. Here `:layout`, `:children`, `:source-handle`, and
|
||
`:owned-facts` are evaluated constructor fields. They are not author
|
||
properties and do not extend the seven-entry grammar.
|
||
|
||
## 10. Optional native reflow and verification
|
||
|
||
The Rust module is optional and Ebox never builds it while loading:
|
||
|
||
```elisp
|
||
(ebox-native-status)
|
||
(ebox-native-build)
|
||
```
|
||
|
||
From the repository root:
|
||
|
||
```sh
|
||
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||
make interaction-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||
make dsl-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
|
||
```
|