Skip to content

Multiple Windows

An app is not tied to one window. nv.Window is a window of its own — with its own widget tree, overlay, navigator, focus and menu bar — constructed as a model and shown with open():

import nuiitivet.material as nv


class Main(nv.ComposableWidget):
    def _open_palette(self) -> None:
        nv.Window(
            content=lambda: Palette(self.state),
            title="Palette",
            width=280,
            height=170,
        ).open()

    ...


app = nv.App(nv.Window(content=Main, title="My app"))
app.run()

nv.App takes its main window as the first argument, plus the app-level options (theme, exit_policy). Every window-flavored keyword (width, height, title, chrome, menu, ...) belongs to nv.Window — the same constructor whether it builds the main window or a secondary one.

A runnable demo is at samples/window/multi_window.py.

One object, one window lifetime

Window(...) builds a model — no OS window yet. open() realizes it (from anywhere: before app.run(), or from a callback while the app runs) and returns the window for chaining. close() destroys it: the tree is unmounted and the object is finished — to show the same content again, construct a new Window. The OS close button is equivalent to close().

State that must survive a window therefore lives in the app layer — an Observable created outside the window and passed into its content. This is the framework's ordinary state idiom; a palette rebuilt from the same shared state reopens exactly where it left off:

class AppState:
    def __init__(self) -> None:
        self.color = nv.Observable("#6750A4")

state = AppState()
nv.Window(content=lambda: Palette(state), title="Palette").open()

window.is_open is an Observable[bool], and await window.closed resolves once the window has closed.

Everything resolves per window

Each window has its own overlay, navigator, and focus state. Overlay.of(context), Navigator.of(context) and every other .of(context) lookup resolve to the window the context belongs to, so dialogs, menus and navigation confine themselves to their own window with no extra plumbing. nv.Window.of(context) returns the window itself — the way content closes its own window:

nv.Button("Done", on_click=lambda: nv.Window.of(self).close())

The menu bar is per window too: Window(menu=...) (and window.menu = ... for wholesale replacement). On macOS, where the menu renders on the global bar, the bar follows the focused window; a window with menu=None shows the main window's menu, so a single-menu app needs no per-window declarations.

Parent, child, modal

parent= makes a child window: it stacks with its parent and closes, transitively, when the parent closes. modal=True (requires parent) additionally blocks pointer and keyboard input to the parent chain while the child is open — a settings window the main window must wait for:

nv.Window(
    content=lambda: Settings(state),
    title="Settings",
    parent=nv.Window.of(self),
    modal=True,
).open()

Modality is enforced by the framework (the OS backend has no cross-platform window modality): sibling top-level windows stay interactive, and activating a blocked parent hands focus back to its modal child.

The first click into an inactive window

By default, an inactive window is clickable on the first click: the click activates the window and acts — a tool palette does not cost two clicks per swatch.

Under the classic macOS convention, the first click into an inactive window only activated it; the click itself was not delivered. If you want that classic behavior, set the flag to False per window:

nv.Window(content=..., accepts_first_mouse=False)

This is a macOS-only parameter — other platforms always deliver the click.

Window operations

The window-management verbs are methods on the window: maximize(), minimize(), restore(), full_screen(), center(), move_to(x, y), resize(w, h), close() — see Window Operations. Exiting the whole application is App.of(context).exit(). Menu-bar standard items (MenuEntry.close_window(), MenuEntry.quit(), ...) call the right method on the right scope themselves.

When the app exits

App(..., exit_policy=...) decides when app.run() returns:

Policy Behavior
nv.ExitPolicy.LAST_WINDOW_CLOSED Default — exit once no window remains open.
nv.ExitPolicy.MAIN_WINDOW_CLOSED Closing the main window closes every other window and exits.
nv.ExitPolicy.EXPLICIT Only app.exit() exits; the app keeps running with zero windows, so keep some way to reopen one.

app.exit() always closes every window and exits, under any policy.

Theme

The theme is app-wide: App(..., theme=...) supplies every window, and app.set_theme(...) switches it for every window. Per-window theme overrides are a planned extension (Window(theme=...)), not yet implemented.