Repository navigation
Stop flicker and layout shift when loading tables and switching sidebar tabs - #109
Merged
Merged
Conversation
With the Empty & NULL Markers extension off, a real NULL and the text 'NULL' drew the same four letters, grey against white, and empty and whitespace-only strings both drew blank. Whether to write IS NULL or = 'NULL' depended on reading that tint. Anyone who switched the extension off keeps it off.
Apply raised one "Could not save" per failed edit, then a green "Changes applied". When the same Apply also deleted rows, the failed edits were dropped with nothing said. Now one error names how many edits failed, whether they are still staged or were discarded, and up to three column: error lines in the code block. The success toast reads "Some changes applied" when anything failed. The grid's own "Could not delete" toast is gone, since the delete handler already raises one.
An empty result under a search or filter read "No rows in this table", which says the data is gone, and the overlay had no way out. The grid now gets a `filtered` flag and a Clear filters button that drops the search and every filter in one reload. An unfiltered empty table still reads as before.
Close Tabs to Left/Right asked before throwing away unsaved changes, Close All and Close Others did not. The three now share one confirm, which also resets the visible grid's live edits when its tab is among those closed, as closing a single tab does.
It was red with a trash icon, though every closed tab goes on the reopen stack. Red should mean something can't be undone, so it is now a plain item with the x icon in both the tab menu and the File menu.
Truncation started with measureText on the whole value, so a 2,000-character Devanagari or markdown chunk was shaped end to end to show its first 60 characters, once per cell scrolled into view. It now measures a head of four times the monospace estimate, which covers narrow fallback glyphs and zero-width marks, and falls back to the whole string only when that head fits.
The canvas kept the old table's pixels only for the 220ms grace window, then repainted blank under a spinner that faded in over 300ms. A load just past the window strobed: old table, about 70ms of empty grid, new table. The held frame now stays until the new rows land, dimmed by the overlay, and the fade is gone. A fresh or resized canvas still paints, as before.
Objects needs an icon of its own (next commit), so the registry gains 'shapes': Lucide Shapes, Hugeicons Shapes01 and Phosphor Shapes. 'replace' had only a Lucide glyph, so in the default Hugeicons set the Find & replace tab drew in a different stroke and proportion from the five tabs beside it. It now maps to SearchReplace.
Objects used 'blocks', which draws a puzzle piece in Hugeicons and is the Extensions icon everywhere else, so the two read as the same thing. Objects and its empty states now use shapes.
Left and Right never switched tabs. The line after `sidebarTab = SIDEBAR_TABS[next].id` started with `(`, so automatic semicolon insertion joined the two into a call and every press threw "SIDEBAR_TABS[next].id is not a function". The button is read into a variable first now, then the tab is set, then focus moves.
The accent rail snapped to the new tab while the pill and the icon faded over 150ms, so every switch showed two tabs half lit. The switch is one frame now.
Switching tabs destroyed the panel being left and rebuilt the one being opened: its DOM, its scroll position, and for Objects a fresh read of the catalog. Queries, Objects, Tables, Databases and Recent now mount on first visit and then stay, hidden with display:none. Each list tab has its own scroller, and its position is saved on the way out and put back on return (display:none drops it). Objects stops loading while it is hidden. Returning to Tables went from about 921 DOM mutations to 15. Find & replace still mounts per visit: its preview reads the open table's rows, which a hidden copy would keep recomputing.
The Databases tab counted as empty from the moment it opened until its list arrived, so a first visit read "No databases. Nothing else on this server." for about 200ms, with the loading dots unreachable behind it. A pending or failed load now shows the section's dots or its error instead.
Objects dropped the display options button, and Queries and Find & replace showed table actions that did nothing for their panels, so the button group changed width from tab to tab. A spacer holds the slot on Objects, and the table actions keep their space but are hidden on Queries and Find & replace.
The panel remounts on every visit and kept what was typed in its own state, so going to Tables and back cleared the find and replace fields. The text and the regex, whole-value and case options are bindable now, held by the sidebar.
Opening a table set total to 0, which unmounted the range readout, the page picker and the arrows until the rows landed, then brought them back and moved them again when the count arrived. The page-size select jumped up to 179px each way. Now: - openTableTab seeds total = -1, the "counting" shape, and keeps the last fetch time instead of zeroing it. - The readout sits in a fixed-width box with its text held left, so "…" becoming a number grows into the box. - Outside page size All the page picker is always there, disabled on a single page. Under All it never shows, and while counting the range reads the rows loaded, not "1-1". - The Mod+F hint no longer waits for columns.
The go-to-first and go-to-last column buttons were removed whenever the grid fit, and the fetch time only rendered once a time was known, so both left and came back around every table open and shifted what sat beside them. The buttons are disabled instead of removed, and on table tabs the timing has a fixed slot.
The filter button grew from 28 to 42px to fit the count inline, which moved the five buttons after it 14px every time a filter value was typed or cleared. The count is a badge on the corner now, and the button's accessible name says how many filters are active.
A refresh swapped the table list for loading dots, which reset its scroll, and the reply's unknown counts blanked counts the sidebar already had until the recount waves came back. loadTables now remembers which connection and schema the list on screen belongs to: re-reading that same list stays quiet and keeps known counts, while tables the reply left unknown are still recounted. A new connection or schema shows the dots as before.
The count taken when a table opens never set _countedView, so the first page change or refresh afterwards dropped total back to -1 and the readout flashed "…" through another COUNT(*). The open path records the counted view now, and a recount of the same table under the same predicate keeps the last number until the new one lands.
Changing schema cleared the active table before the new table list loaded, so the main area showed "Select a table" for a moment, then the old toolbar over an empty grid, and the filter bar remounted and pushed the grid down 82px. The old table now stays until the list lands, then the tab moves to the first table in one step under the grid's held frame. The previous table's search, filters and sort are cleared on the way, the tab title follows, and the filter bar stays mounted through a reload.
A switch unmounts the shell before the new connection exists, and the overlay held its ring and label back 350ms, so a fast switch was about a third of a second of plain black window. The hold-back now applies only to the startup reconnect it was added for.
Under page size All, a large tab that fell out of the three-tab row cache lost its columns along with its rows, so coming back was a full cold load: the loading overlay, the pager collapsing and the count blinking. Eviction now drops only the rows and marks the tab, which refetches its rows on return with the header and toolbar already in place, and a refetch keeps the tab's known count until the background count lands.
Contributor
|
I have read the CLA Document and I hereby sign the CLA You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot. |
broisnees
force-pushed
the
fix/smooth-grid-and-sidebar
branch
from
October 29, 2026 07:36
11135bd to
cc0edf6
Compare
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Stroke didn't feel smooth: horizontal scrolling dropped frames on text-heavy tables, and opening tables, switching tabs, changing schema or refreshing flickered and shifted the layout. This branch fixes the flicker, makes long text columns cheaper to draw, and fixes a few data-trust problems I found on the way. It's 24 commits, one change each, so the history reads change by change.
Grid
Data trust
'NULL'.Toolbar, status bar and loading
totalstarts at "counting", the readout has a fixed width, and the page picker is always present outside page size All.Sidebar
Measurements
I measured flicker and layout shift on a production build in headless Chromium (the engine behind WebView2). The backend was stubbed with realistic latency: rows 300-800ms, counts 600ms, the table list 150ms.
Measuring only the head of long cells cut the JS time of a vertical scroll over text columns from 1,217ms to 964ms. The 72ms long task in the before run was also gone. That's one run each, so treat it as indicative.
Verification
Not in this PR
A horizontal version of the scroll copy. I built one, then measured it. In Chromium on a CPU canvas, copying through the scratch surface cost more than the text it saved: with the CPU throttled 4x, janky frames on horizontal scroll went from 27-46% to 66%. The existing vertical copy pays the same price, about 4.8ms a frame for its two drawImage passes. A single-copy version (two canvases that swap) benchmarks 24-53% cheaper than the scratch copy. I want to A/B that on WebKitGTK before shipping either direction.
Two costs during a sidebar drag: the grid's canvas backing store is reallocated on every frame (
syncCanvasSurface), andctx.fontset per cell forces style recalcs.Sidebar scrolling with about 400 tables spends 44-49% of the main thread on per-frame hover hit-testing.
Switching back to some tabs refetches all their rows, and five windows are prefetched eagerly right after the first paint.
bits-ui's body scroll lock forces a layout on every dialog and menu open (about 12-15ms), and the command palette mounts every item up front (one 105ms task).
On a connection switch the schema picker still appears a moment after the connection lands.
The first visit to Objects still pops its groups in. A background prefetch after the table list loads would fix it.
Background row counts still fail against a closed pool when switching connections. This was already happening before this branch.
Release
There's no
release:*label on this PR. The release is held until I give the go-ahead.