diff options
| author | user <user@clank> | 2026-08-20 23:29:18 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-20 23:29:18 +0200 |
| commit | 91cac346fa3e3d24844cae7fb4ebfc2e425d3676 (patch) | |
| tree | 62c8ebdd49e2dd15d7dd9d0f7b4cf2302f961641 | |
| parent | slosh: OSC 8 hyperlinks pass through the compositor (diff) | |
| download | slosh-91cac346fa3e3d24844cae7fb4ebfc2e425d3676.tar.gz slosh-91cac346fa3e3d24844cae7fb4ebfc2e425d3676.tar.xz slosh-91cac346fa3e3d24844cae7fb4ebfc2e425d3676.zip | |
slosh: fold floating panes and the drop grammar into DESIGN.md
D22 records the float contract: the minimized pattern (a flag and a wanted
rect on a leaf that keeps its seat), derived-vs-remembered rects, the hit
list as the z-order, the pop to the centre, the shadow-not-a-state, the
backdrop invariant, edge-pinned resizes, and the key decisions -- with M10c
(layout files, dump-layout, placement over the API, graphics occlusion)
recorded as still owed. M11 extends the drag machine section with the drop
grammar: swap centre plus third-width insert bands, the fill as the
promise, release derived from the painted hits, and the sibling-reorder
rule. The working plan file served its purpose and goes; git remembers it.
| -rw-r--r-- | DESIGN.md | 37 | ||||
| -rw-r--r-- | PLAN-FLOATING-PANES.md | 254 | ||||
| -rw-r--r-- | docs/design.md | 4 |
3 files changed, 37 insertions, 258 deletions
@@ -164,6 +164,7 @@ unreadable. | **D19** | **The finder and the command palette are one picker with a subject.** They are the same object — a modal, a query, a filtered list, one selection, the same keys and the same mouse rules — differing only in what fills the rows and what Enter does with one. Building the second as its own machine would have meant two of everything, and the pair would have drifted the way the pane title editor and a tab label editor would have if they had not been made one thing with a subject. So there is one `picker` state (`PICK_FINDER`/`PICK_PALETTE`), one draw, one key handler, and a `pick_row_t` of three columns that both fill: a dim left (a tab, a group), the thing itself, and a dim right (a purpose, the chord). Choosing goes through the row's own action string, so the keyboard and the mouse cannot take different paths to the same row. **The palette lists actions that are not bound to anything**, which the cheatsheet by design cannot — it is built from the binds, so an action with no key does not appear, and that is exactly the action you have no other way to reach. Where a binding does exist the palette shows it, so using the palette teaches the key that would have skipped it; where several exist it shows them all, because a config's binds add to the defaults rather than replacing them and naming only the first would hide the one the user chose. Two actions are left out: `literal-prefix`, which means "type this key" rather than "do this thing" and would send the prefix to a pane hidden behind the list, and `select-tab-N`, which is positional — a row saying "go to that tab" cannot say which. Running is by `action_t`, not by synthesising the keystroke that would have caused it: a palette that had to invent input to do its job would be lying to the rest of the program about what happened. | | **D20** | **Chrome is a rect too, so it is a pass too.** `where="chrome"` on any entry in `shaders` or in a `states` block runs that pass over a pane's **frame** — border, padding, title, buttons — instead of its contents. Nothing else changes: same registry, same `amount`, same expressions (D16), same chain order, one new word. It is one pass over the frame's *whole* rect with the contents punched out as a hole, not four passes over four sides: an effect has to be able to travel round a border, and four passes would each count `x`/`y` from zero and restart at every corner. The hole is what keeps the two honest — a skipped cell is never visited, so a full-strength chrome tint cannot reach your text and a full-strength content tint cannot reach the border. The chains are stored separately rather than as a flag on each shader, because a pass *is* a chain and a rect, and because `shader_abi.h` has no business knowing where a session chose to run an effect. This is also the only kind of pass that can colour a pane a small window has collapsed to a single row (D6), which is the state where a colour is worth the most. **Animation needs a clock**, so `app_next_deadline_ms` reports one while a chain that reads `t` ran over the last frame — derived from what was applied, like everything else about a frame, so a pulse hung off `dead` costs `anim_ms` while a pane is dead and nothing for the rest of the session. Without it a border froze the moment you stopped typing, which is the bug that makes an animated shader look broken rather than absent. A **`bell` pane state** and a **`since`** variable came with it, because "flash the border when a pane rings" needed both and neither existed: a bell had no state to hang a chain off, and `t` says what time it is rather than how long ago something happened, so a one-shot effect was not expressible at all. `since` is milliseconds since the pane entered its current state, stamped in the one function that decides the state — a transition is the one thing a frame cannot tell you, so it is remembered, but it is remembered where it cannot disagree with the state it dates. `bell` ranks above the ambient states and below `dead`: a pane that rang while you were elsewhere is news, and a pane that rang and then exited is a pane that exited. It ends when you look at the pane, because that is what answering a bell is (the frame clock stops with it). **Shipped on by default**: the compiled-in `bell` state is two tints in theme's `bell` colour whose amounts read `since` — on the frame, three full-strength blinks over the first 900ms and then a slow breathe; on the body, a quiet shimmer — one soft diagonal sheen gliding across, peak strength 95 against the blinks' 255, its colour softened halfway towards `default_fg` (light, not paint), its width breathing on a sine as it travels, gone within those same 900ms — so what you are reading is only touched, gently, while the announcement lasts. Derived after the theme block the way the scrolled wash is, so a theme moves the mark, the flash and the shimmer together. The original objection was that an animated default would spend a frame clock on every session, and the clock's own derivation is what answers it: `since` carries `EXPR_DEP_TIME`, so the `anim_ms` clock runs only while a rung, unanswered pane is actually on screen — an idle session pays nothing. It follows `bell_indicator false` off (that knob's promise is silent *and* invisible), and `states { bell { … } }` — including an empty one — replaces it like any state, tracked across includes like `scrolled`. **`channel="fg"|"bg"|"both"`** came from the first border flash actually being looked at: `tint` mixes both of a cell's colours, and a frame's background is the terminal's own default, so the pass materialised it to our *guess* at that default and mixed towards the colour — a dark rectangle behind the glyphs, left there until the pane changed state. The mask is enforced by the pass, not by the shader: the cell's other colour is snapshotted before materialising and put back after, so it works for a built-in and a plugin alike, an unset colour goes back to *unset* rather than to our guess, and no shader grows an fg-only variant of itself. It is one byte tested once per pass, not per cell, and 0 means both so every shader built before it behaves as it did. | | **D21** | **A project is a directory; a workspace is the tab it occupies.** Two words because they are two things, and conflating them is how a session grows a second source of truth about what it is looking at. The directory is found rather than named: a `project_roots "~/dev" "~/work" depth=2` is scanned for subdirectories holding a `slosh.layout` — or a `.git`, which is the project that has not said what it needs yet, and without which `~/dev` with forty checkouts and three layout files is a picker with three rows. A directory that *is* a project is never descended into, so the walk stays off `node_modules` without anyone writing a rule about `node_modules`. **Identity is the tab's `purpose`**, `project:<name>.<hash8>` of the resolved path — the shape D3 already published — so there is no second field to disagree with it, membership is a string comparison, and a dumped session restores it for free because a dump already writes tab purposes and applying one already locks them (D8). Hashed on the *path* rather than the name, so two worktrees of one repo are two workspaces, which is what they are. A tab whose layout gave it a purpose of its own keeps it and is simply not a member: overwriting a declared purpose is the one thing D8 forbids, and the reply counts the ones it left alone rather than letting that be a surprise later. **Nothing watches the filesystem.** The list is derived when it is asked for — one `readdir` per root and two `stat`s per entry, forty checkouts for eighty-one syscalls next to the keystroke that asked — and an answer nobody remembered cannot be stale, which is a promise no watcher can make on a bind mount. The picker scans once as it opens, not per frame, because `draw_picker` asks for its rows every frame and a `readdir` per repaint is a filesystem walk at 120Hz. The one thing a scan cannot see is a project's layout file changing under a workspace that is already open, and that is deliberately not answered by re-applying it over running processes: `workspaces` reports the file's `mtime`, and drift is derivable outside without this session storing a byte. **Opening is idempotent** (`sl0ppi up`'s property, D3): the same request twice is one workspace, focused, and the answer says which branch it took so a caller never has to ask first. **Saving is the inverse, and it is the whole of onboarding.** `save-workspace` is `dump-layout` for one tab, relative to the project, written beside it — the same dumper, so there is one answer to "what does this tab look like written down". Four things made that possible and none of them were true before: a relative `cwd=` in a layout file now resolves against that file's directory (the rule `include` already followed, applied to the other half of the same syntax), a dump can be asked for one tab against a base, a pane from a split now starts where the pane it came out of stands rather than where the server was launched — which had been quietly writing an absolute path to somebody else's directory into every saved project — and **a dump now records what a pane is actually running**. That last one is what turns arranging a project and writing it down into one act instead of two: `label` only ever knew what a *layout* said, so a pane you split and typed `npm run dev` into had nothing to say for itself and came back as a bare shell. The kernel knows what the label does not — a pty has a foreground process group, and that group is by definition the job that owns the terminal — so the answer is read rather than guessed, the way `live_cwd` already reads `/proc/PID/cwd` rather than trusting where a pane was started. Three things are deliberately not commands: a shell at a prompt (its group is the pane's own, and `command="zsh"` on every idle pane restores a session where each shell runs inside a shell), a background job (it does not own the terminal, and resurrecting `&` jobs in the foreground describes a session nobody had), and an ephemeral pane (restoring one reopens a file somebody finished with). A declared label still outranks a live process, because it survives the program exiting (D14) and re-saving must not degrade `npm run dev` into whatever the process tree looks like this minute. argv is joined with shell quoting, since `command=` is run through `/bin/sh -c` and an argument with a space in it would otherwise come back as two. A saved project defaults to `suspend="commands"`: the pane running this morning's dev server is written asleep, because a *project's* layout is a statement of shape and a *session's* dump is a statement of fact, and starting a dev server on every open is exactly what `suspended` exists to prevent — which is worth much more now that there are commands to suspend. What is **not** built: no guessing commands from `package.json` or `Cargo.toml` (a table of guesses that is wrong on day one and stale by year two — recording what you actually ran makes it unnecessary), no watcher, and no auto-save on close, because a layout file is a commit-sized decision. | +| **D22** | **A pane can float**: lifted out of the layout, drawn over the tiled panes and under the modals, freely moved and shaped. The model is a flag, a wanted rect and a raise stamp on the leaf — exactly the `minimized` pattern, not a second collection: the seat in the tree is kept, `layout()` skips it, the siblings absorb its share, and un-floating returns it home, so everything that walks leaves (the finder, `close_leaf`, the dump, the D6 flatten) works with zero new cases. **The drawn rect is derived, the wanted rect is remembered**: what is painted is the intent clamped into the tab every frame and the clamp never writes back, so a terminal squeeze round-trips exactly — the same lesson the weights taught — while an explicit move or resize *does* write the clamped answer back, because the hand is the author of the intent (and is refused while the tab is flattened or zoomed, where the shown rect is not a place to return to). A resize pins the **grabbed edge** at walls and floors and never moves the opposite one — the general clamp preserves size by sliding the rect, which turns "drag the right edge past the wall" into the window walking left — and which edges a grab holds is derived from where the press landed (`float_edges_at`), shared by the press, the hover hint and the guide, so a bottom corner is two edges with no corner target to register and the promise cannot disagree with the drag. **Floats paint after everything tiled and before the modals**, so the backwards-searched hit list makes the z-order and the click order the same list — the one-geometry commitment paying out as "no routing code to write" — and before the status line, which reads that list for its hover hint and must not hint a float's border as the pane underneath. Focusing a float raises it, stamped in `layout()` once rather than at each place focus can move. **Floating pops the pane to the centre**: the jump is what says the float happened — lifted in place it looks like a pane whose neighbours flinched — so position is the announcement and only the size is remembered. The shadow is what tells a float from a tile (the frame is the same frame): `float_shadow`, a number and not a state chain, for the `dim_unfocused` reason — turning it off must not require knowing the states block exists. A `floating` state joined the D13 table **above `unfocused`**, shipped empty: a float is never dimmed as merely-unfocused, because full strength is what keeps the thing on top reading as lifted (the `dragging` argument). Guards, all of one invariant — **an overlay needs a backdrop**: the last tiled pane cannot float or be minimised away, and closing it lands the top float (`ensure_tiled`, checked once per frame in `layout()` like the minimised-focus rule). A float's splits are refused through `split_fits`, which reaches the guide, the border click and the keyboard in one place; it is no swap target, because its tree seat is not where it is. The keys: `f` floats (what "float" sounds like; the finder moved to `s`, where it reads as *search*), `F` opens a **new floating shell** — the throwaway terminal, a real leaf beside the focused pane so un-floating lands it and D14's shell rule closes it — `H J K L` move a focused float, the `=`/`+` key grows it about its own centre and `-` shrinks it (its split was a refusal anyway); equalize moved to `0` because a legacy terminal reports `+` as a bare `=`, and two verbs on one key survive only until the shift is dropped. **Still owed (M10c)**: `floating`/`x y w h` in layout files and `dump-layout` — today a dumped session quietly re-tiles its floats, which is the one data-loss gap — placement arguments on the `float` command, and the kitty-graphics occlusion rule (placements emit after the cell diff and would paint an image over a float covering it: crop what one clean edge can express, suppress what it cannot, and give a float's own images a `z=` above the tiled ones). | ### D6 — responsive layout as a pure function @@ -275,6 +276,14 @@ Frame pacing: pty reads are coalesced and painted on a timerfd at a cap *running* rather than only what a layout told it to run, `save-workspace` to write one out, and a purpose you can set from the keyboard — the last two being what made a hand-arranged tab worth saving at all ✅ +- **M10** — floating panes (D22): the model and the guards, then the hands — + drag to move, edge-pinned resize, keyboard moves, the shadow, the hover + guide — plus `new-float` and the key decisions ✅ (**M10c pending**: floats + in layout files and `dump-layout`, placement over the API, and the + kitty-graphics occlusion rule) +- **M11** — the drop grammar: while a pane is carried, the pane under the + pointer subdivides into a swap centre and four insert bands — dragging + became re-layout (see the drag machine below) ✅ ### The border is the button @@ -377,7 +386,7 @@ layout from before it, until the mouse moved and corrected itself. Same lesson as the hit list, one level up: do not remember what you can derive from the frame you are drawing. -### One drag machine, four verbs (M8) +### One drag machine (M8, grown through M10–11) Pane sizes are **weights**, so an even split is simply equal weights and resizing is not a special case of anything: the layout pass stays the same @@ -417,10 +426,11 @@ what is on screen: | target | painted by | drag does | |---|---|---| -| a frame's top row | `draw_frame` | swap this pane with the one you drop it on | +| a frame's top row | `draw_frame` | carry the pane: where it lands decides (the drop grammar, below) | | the gap between two children | `draw_node`, from the rects they were just given | move that boundary | | a tab in the strip | `draw_tab_strip` | move it to where you drop it | | ...and a *pane* dropped on a tab | `draw_tab_strip` again | move the pane into that tab; onto the `+`, into one of its own | +| a floating pane's title / border | `draw_frame` | move / resize the float, its own verbs — everything a title or border drag means on release is what a float must not do (D22) | The fourth is the third one's target read the other way round, which is why it cost almost nothing: the strip was already a row of drop targets and a pane in your hand @@ -439,6 +449,29 @@ cancel: it is simply not a place to put a tab, so it stays where it last was. `cur` is a *position*, so a move has to recompute it; carrying it across would leave you looking at whichever tab slid into the index you used to be at. +**The drop grammar (M11).** A carried pane's drop used to mean one thing — +swap — so the whole target was one answer. Now the pane under the pointer +subdivides: a **centre**, which is the swap it always was, and four **edge +bands** meaning *insert me beside this pane, on this side* — `detach_leaf` + +`place_beside`, the surgery cross-tab moves already had, aimed within the +tab. Bands are a third of each dimension (they began as quarters, and the +first map of the result showed the swap owning half of each axis — a target +that big reads as dead space mid-drag); the hovered band **fills the half of +the pane the drop would hand over**, where the pane will *be* rather than +where the button is; and zones materialise only under the pointer, because +four bands on every candidate is noise. Two rules are load-bearing. **The +release derives from the painted frame's hits**, not from a side remembered +off the last motion event: zones are painted a frame behind the pointer, and +a remembered answer could disagree with the fill on screen — the same +"derive from what the user saw" rule as everything else. And **a drop beside +a sibling along the axis is always offered**: the dragged pane leaves the +split it rejoins, so the child count never changes and there is nothing to +fit — `split_fits`' arithmetic, right for a border click, refused the most +ordinary drag there is, rearranging a row. Everything else answers to the +same floor a border click does, conservatively; a side there is no room for +simply never registers, so the preview cannot promise what the drop cannot +deliver. Floats subdivide for nobody. + The drop target is highlighted while dragging, and **any keystroke ends a drag** — a release can go missing (the pointer leaves the terminal, the client detaches mid-drag) and a mouse wedged in an invisible state is the worst diff --git a/PLAN-FLOATING-PANES.md b/PLAN-FLOATING-PANES.md deleted file mode 100644 index f3fdaeb..0000000 --- a/PLAN-FLOATING-PANES.md +++ /dev/null @@ -1,254 +0,0 @@ -# Floating panes — plan - -Status: **M10a and M10b built and green** (56 checks in -`tests/test_float.py`, full suite passing). When agreed, the decision below -becomes D22 and the milestone becomes M10 in DESIGN.md. - -A floating pane renders on top of the tiled panes — below the modals, the -toasts and the splash — and can be freely moved and resized. The claim this -plan defends: **almost all of it already exists**, because the two structural -commitments (one geometry; layout as a pure function) were designed for -exactly this shape of feature. The hit list *is* a z-order, and intent-plus- -derivation *is* how a remembered rect survives a resize. - -## The model: a flag and a rect, like `minimized` - -A floating pane is **a leaf still in the tree**, carrying two new pieces of -intent: - -```c -bool floating; /* out of the layout, drawn on top */ -rect_t float_rect; /* where it wants to be, in cells; intent, never clamped - in place */ -uint32_t raised; /* monotonic stamp; highest paints last (= on top) */ -``` - -Not a separate per-tab list. `minimized` already proved the pattern: the leaf -keeps its seat in the tree, `layout_node` skips it, the siblings absorb its -share, and un-floating returns it home — same id, same place, nothing that -referred to it has to be told anything (the D14 property). Everything that -walks leaves — `pane_by_id`, the finder, `close_leaf`, `dump-layout`, the D6 -flatten — keeps working with zero new cases, because there is no second -collection to forget to walk. - -Rules that fall out of reusing the pattern: - -- **The last tiled pane cannot float**, same guard and same reason as - minimise: a tab that is only an overlay is an overlay over nothing. -- **Floating and minimized compose.** A minimized float sits in the strip like - anyone; restoring it restores it floating. Two orthogonal flags, no matrix. -- **Closing a float is `close_leaf`**, unchanged. Its tree seat collapses like - any leaf's. - -### The drawn rect is derived, the wanted rect is remembered - -`float_rect` is what you asked for. What is painted each frame is -`clamp(float_rect, tab_area)` — inside the tab's content area (below the tab -strip, above the status line; those rows own global verbs and nothing may -cover them), floored at `MIN_PANE_COLS/ROWS` plus the frame. The clamp **never -writes back**: shrink the terminal and the float is squeezed in; grow it again -and the float is exactly where you put it. Same rule as the layout tree's -rects — recomputed every frame, never trusted between them. - -Floating an existing tiled pane seeds `float_rect` from its current rect inset -by one cell per side, so the transition reads as the pane lifting off the page -rather than teleporting. - -### Z-order is the raise stamp, and the hit list makes it honest - -Floats paint in ascending `raised`; focusing a float stamps it. That is the -whole z-machine. Because `hit_test` searches the hit list **backwards**, the -float painted last owns the topmost hits automatically — a click lands on -what you see, with no routing code written. The modals still paint after the -floats, so a picker over a float behaves today's way for free. This is the -"one geometry" commitment paying out: the z-order cannot disagree with the -click order because they are the same list. - -Draw order in `app_compose` becomes: - -``` -tab strip → tree → corners → min bar → status line -→ floats, in raise order ← new -→ scrim → picker/help → toasts → splash -``` - -## Input - -**Mouse.** All from the hit list, like the existing four drag verbs: - -| target on a floating frame | drag does | -|---|---| -| the title row | **move** the float (`DRAG_FLOAT_MOVE`) — not swap | -| any border cell / corner | **resize** from that side (`DRAG_FLOAT_RESIZE`) | - -A press that never moves is a click (the distinction the drag machine already -makes): click anywhere on a float focuses and raises it. The ▬ □ ✕ buttons -keep their verbs. A float's frame registers `floatmove:`/`floatresize:` -actions instead of `border:`/`edge:`, so the split guide never arms on it — -derived from the hit list, so it cannot half-arm. **Splitting a float is -refused with a word**: a split is a statement about the tree's arrangement, -and a float is precisely not arranged. Any keystroke ends a drag, as today. - -**Keyboard.** One toggle action, `float`, reachable from the palette on day -one (D19 gives that for free). For a focused float, the existing resize -family gets a second meaning the same way it already means "move the -boundary of whatever is focused": `H J K L` **moves** the float by a step; -resize gets its own chord (open question below). `h j k l` stays focus -movement — floats participate in `focus_dir` geometrically, by their drawn -rects, and in `focus_next` by tree order, both for free. - -## Interactions with what exists - -- **Zoom**: unchanged. A zoomed pane fills the tab whether it floats or not - (zoom reuses the solo path); unzooming a float returns it to `float_rect`. -- **D6 flatten**: a flattened tab lists floats with everyone else — they are - leaves in the tree, so the flatten already includes them. A screen too - small for tiles is too small for overlays; `float_rect` survives untouched - for when room returns. -- **Shader states (D13/D20)**: a new `floating` pane state in the table, so a - float can be told apart by colour. Ranked ambient, below `bell`. Ships no - default tint (a float is live; nothing needs explaining) — the *shadow* - does that job instead: one cell of dimming at (+1,+1) outside the frame, - painted by the float pass over cells already composed. Derived every frame, - knob `float_shadow`, default on. -- **Kitty graphics (D17)** — the one genuinely new cost. The cell diff gets - occlusion right by paint order, but placements are emitted *after* the - diff and would paint an image from a tiled pane over a float covering it. - v1 rule: a placement clipped by a float from one clean side is **cropped** - (the source-rectangle machinery from D17, pointed at a second clipper); - a float landing in the *middle* of an image is a shape one placement - cannot express, so that placement is **suppressed for the frame** and - returns when the float moves. A float's own images already work — cropping - at the pane edge is the same code — and are emitted with a kitty `z=` above - the tiled panes' placements, matching the cells. -- **Connections/clients, D10**: untouched; this is all server-side compose. - -## Persistence and the control API - -- **Layout files (D2)**: pane nodes take `floating=true x= y= w= h=`, both - loaders, both schemas linted the usual way. `dump-layout` writes it, so - `save-workspace` (D21) records a float and a restored session floats it. -- **Control API (D3)**: `{"cmd":"float","id":N}` toggles; - `{"cmd":"float","id":N,"x":..,"y":..,"w":..,"h":..}` places. `panes` - reports `floating` and the rect. Bare-verb alias runs the same code. - -## Tests (headless, per the harness) - -- float a pane → snapshot: its cells overwrite the tiled cells under it; - hit list resolves the overlap to the float. -- two floats → focus the lower one → it is on top next frame (raise = focus). -- drag-move and drag-resize via `send` of SGR mouse; a keystroke mid-drag - ends it. -- shrink the terminal under a float → clamped; grow it back → **exact** - original rect (the fork's collapse/expand lesson, asserted for floats). -- flatten a tab containing a float → the float is a row, reachable. -- dump → apply → the float is back, floating, placed. -- refuse: floating the last tiled pane; splitting a float. - -## Milestone M10, staged - -1. **M10a — the model.** ✅ Flag, rect, stamp; layout skips it; draw pass after - the status line; clamp; focus/raise; `float` action + palette; the guard - rules; tests above minus the drags. Three deltas found by building it: - - A bare `{"cmd":"float","id":N}` toggle shipped early (the harness is a - client; placement arguments stay M10c). - - The backdrop invariant needs a landing rule, not just a guard: closing - or moving away the last tiled pane un-floats the top float - (`ensure_tiled`, checked once in `layout()` like the minimised-focus - rule). The minimise guard tightened to match: another float is not - something to show. - - A float refuses splits through `split_fits` returning false, which - reaches the guide, the border click and the keyboard floor check in one - place; the keyboard, the border release and the API each add the words - ("a floating pane cannot be split"). -2. **M10b — the hands.** ✅ `DRAG_FLOAT_MOVE`/`DRAG_FLOAT_RESIZE`, keyboard - move, the shadow, the `floating` shader state. What building it decided: - - **A grabbed edge pins at the wall.** The general intent clamp preserves - the size by sliding the rect, which turns "drag the right edge past the - wall" into the whole window walking left — so `float_resize` clamps each - grabbed edge against the tab area itself and never moves the opposite - one. Which edges are held is derived from where the press landed on the - rect painted that frame, so a bottom corner is two edges for free. - - **Explicit moves write intent back.** A drag or keyboard nudge applies - its delta to the rect *as drawn* and, after the layout clamps, writes - the clamped answer back into `float_rect`: the hand is the author of the - intent, so "where you see it is where it stays". A terminal resize still - never writes back. Refused while the float is not drawn as a float - (flattened or zoomed), where writing the shown rect back would destroy - the place it returns to. - - **The float verbs are their own drag kinds**, not flags on - `DRAG_TITLE`/`DRAG_BORDER`: everything those mean on release — swap, - split — is exactly what a float must not do. The rename double-click on - a float's name still wins, and a float is excluded as a swap *target* - too (its tree seat is not where it is). - - **`floating` ranks above `unfocused`**, so a float is never dimmed by - `dim_unfocused`: full strength is what keeps the thing on top reading - as lifted — the `dragging` argument. It ships no chain; the shadow does - the telling apart. - - **The shadow is a number, not a state** (`float_shadow`, default 110, - 0 for off) — the `dim_unfocused` argument about discoverability. Cast - as the scrim's own dim pass over two non-overlapping strips - (`gap_aspect` columns beside, one row below, offset so the light comes - from the top left), before the float paints, so it falls on lower - floats too. - - `H J K L` on a focused float move it one gap-aspect-square step; - keyboard *resize* of a float remains open (question 2). A float's title - also does not target the tab strip yet — moving between tabs stays - `pane-to-next-tab`'s job for now. - - **The border says it is a handle** (added after a hand went looking): - hovering lights the edge a grab would move in the resize colour, with - the `⇔`/`⇕` arrow at the grab point; a corner lights both edges; the - held edges stay lit through the drag; the status hint says "drag to - resize (both ways)" and a float's title handle stops promising the - split it does not offer. All read from one derivation - (`float_edges_at`) shared with the press, so the promise, the paint - and the drag cannot disagree. Finding this also found an ordering bug: - the status line — which reads the hit list for its hover hint — was - painted *before* the floats, so a float's border hinted as the pane - underneath. Floats now paint before the status line (their rects - cannot overlap; the clamp sees to it), which also keeps a float's - shadow off the status row on a gap-less config. -3. **M10c — the memory.** Layout schema, dump, control API, the graphics - occlusion rule. - -## Open questions - -1. ~~**The default key.**~~ **Resolved: `C-a f` floats.** Both float and the - finder wanted the letter; the tie went to the verb — f is what "float" - sounds like, and a float toggle is pressed in a flow (lift, look, put - back) where a reach hurts. The finder moved to `C-a s`, where it reads - just as naturally as *search*. -2. ~~**Keyboard resize chord**~~ **Resolved: the =/+ key grows, `-` - shrinks**, about the float's own centre so it stays under your eyes. - Grow is bound with and without shift because a legacy terminal reports - `+` as a bare `=` (the `?`/`/` deal), which is also why **equalize moved - to `0`** rather than sharing the key: two verbs on one key survive only - until a terminal drops the shift — and `0` reads as "reset the shares" - on the digit row the tabs already own. Shrink rides the split-rows case, - where a float's split was a refusal anyway. -3. ~~**Docking**~~ **Resolved: unfloat is the dock.** The float kept its - seat all along, so `C-a f` on a focused float already lands it — no drop - gesture needed. -4. ~~**Creating panes floating**~~ **Resolved: `C-a F` / `new-float`** — the - throwaway terminal. A real leaf beside the focused pane (so un-floating - lands it there and `exit` closes it under D14's shell rule), born - floating at the centred default size, in the focused pane's directory. - `f`/`F` is a shifted pair: float this pane / float a fresh one. (It - spent a day on F12, the IDE key for the same idea, and came back — a - leader-and-letter chord is what every other verb costs, and the function - row is a reach the home row is not. The detour still paid: `f1`..`f12` - joined NAMED_KEYS, so the function row is bindable by any config — the - decoder always understood it, the config just had no names for it.) The - layout-file case stays M10c. -5. **Graphics crop vs suppress** — start with the simple rule and let a real - program complain. - -## A later decision: the pop to the centre - -Floating a pane now **always centres it** (M10b originally lifted it in -place, inset a cell). The jump is what says the float *happened* — a pane -lifted in place looks like a pane whose neighbours flinched. The size is -still the one remembered from the last time a hand shaped it (or seeded -from the pane's tiled rect); only the position is opinionated, because the -position is the announcement. This traded away "refloat returns to where -you parked it" deliberately: position is re-announced, size is remembered. diff --git a/docs/design.md b/docs/design.md index af5b139..0236212 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,7 +1,7 @@ # How it works -Two decisions do most of the work. The rest — twenty numbered decisions, each -with what it cost to get wrong — is in +Two decisions do most of the work. The rest — twenty-two numbered decisions, +each with what it cost to get wrong — is in `DESIGN.md`. ## One geometry |
