Custom Title Bars¶
An app can turn off the operating system's title bar and draw its own,
in its theme, with its own controls in it: a search field, tabs, a
menu.
examples/custom_title_bar/ in the repository shows each way below.
A borderless window¶
app = App(width=960, height=640, title="Notes", theme_seed=(0x67, 0x50, 0xA4, 0xFF),
borderless=True, min_width=480, min_height=320)
borderless=True takes away the OS's title bar and borders (in a window view, borderless: true). The app
then draws a title bar, and the window is still:
- moved by dragging the bar, and maximized by double-clicking it;
- resized from within
resize_border(6 px) of any edge or corner; - snapped by the OS (Aero Snap on Windows, edge tiling on Linux desktops that have it), since the drag is the OS's own;
- outlined by a 1 px border, since nothing else marks where it ends.
min_width and min_height keep the user from resizing it so small
that the bar's buttons crush.
The options (borderless, resize_border, min_width, system_menu, icon, fullscreen), the
window's actions and the app.maximized and app.active values a binding can follow are in
The window, and the Python API.
tesserae new notes --window --custom-title-bar makes an app like this
(The tesserae command); the window is a kind: Window with borderless: true and a title_bar:.
kind: TitleBar¶
The simplest title bar is one node in a view:
children:
- id: bar
kind: TitleBar
title: Notes
icon: home # optional: an icon name
buttons: [minimize, maximize, close] # the default; any of them, each once
children: # the app's own, between the
- {id: search, kind: TextField, ...} # title and the buttons
- id: body
...
It's a bar that moves the window when it's dragged (from anywhere but
its buttons and the app's own controls) and maximizes on a double-click.
It holds the icon and title at its start, the app's children in the
middle, and minimize, maximize and close at the right. Maximize shows
the restore glyph while the window is maximized, and its buttons call
the app's actions with no ViewModel method needed. Its parts' ids are
after the bar's: bar.title, bar.close and so on.
In a dialog or a sheet¶
A TitleBar is a header for any surface, not only a window. With buttons: [dismiss] it has no window buttons, no room for the
OS's controls and no drag region (a press on it doesn't move the window), and its one button closes the dialog or sheet the bar is
in (surface.dismiss). Use it in a ViewDialog. dismiss is on its own: a bar is
for a window or for a surface.
- {id: bar, kind: TitleBar, title: Settings, icon: settings, buttons: [dismiss]}
Height and look¶
It's 40 px high unless its style: {height: ...} says otherwise, and
every part follows, its glyphs staying centred. Its colours are Material
3 roles (surface, on_surface): with a theme_seed= it follows the theme,
light and dark, and an app with no theme gets MD3's baseline palette (light). The title, icon and buttons fade
while the window isn't the focused one, and close's hover is red
(error). Each part has a class, so a stylesheet or theme restyles it (see
Stylesheets and Themes):
| Class | Part |
|---|---|
title_bar |
The bar |
title_bar_icon, title_bar_title |
The icon and title |
title_bar_content |
The app's children |
title_bar_buttons, title_bar_button, title_bar_close |
The buttons |
title_bar_glyph |
Each button's glyph |
styles:
- classes: [title_bar]
style: {background: surface_container}
- classes: [title_bar_title]
style: {foreground: primary}
The bar's own look is the cascade's first layer, under every theme and stylesheet, so any of them can change it, and an app's own default theme doesn't take it away.
macOS¶
On macOS a borderless window keeps the OS's title bar, transparent,
with the traffic lights. A TitleBar leaves room for them at its start
and hides its own buttons. app.titlebar_inset (the (height, width)
they take, (0, 0) elsewhere and in fullscreen) and
app.native_controls (whether they show) are read-only Computeds, for a
bar of your own:
- id: inset
kind: Rect
bindings: {width: "{{ app.titlebar_inset.get()[1] }}"}
style: {height: 36}
A bar of your own¶
A bar of your own is any node marked as the window's drag region, with buttons whose handlers are the window's actions:
- id: my_bar
kind: Container
window_region: drag # a press here moves the window
style: {height: 36, background: surface_container}
children:
- {id: tabs, kind: Container, window_region: none, ...} # not a handle
- id: close
kind: Rect
handlers: {on_click: window.close}
...
window_region: drag covers the node and everything in it that isn't
interactive: a node with a click handler, a focusable one or a text
field is pressed as usual. none keeps a node, and what's in it, from
moving the window. The handlers are window.minimize,
window.maximize, window.restore, window.toggle_maximized and
window.close; they need the view to be on an App's window.
app.close() closes the window as the user's close would, so a "save
changes?" check on close_requested still runs and can cancel it.
A binding follows the window's state like any Signal's:
bindings:
text: "{{ app.maximized.get() and 'Restore' or 'Maximize' }}"
pointer_cancel¶
When the OS takes a press (moving the window from its bar, maximizing it
on a double-click), the pressed node gets pointer_cancel instead of
pointer_up, and no click. Every Tesserae widget releases its press on
it; a widget of your own that tracks a press should listen for both.
A top app bar as the title bar¶
In a borderless app, a top_app_bar made in code is the title bar: the window's drag region, with the window
buttons after its trailing icons. top_app_bar(..., window_controls=) asks for it or refuses it. In a view, use
a kind: TitleBar or a window's title_bar: instead.
The window border¶
A borderless window would blend into what's behind it, so Tesserae
draws a 1 px border around it, in the theme's outline_variant (an
unthemed app gets the baseline colour). It lies over every screen
and panel, but takes no presses: a press reaches whatever is under it,
and one within resize_border of an edge resizes the window. It hides
while the window is maximized or fullscreen, and it isn't drawn on
macOS, where the OS keeps the frame. It follows the app's theme, light
and dark.
App(window_border=False), or app.window_border = False, turns it
off. Its class is window_border, so a stylesheet or theme restyles it:
styles:
- classes: [window_border]
style: {border_color: "#FF0000", border_width: 2} # 0 hides it too
Rounded corners and a drawn shadow aren't possible: they need transparent windows.
Hot reload¶
A TitleBar hot-reloads like the rest of its view: an edit to its title,
icon, buttons or content shows while the app runs, and the buttons keep
working. So does a window view's title_bar:. See Hot Reload.
On each platform¶
| Windows | macOS | Linux (Wayland, X11) | |
|---|---|---|---|
| Title bar | The app's | The OS's, transparent, under the app's | The app's |
| Window buttons | The app's | The traffic lights | The app's |
| Resizing | resize_border |
The OS's frame | resize_border |
| Border | Tesserae's | The OS's frame | Tesserae's |
| Right-click on the bar | The app's, or the OS's window menu with system_menu=True |
The app's | The app's, or (on Wayland compositors that have one) the window menu with system_menu=True |