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 a translucent backdrop.
The main window#
MainWindow (QMainWindow, translucent, custom frame)
└── RootWidget paints the backdrop wash (colour tint or incognito black)
├── 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.
apply_backdrop() asks DWM for Mica, Mica Alt or Acrylic
(DWMWA_SYSTEMBACKDROP_TYPE); translucent pixels in the window then show the backdrop.
Changed in 1.6.2 One owner for each window's look. DWM draws the material light or dark according to the
window's DWMWA_USE_IMMERSIVE_DARK_MODE, and more than one party sets it: JBrowser, and Qt, which re-applies its own
idea of light/dark to every window frame when the application palette or colour scheme changes
(QWindowsWindow::windowEvent, ApplicationPaletteChange; Qt judges by the colour scheme and the palette at that
moment). 1.6.0 reacted to that by reading the attribute back, rebuilding the material and refreshing the frame on
activation, and the main window switched JBrowser between its translucent and solid look whenever a DWM call failed.
On some PCs those reactions fed each other: pale windows, a stale rectangle over the title bar, a caption that
stopped dragging (a frame refresh during activation interrupts a drag), black or white flashes and a frozen app (a
restyle loop). ui/backdrop.py replaces all of it:
Backdrop(window, kind)owns one top-level window:"window"(frameless windows with a material: the main window andChromeWindows),"popup"(the Archive and everyQMenu: Acrylic over the whole window) or"frame"(dialogs with a native title bar: the mode and the caption colours). It applies the material and the mode when they change, and refreshes the frame once per native window soWM_NCCALCSIZEtakes over.- After anything that can disturb the mode (palette changes, activation, window state changes,
WinIdChange, and Windows settings broadcasts thatNativeFramereports throughon_system_change), it sets the mode again, coalesced into one deferred call. Setting the same value changes nothing on screen, so nothing can loop. It never reads the state back, never rebuilds the material and never restyles the app. Theme.translucentdepends only on the setting andwin.backdrop_supported()(Windows 11, or Windows 10 with pywinstyles), never on a single DWM call.Theme.backdrop_layers(): translucent windows paint JBrowser's own base colour (backdrop_base, about 55–60 % opaque) and then the tint over the backdrop, so a window stays dark or light whatever DWM draws behind it.Theme.refresh()applies the new palette before telling Qt the colour scheme (_request_scheme()), so Qt's re-application finds the matching palette.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.- Menu actions (
menu_action()) run after the menu has closed, outsideQMenu.exec()'s event loop.
New in 2.0.1 When Windows draws no material, or the wrong one. With transparency effects or energy saver off, a
high-contrast theme, over Remote Desktop, or with a graphics driver that can't give windows see-through pixels, DWM
draws a flat fill (or black) behind the window instead of Mica or Acrylic. LookGuard in
ui/backdrop.py decides Theme.see_through from three sources:
win.material_blockers()(the Transparency effects setting,SPI_GETHIGHCONTRAST,GetSystemPowerStatus,SM_REMOTESESSION), every 3 seconds and wheneverNativeFrame.on_system_changereports a broadcast;- Qt's own warnings about Direct Composition (
note_qt_message(), fed by the message handler in app.py, which also writes Qt's warnings to the log); - the screen. After a window appears, a theme change or a system change, and now and then on activation, the active
window leaves a 3 × 3 spot at the top edge of its title bar unpainted for one frame (
paint_probe_hole()) andwin.sample_own_pixel()reads what DWM composited there: the bare material. Black means no see-through pixels; a light material behind a dark theme (luma above 170) or a dark one behind a light theme (below 110) means the wrong material. (Measured on Windows 11: dark Acrylic reads 54 over black and 146 over white, light Acrylic 135 and 227, Mica about 32 and 244.) Two readings in a row must agree.
While see_through is off, backdrop_layers() puts the opaque TRANSITION_BASE under everything and menus and glass
popups get opaque backgrounds, so the window looks like the Solid material until the material is back.
Two more things keep the look steady:
NativeFrameanswersWM_NCACTIVATEwithwParam = TRUE, so DWM keeps the active material when the window loses the focus, instead of its flat grey or white inactive fill. Qt still learns about activation fromWM_ACTIVATE.Backdropcloaks a new"window"or"frame"window (DWMWA_CLOAK) from its firstShowevent until its material, mode and first frame are ready (Backdrop.CLOAK_MS), so it never appears as a white or black rectangle.
For testing: JBROWSER_NO_MATERIAL=1 behaves as if Windows drew no materials, JBROWSER_INACTIVE_LOOK=1 restores
Windows' inactive look and JBROWSER_NO_CLOAK=1 turns the cloak off.
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.
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.