Root cause of the post-tab lag: the GPUI 16 ms timer was calling
`WebSurfaceRuntime::ensure_tab` and `tick` on the UI thread, and each
call did a synchronous `serde_json` write plus `read_line` against the
Servo sidecar over stdin/stdout. With even one visible tab, every
frame stalled on cross-process IPC.
Introduce `web_surface_worker.rs` — a per-profile worker thread that
owns the `ServoLiveClient`, drains a coalescing request queue
(latest Ensure/Poll per tab wins, no unbounded growth), and ships
results back through a `std::sync::mpsc` channel. `WebSurfaceRuntime`
now submits work non-blockingly and drains responses in `tick`; the
UI thread never blocks on the sidecar.
Adjacent in-flight cleanup riding along: hardware IOSurface
rendering-context completion (sidecar `live_protocol`,
`hardware_rendering_context`, GPUI BGRA surface shader), CSS viewport
size + device pixel ratio plumbing into `ServoLiveFrame`, and the
Send opt-ins for `CVPixelBuffer`-bearing types so frames can cross
the thread boundary.
A GPUI harness boots a real ElyShell, navigates to an external URL, and
asks the input_overlay's sibling canvas tracker where it laid out. On
main before this change the canvas reports
Bounds { origin: (309, window_height - 17), size: (W - 326, content_h) }
i.e. the overlay's top edge sits at the very bottom of the visible
window. Every user click in the visible area lands above (or beside)
the overlay; the on_mouse_down + capture_any_mouse_up listeners never
even see the event because the hitbox is off-screen. Twelve commits
chased focus/coords/outcome enums on the sidecar side while every click
in the live shell hit empty space.
Root cause: in render_web_surface the rendered web image (img / loading
div / error page) was a non-absolute child of a `.relative().size_full()`
wrapper. The non-absolute child claims `size_full` block-flow height
inside that wrapper, which made the wrapper's intrinsic height
content_height + content_height. The two `.absolute().size_full()`
siblings (viewport_tracker, input_overlay) then sized against that
inflated parent and were positioned in the bottom half — exactly
content_height below where they were supposed to be.
Fix: keep the relative wrapper as the layout owner of the panel slot
(size_full, overflow_hidden, min_w_0) and put the rendered image into
an absolute `inset_0` child of its own. viewport_tracker and
input_overlay stay as absolute siblings. With the image out of in-flow
the wrapper sizes to its parent and the overlay's hitbox lands at
y = top of content area (71 in a 1080-tall window) instead of
y = window_height - 17.
GPUI test harness (`gpui_harness_tests.rs`) is the holdout set:
- `baseline_overlay_div_receives_simulated_click` proves GPUI's
occlude + capture_any_mouse_up primitive works under TestAppContext.
- `baseline_overlay_with_full_listener_combo_receives_click` proves
the exact listener combo render_input_overlay uses works in
isolation.
- `ely_shell_external_canvas_lays_out_inside_window` boots a real
ElyShell, navigates, and asserts the overlay's measured bounds fit
inside the visible window. Without the fix above, this test trips
on bounds extending below the window bottom.
The three new store-layer tests in web_surface_tests.rs pin per-tab
isolation, zero-delta short-circuit, and resize-mid-drain decoupling
invariants the harness work flushed out.
ely_app picks up gpui's test-support feature as a dev-dependency so the
harness can use VisualTestContext + simulate_mouse_*.
cargo test --bin ely_app: 112 passed (was 108 + 4 new harness/store tests).
Remaining work (not in this commit): even with the layout fixed, the
harness shows MouseUp's capture_any_mouse_up still doesn't fire on the
ElyShell tree, while MouseDown's bubble does. Some sibling/ancestor
listener in the live shell is eating the MouseUp capture phase that
the standalone listener-combo baseline does not. Tracked separately.
User reports the sidebar can't be resized. Until now sidebar width
was only mutable through the COLLAPSED/DEFAULT toggle and through
typed settings. Real browsers (Arc, Dia, Zen) all let you drag the
right edge of the sidebar to size it live.
Wire it through the existing space.sidebar_width_px:
* `ElyShell.sidebar_resize_origin: Option<(f32, u16)>` records the
cursor-x and width-px at the moment the handle is grabbed.
* `begin_sidebar_resize` / `end_sidebar_resize` set and clear it.
* Window-level `on_mouse_move` consults the origin first; while held,
it forwards the delta to `set_active_sidebar_width` clamped to
220–480 px so you can't accidentally annihilate either pane.
* Window-level `on_mouse_up` releases the drag.
* The handle itself is a 6 px transparent strip pinned to the right
edge of the expanded sidebar, `cursor_col_resize`, with a soft white
hover. mouse-down captures the origin; the rest is window events.
cargo test --workspace: 440 passed, 0 failed.
The design's TopBar.url placeholder reads `Search ELY or type a
command…`. We were shipping `Search or enter address`, the generic
URL-bar string from when the omnibar didn't yet host commands.
Match the design copy so the empty-state on `ely://new-tab` matches
home.jsx and command.jsx exactly.
The footer chips already advertised ↑↓ to navigate and ↵ to open;
they now match reality.
- chrome::command_match exposes COMMAND_ACTIONS / matching_actions
alongside the tab/history/bookmark match helpers and a
CommandSelection enum + visible_command_rows that returns the flat
ordered list of activatable rows. command_overlay drops its private
copies of the action const and matcher and consumes them from the
shared module so render and key handling share one source of truth.
- ElyShell tracks command_selected_index with command_select_next /
command_select_prev (cyclic, no notify when index doesn't change)
and activate_selected_command which dispatches the right shell call
for the currently selected CommandSelection variant and dismisses
the overlay. Dismissing command mode resets the index to 0.
- The shell root captures key_down via on_command_overlay_key_down.
When the live snapshot's command_query starts with '>', up / down
/ enter run the matching helper and the event stops propagating so
the omnibar input doesn't move its caret.
- Each rendered row receives a `selected` flag. The selected row gets
the design's tinted bg + accent-bar on the left edge so the active
result is unambiguous at any keyboard step.
Domain ships HIDDEN_SIDEBAR_WIDTH_PX = 8 alongside the existing
collapsed/default tiers. ElyShell tracks sidebar_hover_expanded with
expand_hidden_sidebar / collapse_hidden_sidebar helpers; switching
back to a non-hidden width via the layout cards or core API resets
the flag automatically so the sidebar can never be both hidden and
expanded after a mode change.
Renderer:
- render_sidebar takes a sidebar_hidden flag and routes to a thin
8 px clickable rail (hover bg + click expands) when the active
space's width is at HIDDEN.
- While the rail is expanded, render_browser overlays a transparent
backdrop + the full default-width sidebar absolutely positioned in
the shell inset, so the main pane content never reflows.
- collapsed_sidebar_active still drives the COLLAPSED-tier compact
sidebar; the hidden tier is opted out of that path.
Appearance form:
- Layout cards section gains the design's third "Hidden on hover"
card with a 6 px sliver preview that mutates the active space to
HIDDEN_SIDEBAR_WIDTH_PX. LayoutMode now derives id/width/preview
from a small enum so adding a fourth mode would be one match arm.
The hover-expanded state never persists into the domain; once the
user switches modes or clicks the backdrop, it collapses cleanly.
Clicking the picker pill now opens an in-flow space-list disclosure
beneath the workspace row instead of cycling to the next space. The
list shows every space's emoji glyph + name, highlights the active one
with the accent check, and on row click switches to that space and
closes the disclosure.
Sidebar disclosure rules:
- The chevron flips ChevronDown → ChevronUp when open so the affordance
reads at a glance.
- A "Manage spaces" footer routes to ely://settings/spaces and closes
the picker.
- ElyShell carries a workspace_picker_open: bool with toggle / close /
select_space_from_picker helpers; render_sidebar_header receives a
&ElyShell so it can read the open flag without leaking shell internals
to free helpers.
The existing SelectNextSpace shortcut still routes through
cycle_to_next_space, so the keyboard cycle behaviour is unchanged.
ElyShell now hosts a translucency_slider: Entity<SliderState> bound to
0..=100 step 1, defaulting to DEFAULT_TRANSLUCENCY_PCT. A subscription
on SliderEvent::Change writes the rounded value into the core via
set_translucency_pct, so dragging the thumb mutates the persisted
appearance setting in real time.
The appearance form swaps the static track-and-thumb visual for the
gpui_component Slider (160 wide) plus a live percentage readout. The
three preset chips move below the row as fast-set buttons that go
through a new set_translucency_pct_from_preset helper which writes
both core and the SliderState so the thumb tracks the chip choice.
reset_appearance now resets the slider to DEFAULT_TRANSLUCENCY_PCT
alongside resetting the core, keeping every UI source of truth in
lock-step.
Replaces the static "Install plugin · N active" hero button with the
design's pair: a glass search input + a primary Install action. The
input is bound to a new InputState on ElyShell (plugin_search_input);
on every render the catalog reads its current value and filters the
plugin grid by case-insensitive contains on name, author, and
description. An empty-needle render shows every installed plugin so
the page still works at rest.
When the search yields no matches, the grid swaps to an empty-state
copy that quotes the user's needle. Install action moves into a
trailing dark chip that calls choose_plugin_package, preserving the
existing file-picker flow.
Designs call for a radial-gradient dawn (cream base + pink upper-right
glow + blue lower-left glow). GPUI 0.2.2 only supports linear gradients,
so the wallpaper is composed of a cream base layer with two diagonal
linear gradients that fade to transparent at ~60% — the perceived effect
matches the design's softness without needing radial-gradient support.
Wallpaper is isolated to chrome::wallpaper so future appearance themes
(violet, mint, slate) extend the enum without touching the shell.