Arachnid issues in EWC - #518
Merged
Merged
Conversation
Contributor
Author
|
Closes #449 |
- Answer Button State / Scroll+Trackbar Thumb WG from the central tree (dataRef) instead of the redundant localStorage branch, so a freshly-created widget can answer a WG during init (State stays client-authoritative). - Add TrackBar (range slider) and ColorButton (colour picker) components and route them in SelectComponent. - Rebuilt dist.
Client half of EWC's new Static: the plain ⎕WC panel that pre-dates
SubForm and Group. A positioned container with BCol/FCol, optional
EdgeStyle border, and mouse events.
Two details that matter for the legacy applications that use it:
- overflow:hidden, because Win32 clips children to the panel. Without
it a column of cards taller than its Static spills over the panels
beside it.
- No caption and no etched frame. Group defaults its EdgeStyle to
'Groove' to restore the frame ⎕WC draws; Static must not, because
native Static defaults to EdgeStyle 'None' and renders flush.
Children are positioned against this element and mouse coordinates are
measured from it, so a click landing on a child Image bubbles up and is
still reported in this element's space — which is what ⎕WC does, and
what applications that hit-test drawn children against ⎕WG'Points'
depend on.
…menubar MenuBarItem. A ⎕WC MenuItem may be parented directly on a MenuBar rather than inside a Menu — that is how legacy code writes a command with no submenu, e.g. Arachnid's "Deal Row". SelectComponent had no MenuItem branch at all, so the object arrived from the APL side and was silently dropped. MenuItems inside a Menu still go through DropDown, so this only catches the bar-level case. Separator renders as a divider in the dropdown, keyed off Type rather than a caption convention. Active is honoured in both places: greyed and non-clickable. Previously a disabled command still looked and behaved live, letting the user invoke something the application had deliberately turned off. Align 'Right' on a Menu pushes it to the end of the bar (auto left margin in the existing flex row) — where ⎕WC applications put Help. The menu bar now draws on the system control colour instead of being transparent. ⎕WC draws it on a plinth, and against a coloured form — Arachnid's table is dark green — transparent was close to unreadable. Also made the Select-event lookup in DropDown case-insensitive, matching what the mouse handlers already do.
Client half of ⎕WC's Locator. Native creates the outline and then MOVES
THE OS CURSOR into it (⎕NQ obj 3 y x) so the user is already holding it.
No browser can move the pointer, so instead the outline starts at the
requested Posn and follows the pointer, centred on it — exactly where
the cursor would have been put. See the port's ADR-005.
Three details that are not obvious:
- Resolve on mouseDOWN, not mouseup. The gesture that CREATES a
Locator is itself a press (a right-click on a card), and its
trailing mouseup arrives first — placing the locator instantly at
the point it started from, so every drag became a no-op.
- Stop rendering once placed. The native locator is a transient rubber
band; leaving it up also leaves a live listener that swallows the
next click.
- Reset when the object is recreated. ⎕WC on an existing name
recreates it, and applications reuse one locator name for every pick
— SELECT_STACK does. React keeps the same component instance, so
without this the SECOND pick renders nothing, still "placed" from
the first.
The reply matches what ⎕DQ returns natively — (name 80 y x h w), the
event then Posn and Size — because callers do arithmetic on it:
POSN←1 0.5+.×2 2⍴2↓POSN takes the centre.
⎕WC Forms carry their window title in Caption, and applications keep
state there: Arachnid's TITLE rewrites it on every deal ("APL Arachnid :
Deal 2 of 5"), which is how the player knows how many deals remain.
Desktop mode shows it in the HTMLRenderer window chrome. A browser has
no chrome, so it had nowhere to go and the user never saw any of it. It
belongs in the tab.
⎕WC Points is (Y X) and either axis may be a SCALAR standing for every
point: ('Points'(0 (x1 x2 x3))) is three points sharing y=0. Mapping over
Points[0] drew only the first, so an Image asked to show a row of bitmaps
rendered exactly one of them.
Picture likewise may be one name per point, a single name shared by all,
or a (name style) pair. All three are handled now.
Together with the APL-side fixes this makes Arachnid's Show Stack window
display its cards.
Two defects that between them made the application unreachable after using a subsidiary window. Creating a Form ran deleteFormAndSiblings over the whole data tree, destroying every form already present. ⎕WC does no such thing: a second window opens over the first and the first is still there — which is exactly what DISPSTACK and HELP rely on. So when the second window closed there was NO window left and the game was unusable. Forms now accumulate, findFormParentID renders the most recent, and the existing EX handler removing one brings the previous back for free. A Locator is only live while something is ⎕DQ-ing it, but the object persists afterwards — SELECT_STACK never expunges it — so "the object exists" is not "the locator is active", and the client could not tell them apart. A leftover locator captured the user's NEXT click anywhere on the form; registered with handler 1 that event returns straight out of ⎕DQ, so it satisfied the application's MAIN ⎕DQ, SETUP ran to completion, and everything stopped responding while still looking fine. Arming is now once per ⎕WC: App.jsx stamps each creation, the component arms for a stamp it has not resolved, and resolved stamps are remembered at module level so a remount cannot revive one. Unstamped means inert — an armed stray is far worse than a dead one. Two wrong turns worth recording. Re-arming on `data` change fails because object identity changes on every message. Re-arming on geometry change fails because the component REMOUNTS when the rendered form changes, which is precisely what closing Show Stack does. And returning null from render does not stop the effect running, so the guard had to go inside the effect too or the listeners were still attached.
On Win32 the menu bar sits in the window's non-client area, above the client
rectangle, so ⎕WC ('Size' (300 600)) gives 300 pixels of usable height
whether or not the form has menus. EWC draws the menu bar inside the form,
so the content area was Size minus the menu: every form with a menu bar was
25px short, the application's own layout arithmetic overflowed, and children
positioned against the bottom fell off.
Visible in Arachnid, which sizes its ten card panels to the full form
height — they hung past the bottom edge onto the page background. That is
what the layout-parity check against the Win32 build caught; it went from
10.56% of cells disagreeing to 0.56%.
hasMenuBar also moves out of the render closure so the sizing effect can
depend on it. Computed inside, it was invisible to the effect, which then
kept a stale size when a menu bar arrived after the form.
The browser raises its own menu on right-button RELEASE. Native ⎕WC has none: button 2 arrives as MouseDown/MouseUp with the button in the message and nothing else happens. On a ported application that uses the right button — picking up cards to drag, say — the menu lands on top of the result. Gated on registration, exactly like handleMouseDown, which already sends nothing unless the object registered MouseDown. An object the application has not claimed the button on keeps the browser's menu: that is the platform's behaviour and EWC has no business overriding it globally. A first attempt did exactly that, with one handler on the App root suppressing the menu for every EWC application everywhere except editable fields. Wired in beside onMouseDown across the 24 components that take mouse events. Leaving it off a component simply means the menu still appears there. The Splitters are deliberately not included: they declare a local handleMouseDown for a drag they implement themselves and register no ⎕WC events, so there is nothing to gate on.
…n release Two defects a player found in a minute that the suite had been green over. Points are ABSOLUTE positions within the parent. setStyle returns position:relative for an object with no Posn — which a multi-point Image never has — and was spread after the per-point placement, so React kept the per-point left and made it an offset from the FLOW position: each image displaced by the accumulated width of the ones before it. A 58px pitch rendered at 112px, and Arachnid's Show Stack, sized by DISPSTACK to fit exactly four cards, showed two and clipped the rest. Fixed by ordering, not by force. setStyle's output is EWC's own DERIVED default and must not beat the Points; customStyles is the CSS the application asked for explicitly and still beats everything, position included. A first attempt put position last and made the application's own CSS unoverridable. Separately: ⎕DQ on a Locator returns when the button comes UP, and DRAGCARDS creates one from a press the user is still holding, so the release places it. The client resolved only on mousedown. That was right for a locator created from a gesture already over — Show Stack's, from a menu item, has no release coming and would otherwise place instantly where it started — and wrong for one created mid-press. Now decided by whether a button is held when the locator arms. Purely additive: mousedown stays registered either way as a backstop, resolve is idempotent, so whichever arrives first wins and a release lost to a drag ending outside the window does not strand the locator. Until the browser context menu was suppressed, the missing release was masked — the second click was supplied by accident when the user dismissed the menu.
EWC showed one form at a time (findFormParentID, most recent) and removed the
rest from the DOM, so a subsidiary window wiped out the main one. In
Browser/Multi mode a form tagged ('Primary' 1) now renders in-flow and every
other top-level form renders as a FloatingForm over it — a window-in-the-page
with a titlebar, an X that sends Close, and drag. A transparent click-shield
under the topmost floater keeps the forms beneath inert (a v0 stand-in for the
real pseudo-modal behaviour, which gates event dispatch on the active ⎕DQ).
The policy is one function, findPrimaryFormID: the tagged form, or — with no
tag — the sole form, because most apps have exactly one and must not be made
to care. Several untagged forms return null and the previous single-form
render stands: no inference (creation order breaks on replace-main and on a
login window), no regression, no new behaviour until an app opts in.
Desktop mode is gated off entirely (Mode.Properties.Desktop): there every
form is a real OS window, as before, and Primary is inert. window.open was
never an option — a blocked popup is worse than a visible bug.
The shield blocks pointer events to the forms beneath a floating window, but keyboard events go to the focused element, not through the shield — so a focused input in a frozen form still took keystrokes. While a floater is up, a capture-phase key listener now swallows key events whose target is not the topmost floater (marked data-floattop) or a MsgBox raised over it. So only the active window — the top of the ⎕DQ stack — is live, for keys as well as clicks. The listener attaches ONCE and reads a live ref rather than attaching and detaching as the floater count changes: a transient re-render briefly reporting zero floaters would otherwise drop it for a tick, and a keystroke in that window would slip through. That was flaky until the ref pattern. This is the browser reimposing "only the top pump is live", which native gets for free because the OS only feeds events to the foreground window. The server does not scope dispatch at all — every event goes to the active ⎕DQ and fires whatever object it names — so the client is where modality lives; the frozen form simply never generates the event. See ADR-007 / FINDINGS B20.
findPrimaryFormID drops the redundant "a lone form is its own primary" clause: a single untagged form already renders through the legacy most-recent path, so the special case earned nothing. The whole rule is now just "the form tagged Primary, or null", and the two render branches collapse to one — render the primary if there is one to float over, else the most-recent form, exactly as before. Single-form apps set nothing and are untouched; there is no inference policy. Lighter, same behaviour.
The Locator attached its window-level mousedown resolve listener in a passive
useEffect, which runs AFTER the browser paints. So between the outline being
painted — looking armed — and its listener actually attaching, there was a
window in which a placement click was silently dropped: the Locator sat there
still armed, and the next click resolved it, usually to no valid stack ("Error
selecting stack"). Fast placement, or extra render work lengthening the gap,
lost the race.
useLayoutEffect for both the reset and the listener-attach (kept in that
order) runs synchronously after the DOM update and BEFORE paint, so by the
time the outline is visible it is already listening. An impatient stress probe
(place-clicks with no wait) went from missing ~1 in 8 to missing none across a
real deal.
This is latent without floating windows but was exposed by them: rendering a
subsidiary form as a floater also mounts the shield and the keyboard-modality
effect, and that extra work widened the gap enough that the race went from
rare to common. FINDINGS B21.
3 tasks
sloorush
added a commit
that referenced
this pull request
Sep 11, 2026
Brings the six commits that landed upstream since the branch was cut: Plugins (#524), Mandelbrot + Arachnid (#518), Extensive tests for Multi (#520), the Rect and pictures test fixes, and visual baselines run #31. Git mapped the moved tree automatically: edits to src/ landed in client/src/, and the eight new components (ColorButton, FloatingForm, BitmapCanvas, Locator, MenuBarItem, Static, TrackBar, pluginHost) were placed there too. Resolutions: package.json upstream's 14 new multitests:*/ewc-multi:* scripts go on the workspace root, next to the demotests:* ones -- they drive Playwright and ci/, both of which live at the root ci/run-server.sh kept upstream's SETUP_APL indirection, defaulting to the in-tree /work/ewc/ci/setup-ewc.apl rather than /scripts dist/ dropped. Upstream still commits it; this branch does not. Git guessed the new bundles belonged in client/public/ (the fonts are identical in both trees) -- they are build output and stay untracked. ci/ewc-multi-start.sh and ci/setup-ewc-multi.apl were adapted to the single mount, matching ewc-demo-start.sh. Note: the Multi suite cannot pass yet. It drives test-apps/multitest, which is still on ewc's unmerged multimode-tests branch with no open PR -- client tests landed without their APL counterpart, which is precisely the split this merge exists to end.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.