Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Runtime window control

The window can be commanded from Python while it is open, and read back the same way. Everything here is a property, so changing the window and asking what it is doing are the same word:

wv.title = f'{filename} — My App'
wv.size = (1080, 720)
wv.maximized = True

Since run() never returns, “while it is open” means from inside a callback: an Api callable, an Event listener or the close hook.

from dry import Webview


def open_settings() -> None:
    wv.size = (1080, 720)
    wv.title = 'My App — Settings'


wv = Webview(
    app_id='com.example.myapp',
    title='My App',
    html=HTML,
    api={'open_settings': open_settings},
)
wv.run()

open_settings names wv above the line that creates it, which is fine: Python looks a global up when the function runs, not when it is defined, and a callback cannot run until run() has the window open. Calling open_settings() yourself in between is the only way to reach a name that is not there yet, and nothing in the Bridge does that.

Settings that keep applying

title, size, min_size, decorations and icon_path are constructor arguments that go on working afterwards. Assigning one before run() decides what the window opens as; assigning it afterwards changes the open window. Every other constructor argument is read once while the Webview is built and raises if assigned later — see Window options.

SettingAssigned while running
titleRetitles the window
sizeResizes it, logical pixels
min_sizeSets the floor, and grows the window to meet it
decorationsAdds or removes the native titlebar and borders
icon_pathReplaces the icon; None restores the platform default

size reads back what the window currently measures, not the last number you gave it, so a window the user dragged reports the size it was dragged to. A min_size larger than the window resizes the window up to it:

wv.min_size = (900, 600)
wv.size  # (900, 600)

States a window only has once it is open

position, visible, maximized, minimized and fullscreen are not settings — a window that does not exist has no corner to sit in and no screen to fill. They are not constructor arguments, and touching one before run() raises rather than quietly storing a value that would never be applied:

wv = Webview(app_id='com.example.myapp', html=HTML)
wv.maximized = True
# RuntimeError: maximized belongs to a window that is on screen, and this
# Webview has not opened one yet. Ask for it once run() has, from inside an Api
# callable, an Event listener or the close hook.
StateAssignedRead
positionMoves the window, logical pixelsWhere its top-left corner is, decorations included
visibleTakes it off the screen, or puts it backWhether it is on screen
maximizedMaximizes, or restores the previous sizeWhether it fills its screen
minimizedMinimizes to the dock or taskbar, or restoresWhether it is minimized
fullscreenTakes over the screen, or comes backWhether it has

A platform that refuses part of what you asked for simply reports where the window actually went — macOS will not lift a window above the menu bar, and wv.position afterwards is the corner it settled on, not the one you named.

A minimized window is still visible. The user can see it in the dock or the taskbar and put it back; only visible = False takes it off the screen altogether.

fullscreen is borderless fullscreen on the window’s current monitor, which is what a desktop application wants: it does not change the display’s resolution under the user.

Reading the whole window at once

wv.state() answers with a WindowState, a NamedTuple of everything the window is doing, taken as one reading:

state = wv.state()
# WindowState(maximized=False, minimized=False, fullscreen=False, visible=True,
#             focused=True, size=(640, 480), position=(436, 144))

if not state.maximized:
    wv.maximized = True

Reading the properties one at a time is the same information, but a reading is taken at one instant, so its fields cannot contradict each other the way two properties read a moment apart can. size and position are pairs of logical pixels; size is the area the frontend renders into — the same measurement size= sets, and the same number the page reads back as window.innerWidth.

focused is in the reading and is not a property of its own: Dry reports which window has the keyboard, through window:focused and window:blurred as much as here, but does not take focus for you.

Like the five states, state() raises before run(): answering with the settings the window will be built from would be a guess dressed as a measurement.

Asking from the frontend

The same reading is a Promise in the page. A titlebar that has just loaded has observed no window Event at all and still has to draw its maximize button one way round:

const { maximized, size } = await window.dry.state();
icon.src = maximized ? RESTORE : MAXIMIZE;

The frontend gets the shape the window Events use — size is {width, height} and position is {x, y} — so a value from window.dry.on('window:resized', ...) and a value from dry.state() can go to the same code. Python prefers a pair, and that is the only difference between the two sides.

Every call waiting on the same trip resolves with the same reading, so a page polling the query costs one trip either way.

The frontend commands the window with window.dry.minimize(), window.dry.toggleMaximize() and window.dry.close(), which are the three a custom titlebar needs and are covered in Custom titlebars. Anything beyond those three is Python’s: give the page an Api callable that does it.

Every change announces itself

A change made from Python is announced exactly as a change the user made. Dry reads the window once per turn of its event loop and emits the window: Events for whatever moved, so wv.maximized = True and a double-click on the titlebar reach a listener identically, and a change the platform refused announces nothing:

wv.on('window:hidden', lambda _: tray.show_restore_item())
wv.on('window:shown', lambda _: tray.hide_restore_item())

window:hidden and window:shown are reachable exactly because visible is assignable — nothing else in Dry hides a window.

fullscreen is the one state with no Event of its own: macOS reports entering it as the platform’s own mix of a maximize, a window:moved and a window:resized, so a name for it could not be told from what already arrives. Read wv.fullscreen, or wv.state().fullscreen, instead of listening for it.

The reading can be one turn old

An assignment crosses to the thread that draws the window and is applied on its next turn of the event loop. The state query answers from the reading taken at the last turn — the same reading every window: Event was a difference from — so a query and an Event can never disagree, but a reading taken in the same breath as an assignment is still the state before it:

wv.maximized = True
wv.state().maximized  # False — the window has not had its turn yet

That is not a race to be won by sleeping. If the order matters, wait for the Event that announces the change:

maximized = threading.Event()
wv.on('window:maximized', lambda _: maximized.set())

wv.maximized = True
maximized.wait(timeout=5)

decorations at runtime does not add resize edges

An undecorated Webview draws its own resize edges, and the script that draws them is installed when the Webview is built, from the decorations the constructor was given. Assigning decorations later moves the native titlebar and borders and nothing else:

  • A window built with decorations=True and undecorated at runtime has no resize edges, and reloading the Content does not produce them. It can still be resized from Python and from window.dry.resize(direction), but there is nothing on its border to grab.
  • A window built with decorations=False keeps its eight edge strips even after decorations = True, sitting just inside the native frame.

So a window that means to toggle its titlebar should be built undecorated and draw its own, which is what Custom titlebars covers anyway. data-drag-region is unaffected either way: it is installed for every window.

Hiding a window instead of closing it

visible = False takes the window off the screen without closing it: no titlebar button, no dock entry, no close hook. The Webview goes on running — the event loop still turns, the Bridge still carries Calls and Events, and the page keeps its state — which is what makes a tray application possible, where closing the window would end the process.

def on_close() -> bool:
    wv.visible = False
    return False  # refuses the close; the window is hidden, not gone


wv = Webview(app_id='com.example.myapp', html=HTML, on_close=on_close)

Something else then has to bring it back with wv.visible = True — a tray icon, a global shortcut, a second instance handing over. A window hidden with nothing left to show it again is a process the user cannot see or quit.

Everything is logical pixels

Every dimension and coordinate here — size, min_size, position, and both values inside a WindowState — is in logical pixels, independent of display scaling, the same unit the constructor takes and the same unit window Events report. They are the numbers CSS is working in, so a window told to be 640 wide reports 640 from window.innerWidth on a display at any scale factor.