The canvas and cards
SpaceStack and Canvas#
SpaceStack (ui/canvas.py) holds one
Canvas per space and slides vertically between them when the active space changes (instantly with Fluid
animations off). Only the active space's canvas is shown; hidden canvases report all their cards as out of view,
so their pages throttle.
A Canvas lays its space's cards out left to right and scrolls horizontally.
It is not a QScrollArea: it keeps an offset and positions each WebCard itself, which is what makes the smooth
animated layout possible.
Layout#
- A card's width is a fraction of the canvas (
tab.width, 0.1 to 1.0).px_width()turns it into pixels so that two 50 % cards, three 33 % cards or four 25 % cards exactly fill the view, gaps included (MARGINandGAPare 12 px), with a minimum ofMIN_CARD_W(120 px). targets()computes every card's(x, width)in content coordinates;_geoholds the current, animated values.request_layout(animated=True)animates_geotowards the targets overLAYOUT_MS(230 ms). Changes are batched withschedule_layout(), so ten width changes cause one layout.- New cards grow in from their centre; removed cards close the gap.
Width presets (Alt+1 … Alt+9, Alt+0), splits (50/50, 33×3, 25×4) and toggle full width only change tab.width
on the selected cards; the layout follows.
Scrolling and panning#
scroll_to(), scroll_by() and ensure_visible(tab_id, align) move the offset, animated with motion(). The
canvas takes horizontal wheel and trackpad input directly, and MainWindow routes Alt + wheel from anywhere over
the canvas to pan_from_wheel(). The overview strip (Minimap) at the bottom draws every card as a segment and
the viewport as a window; clicking or dragging it scrolls.
Pull to add a card#
Scrolling past either end builds up a "pull" (PULL_DIST, 440 px of wheel travel). The cards slide aside by up to
REVEAL (88 px), an EdgePullIndicator "+" grows in the gap with a ring that fills, and when the ring is full a new
card opens at that end (the Lazy Toolbar opens with the insert position). From the keyboard, Alt+← on the first
card or Alt+→ on the last one shows the "+" (edge_nudge), and pressing it again adds the card.
Visibility and the lifecycle#
After every layout or scroll, _update_visibility() works out which cards intersect the viewport and reports each
change to ctx.lifecycle.set_in_view(tab_id, visible, render_visible). That is what drives
throttling and the memory saver, and what makes a lazily restored card load the moment
it appears. A card also knows on_screen, so an off-screen card only marks itself stale when its Tab changes.
Dragging cards#
Dragging a card's header reorders it: when the card passes a neighbour's midpoint, BrowserState.move_tab() moves
it, and the layout animates. (Lifting, dragging to other spaces and Alt + drag came in 1.5.0.)
Double-clicking a header toggles full width; middle-clicking closes the card; right-clicking opens the card menu.
WebCard#
A WebCard (ui/card.py) is one card:
WebCard
├── CardHeader favicon, title, security, blocked count, buttons (compact when the card is narrow)
├── QStackedWidget
│ ├── QSplitter BrowserView (the QWebEngineView) and, when open, docked DevTools
│ └── SnapshotView shown while the card sleeps
├── info bars from TabController.infobar
├── FindBar Ctrl+F
└── StatusBubble the hovered link's address
apply(fields)updates the header and state from theTab's changed fields.BrowserViewbuilds JBrowser's context menu on top of Chromium's standard actions (open link in a new card or another space, copy clean link, save image, inspect…).take_snapshot()grabs the view as a picture while it still renders (before throttling and sleep);enter_sleep()shows it faded with a moon badge.teardown()detaches the view from the page before deletion: Qt WebEngine requires views to go before pages.
Immersive full screen#
When a page requests HTML5 full screen (a video), set_solo(tab_id) makes that card fill the canvas and the window
hides its chrome; Escape or the page's own button restores it.