JBrowser Source Docs

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 (ui/window.py):

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. Frost reads 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's frost_tint over 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>.png in 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 canvas layer baked in. Up to four of these are kept (CACHE_SIZES). Painting is a 1:1 drawPixmap of 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.changed then 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 emits changed if it differs.
  • Theme.paint_backdrop(p, widget, rect, layer=None) paints either the frosted picture or, with Solid, the layer's _solid colour. RootWidget, the canvas and ChromeWindow paint 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-through sidebar surface).
  • For screenshots and tests, JBROWSER_WALLPAPER points Frost at 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 and ChromeWindows), "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, so WM_NCCALCSIZE takes over.
  • It cloaks a new window (DWMWA_CLOAK) from its first Show event until its first frame is painted (Backdrop.CLOAK_MS), so it never appears as a white or black rectangle. JBROWSER_NO_CLOAK=1 turns 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, outside QMenu.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, Tab models and ctx.settings, and connect to their signals.
  • Act through ui (the BrowserController) or ctx.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 on theme().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.

Theming, the canvas and cards, the sidebar, the Lazy Toolbar, the Gallery.