User interface overview
Every pixel JBrowser draws is a Qt widget, most of them custom-painted in paintEvent with QPainter rather than
styled with style sheets. That is what gives JBrowser its Windows 11 look, over its own frosted backdrop.
The main window#
MainWindow (QMainWindow, opaque, custom frame)
└── RootWidget paints the Frosted picture (tint included) or the Solid colour
├── Sidebar favourites, spaces, the space's cards, tools
└── right column
├── TitleBar navigation, the address pill, tools, window buttons
├── FavoritesBar the bookmarks bar (Ctrl+Shift+B)
└── SpaceStack one Canvas per space, sliding between them
└── Canvas → WebCard one card per tab: header, web view, info bars, find bar
└── Gallery the overlay grid of every card (Ctrl+Shift+G)
overlays: LazyToolbar · HotkeySheet · ToastManager · Onboarding
The constructor also creates the BrowserController, installs WindowHooks as ctx.hooks, registers every
command and installs the shortcuts, and connects the lifecycle signals to the cards.
The window frame#
The window has no native title bar but keeps every native behaviour. NativeFrame in
platform/win.py handles WM_NCCALCSIZE (the whole window is client area),
WM_NCHITTEST (resize edges, the caption, and HTMAXBUTTON over the maximise button so Windows 11 shows the Snap
Layouts flyout), rounded corners and the drop shadow.
To keep the hit test cheap, NativeFrame.handle() reads only the message number from the MSG and returns at once
for every message it doesn't handle, which is almost all of them.
Changed in 2.0.3 Opaque windows, JBrowser's own frosted look. Every JBrowser window is opaque: none sets
WA_TranslucentBackground, and DWM materials (Mica, Mica Alt, Acrylic) are not used at all. Earlier versions asked
DWM for a material and left the window's pixels see-through. On some PCs, DWM drew no material, the wrong one, or a
stale one over part of the window, and no amount of detecting and repainting made that reliable. Now nothing that
Windows draws can show through a JBrowser window.
The look is painted by JBrowser itself, in ui/frost.py:
- The picture.
Frostreads the desktop wallpaper (win.wallpaper_path(),SPI_GETDESKWALLPAPER; or the desktop's solid colour,win.desktop_color()), reduces it to 64 pixels wide in halving steps, saturates it ×1.35, lays the theme'sfrost_tintover it (plus the colour tint's wash), and enlarges it to 1024 pixels wide in doubling steps. Each bilinear step softens the last, so the result is a smooth blur without seams. The 64-pixel copy is cached on disk (frost-<hash>.pngin the cache folder), so starting up never decodes the wallpaper again. The wallpaper never leaves the PC. - One pixmap per window size. For a window of a given size, the smooth picture is cover-fitted, cropped, given
a 2.8 % grain (so soft gradients don't band) and, for the canvas, the
canvaslayer baked in. Up to four of these are kept (CACHE_SIZES). Painting is a 1:1drawPixmapof the dirty rectangle, as cheap as a colour fill. The picture is fitted to the window, not to the screen, so moving a window never repaints anything. - Resizing. A window's first picture is built at once. While a window is being resized, it gets the nearest
picture already made, stretched, and only the size it rests at is built, 140 ms later (
Frost.changedthen repaints the windows). - A new wallpaper.
Frost.check()compares the wallpaper's path, size and date when Windows broadcasts a settings change and when JBrowser is activated, and emitschangedif it differs. Theme.paint_backdrop(p, widget, rect, layer=None)paints either the frosted picture or, with Solid, the layer's_solidcolour.RootWidget, the canvas andChromeWindowpaint every pixel of their area (WA_OpaquePaintEvent), so Qt never fills a background first. The title bar, the bookmarks bar and the sidebar have no base of their own. They draw over the root's picture, in the same opaque window (the sidebar adds its partly see-throughsidebarsurface).- For screenshots and tests,
JBROWSER_WALLPAPERpointsFrostat another picture.
What is left for Windows is small. ui/backdrop.py:
Backdrop(window, kind)keeps one top-level window's frame in step with the theme:"window"(frameless windows: the main window andChromeWindows),"popup"(the Archive: rounded corners only) or"frame"(dialogs with a native title bar: the mode and the caption colours). After anything that can disturb the light/dark mode (palette changes, activation, window state changes,WinIdChange, Windows settings broadcasts), it sets the mode again in one deferred call. Setting the same value changes nothing on screen, so nothing can loop. It refreshes the frame once per native window, soWM_NCCALCSIZEtakes over.- It cloaks a new window (
DWMWA_CLOAK) from its firstShowevent until its first frame is painted (Backdrop.CLOAK_MS), so it never appears as a white or black rectangle.JBROWSER_NO_CLOAK=1turns that off. caption_hit()does the caption hit test for both window kinds and never raises. An exception there used to make the window procedure fall back to "client area", and the window stopped dragging.Theme.refresh()applies the new palette before telling Qt the colour scheme (_request_scheme()), so Qt's re-application finds the matching palette.- Menu actions (
menu_action()) run after the menu has closed, outsideQMenu.exec()'s event loop. Menus are opaque, and Windows rounds their corners.
The window's event filter#
MainWindow.eventFilter() sees events for the whole application. It implements the behaviours that cut across
widgets: Alt + mouse wheel pans the canvas from anywhere, pointing at the left edge peeks the hidden sidebar, mouse
back/forward buttons navigate the card under the pointer, and clicking inside a card focuses it.
Alt + drag anywhere on a card moves it.
It also gives menus and tooltips native rounded corners.
Because it sees every event, it starts by checking the event type against _WATCHED_EVENTS (a frozenset) and
returns at once for everything else, such as paints, timers and layout requests.
How widgets talk#
- Read from
ctx.state,Tabmodels andctx.settings, and connect to their signals. - Act through
ui(theBrowserController) orctx.commands.run(id). Never call another widget. - Draw with the theme's tokens (
theme().c("text2"),theme().surface("card")), never hard-coded colours, and repaint ontheme().changed. - Animate with
motion()(motion).
Overlays#
Full-window modal layers derive from Overlay in ui/widgets.py: a scrim over the
window, a panel from panel_rect(), Escape and clicks outside close it, focus returns where it was. The Lazy Toolbar
and the shortcut sheet are overlays. Transient messages use ToastManager.show(text, glyph, duration_ms).
Reusable widgets#
ui/widgets.py holds the custom-painted controls: IconButton (with badge and
progress ring), ToggleSwitch, Spinner, ProgressLine, ElidedLabel, InfoBarWidget, Toast, helpers such
as menu_action(), and the colour-tint picker TintPicker. Icons are glyphs from the Segoe Fluent Icons font
(ui/icons.py): icon("settings") returns a QIcon whose colour follows the theme,
and draw_glyph() paints one directly.
Accessibility#
- Custom widgets set accessible names and descriptions (
setAccessibleName), and keyboard focus is always visible. - Everything is reachable from the keyboard; the command registry makes every action searchable.
- Fluid animations off removes every animation; on first run it follows the Windows "Animation effects" setting.
Read next#
Theming, the canvas and cards, the sidebar, the Lazy Toolbar, the Gallery.