# Pyzed: Guides > Pyzed is an open-source (MIT) Python framework for building native desktop GUIs. Python describes the interface; Iced, a GPU-accelerated GUI toolkit written in Rust, lays it out and draws it. It aims to be a frontend framework for desktop apps (mobile and web later), with 90+ components whose look is inspired by the Zed editor, themes, node-based GPU shaders, packaging for every desktop platform and Pyzed Design, a drag-and-drop visual designer in the spirit of Visual Basic, Qt Designer and Godot. ## Guides ### App architecture > Pyzed app architecture: state, view, update, messages and callables. The Elm architecture of Iced, explained for Python desktop developers. Source: https://albakore.github.io/pyzed/guides/architecture/ Pyzed follows Iced's **Elm architecture**: state lives in your object, `view()` draws it and events change it. After every change, Pyzed calls `view()` again. ``` ┌──────────── view() ────────────┐ │ ▼ state (self) widget tree ──► Iced draws it ▲ │ └── update(msg) / callable ◄─────┘ the user clicks, types… ``` #### The App class ```python import pyzed as pz class MyApp(pz.App): title = "My app" # or a method/property that returns the title size = (900, 600) min_size = (480, 320) theme = "One Dark" # a Theme, a name or pz.themes.auto(...) def __init__(self): # the initial state self.items = [] def view(self): # required: returns a Widget ... def update(self, message): # optional: receives messages ... pz.run(MyApp) # or MyApp().run() ``` All class attributes are listed in the [App reference](https://albakore.github.io/pyzed/reference/app/#app). :::tip[`view()` must be cheap and side-effect free] Pyzed calls `view()` after every message. Compute what you are going to show there, but don't do I/O or change state: that belongs in `update` or in callbacks. ::: Every interaction runs the same loop: Pyzed calls `view()`, Iced draws the tree and turns each event into a message, and the message updates your state, which triggers the next `view()`. ```mermaid flowchart LR S[("state: self.*")] --> V["view()"] V -->|widget tree| R["Iced (Rust)"] R -->|click, key, input…| M{"message"} M -->|a value| U["update(message)"] M -->|a callable| C["called directly"] U --> S C --> S U -.->|Command| E["effects: tasks, focus, windows…"] E -.-> M ``` #### Messages Interactive widgets take a **message**: any Python value. When activated, Pyzed sends it to `update`: ```python ui.button("Delete", ("delete", item.id)) def update(self, message): match message: case ("delete", item_id): self.items = [i for i in self.items if i.id != item_id] case "clear": self.items.clear() ``` Use messages when you want to keep the logic in one place, log actions, undo them or test `update` without a UI. #### Callables If the message is a callable, Pyzed **calls it directly** and `update` never sees it: ```python ui.button("Save", self.save) ui.button("Delete", lambda: self.delete(item.id)) ``` The callable's return value is handled like this: | Returns | What happens | | --- | --- | | `None` | Nothing else: Pyzed redraws. | | a `Command` (or a list of them) | The command runs. | | a coroutine, a generator or an async generator | It runs in the background and each result goes to `update`. See [async and subscriptions](https://albakore.github.io/pyzed/guides/async/). | | any other value | It is sent to `update` as a message. | Controls that produce a value call the callable with that value: `on_input(text)`, `on_toggle(bool)`, `on_change(value)`, `on_select(id)`, `on_cursor(line, column)`. :::caution[Lambdas inside loops] Capture the variable with a default argument, or every lambda will use the last value: `lambda f=f: self.open(f)`. ::: #### Optional hooks | Method | When it is called | | --- | --- | | `on_start()` | Once, at startup. It can return a `Command` or a coroutine (for example, to load data). | | `on_key(combo)` | On every key press. See [keyboard and shortcuts](https://albakore.github.io/pyzed/guides/keyboard/). | | `subscriptions()` | After every `update`. Returns the active timers and streams (`pz.every`, `pz.listen`). | | `on_open()`, `on_close()`, `on_resize(w, h)` | Window events. See [multiple windows](https://albakore.github.io/pyzed/guides/windows/). | #### Effects: `Command` `update`, `on_start`, `on_key` and callbacks can return a [`Command`](https://albakore.github.io/pyzed/reference/command/) to request effects: focus a field, close the app, run work in the background… See [commands and tasks](https://albakore.github.io/pyzed/guides/commands/). #### Errors If `view()` raises an exception, the window shows the traceback instead of closing. Fix the code and the next interaction tries again. Exceptions in callbacks and in `update` are printed to the console. #### Organizing a large app Since components are functions, split `view()` into regular methods and functions: ```python class Editor(pz.App): def view(self): return ui.workspace(self.center(), title_bar=self.title_bar(), left=self.project_panel() if self.show_left else None, status_bar=self.status_bar()) def project_panel(self): return pz.column([ui.panel_header("Project"), ui.tree(self.nodes, expanded=self.expanded)]) ``` To reuse pieces across apps, write functions that take data and callbacks and return widgets, just like the ones in `pyzed.ui`. See [`examples/zed_editor.py`](https://github.com/albakore/pyzed/blob/main/examples/zed_editor.py) for an example of a large app. ### Apps, windows, forms and components > The methods that matter when you write a pz.App, a pz.Window, a designed form or a pz.Component, when Pyzed calls each one, and how to create your own events. Source: https://albakore.github.io/pyzed/guides/classes/ Everything you write in Pyzed is one of four kinds of class (or file). Each one has a few methods that Pyzed calls for you at the right moment: this page lists them, says when they run and shows what to put in them. | You write | It is | Pyzed calls | | --- | --- | --- | | `class MyApp(pz.App)` | The app and its main window. | `view()`, `update()`, `on_start()`, `on_key()`, `subscriptions()`… | | `class Settings(pz.Window)` | Another window of the same app. | `view()`, `on_open()`, `on_close()`, `on_resize()`… | | `login.pzform` + `login.py` | A form drawn in [Pyzed Design](https://albakore.github.io/pyzed/editor/), with its handlers. | `form_load(form, event)`, `_(form, event)`… | | `class Rating(pz.Component)` | A reusable piece of UI with props, state and events. | `setup()`, `render()`, `on_mount()`, `on_unmount()` | ```mermaid flowchart LR Start(["pz.run(MyApp)"]) --> Init["__init__()
initial state"] Init --> OnStart["on_start()"] OnStart --> View["view()"] View -->|widget tree| Draw["Iced draws it"] Draw -->|click, key, timer…| Event{"event"} Event -->|callable| Call["your method"] Event -->|message| Update["update(message)"] Call --> View Update --> View Draw -.->|window closes| Close["on_close()"] ``` #### `pz.App`: the application ```python import pyzed as pz from pyzed import ui class Notes(pz.App): title = "Notes" # window options are class attributes size = (720, 480) theme = "One Dark" def __init__(self): # 1. the initial state: plain attributes self.notes = [] self.draft = "" self.saved_at = None def on_start(self): # 2. once, when the window is open return pz.Command.focus("draft") def view(self): # 3. after every change: describe the window return pz.column([ ui.input(self.draft, self.set_draft, id="draft", placeholder="New note", on_submit=self.add), pz.column([ui.list_item(n, icon="sticky-note") for n in self.notes]), ui.muted(f"Saved at {self.saved_at}" if self.saved_at else "Not saved yet"), ], spacing=8, padding=16) def set_draft(self, text): # 4. callbacks change the state self.draft = text def add(self): if self.draft.strip(): self.notes.append(self.draft.strip()) self.draft = "" def update(self, message): # 5. messages that are not callables land here if message == "save": self.saved_at = "now" def on_key(self, combo): # 6. every key press of the window if combo == "ctrl-s": return self.update("save") def subscriptions(self): # 7. timers and streams active right now return [pz.every(60, "save")] if self.notes else [] pz.run(Notes) ``` ##### The methods | Method | When Pyzed calls it | What to put in it | May return | | --- | --- | --- | --- | | `__init__()` | Once, when the app is created (`pz.run(MyApp)` creates it). | The initial state as attributes. No widgets and no I/O. | — | | `view()` | At start and after **every** event, message or command. | Build and return the widget tree from the state. Never change state here. | A `Widget` (required). | | `update(message)` | When a widget's handler is a value (a string, a tuple, a dataclass…), or a callable returns one. | Change the state according to the message. | `None`, a `Command`, a list of them, a coroutine or another message. | | `on_start()` | Once, after the window opens. | Load data, focus a field, start a background task. | A `Command`, a coroutine or a message. | | `on_key(combo)` | On every key press of the window, before the focused widget. `combo` reads like `"ctrl-shift-p"`. | Global shortcuts. Returning something (even `[]`) keeps the key from moving the focus. | Like `update`. | | `on_key_up(key)` | When a key is released (`"left"`, `"space"`). | Games and press-and-hold controls. | Like `update`. | | `subscriptions()` | After every update. | Return the timers, streams and frame callbacks that should run *now*: `pz.every`, `pz.listen`, `pz.frames`. A subscription you stop returning stops. | A list. | | `on_close()` | When the user closes the window. | Ask to save. Return `False` to keep the window open. | `None` or `False`. | | `on_resize(width, height)` | When the window changes size. | Switch layouts (also see `self.window_size`). | Like `update`. | | `frame_menus()` | When `frame = "custom"` draws the title bar. | The menus on its left side: `[ui.Menu("File", [...])]`. | A list or `None`. | And the ones you call yourself: | Method | What it does | | --- | --- | | `run(reload=None)` | Opens the window and blocks until the app closes (`pz.run(MyApp)` is the same). `reload=True` turns on [hot reload](https://albakore.github.io/pyzed/editor/console-and-running/). | | `screenshot(path, size=None)` | Renders the current state into a PNG without a screen (tests, docs). | | `resolved_theme()` | The theme in use right now, with `pz.set_style` applied: `self.resolved_theme().accent`. | Useful attributes the runtime keeps up to date: `self.window_size`, `self.window_focused`, `self.window_maximized`, `self.window_position` and `self.system_dark`. The window options (`title`, `size`, `min_size`, `resizable`, `frame`, `transparent`, `icon`, `splash`, `tab_focus`…) are listed in the [App reference](https://albakore.github.io/pyzed/reference/app/#app). :::tip[Callables or messages?] A handler can be a **callable** (`self.add`, `lambda: self.open(path)`), which Pyzed calls directly, or a **message** (`"save"`, `("delete", item.id)`, `Delete(item.id)`), which goes to `update`. Both work in the same app: see [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/) for when to use each. ::: #### `pz.Window`: more windows A `Window` is an `App` without the app-wide parts: it has its own `view()` and window options, and sends its messages to the app's `update()`. Open it by returning a command: ```python class Settings(pz.Window): title = "Settings" size = (420, 300) def on_open(self): # the window is on screen self.draft = self.app.prefs.copy() # self.app is the App that opened it def view(self): return pz.column([ ui.switch(self.draft["dark"], self.set_dark, label="Dark mode"), ui.button("Save", self.save, variant="solid"), ], spacing=12, padding=16) def set_dark(self, value): self.draft["dark"] = value def save(self): self.app.prefs = self.draft return self.close() # a command def on_close(self): # the user pressed the window's X return None if self.draft == self.app.prefs else False class Main(pz.App): prefs = {"dark": True} def view(self): return ui.button("Settings…", lambda: Settings().open()) ``` | Method | When | Notes | | --- | --- | --- | | `view()`, `on_key()`, `on_key_up()`, `on_resize()`, `subscriptions()` | As in `App`. | — | | `on_open()` | The window opened (`self.app` is already set). | May return a command. | | `on_close()` | The user asked to close it. | `False` keeps it open. | | `update(message)` | A message produced inside the window. | Forwards to `self.app.update` by default. | | `open()`, `close()`, `drag()`, `minimize()`, `toggle_maximize()` | You call them. | Each returns a `Command`: return it from a handler. | See [Multiple windows](https://albakore.github.io/pyzed/guides/windows/) for positions, modal windows and custom frames. #### Designed forms: `.pzform` + handlers A form you draw in Pyzed Design is saved as `login.pzform` (the controls and their properties) and `login.py` (your code). The code file has no class: each event is a **function** named `_`, and the form's own events are `form_`. Every handler gets the running `form` and the `event`: ```python # login.py: the handlers of login.pzform def form_load(form, event): # the window opened form.status.text = "" return form.email.focus() # handlers may return commands def email_change(form, event): # every keystroke: event.value is the new text form.sign_in.enabled = "@" in event.value def password_submit(form, event): # Enter in the password box return sign_in_click(form, event) def sign_in_click(form, event): if not form.validate(): # runs the validation rules set in the designer return form.progress.show() return form.open("welcome", data=form.email.text) def form_key(form, event): # every key press: event.value is the combo if event.value == "escape": return form.close() def form_close(form, event): # return False to keep the window open if form.password.text: return False ``` | Handler | When | `event.value` | | --- | --- | --- | | `form_load` | Once, when the window opens. | `None` | | `form_close` | The window is closing; `False` keeps it open. | `None` | | `form_key` | Every key press. | The combo, `"ctrl-s"` | | `form_resize` | The window changed size. | `(width, height)` | | `_` | The control raised the event (`click`, `change`, `submit`, `tick`…). | Depends on the event: the new text, `True`/`False`, the selected item… | The `form` object is your way into the window: `form..` reads and writes properties, `form..show()` / `.hide()` / `.focus()`, `form.title`, `form.data` (what `form.open(..., data=)` passed), `form.validate()`, `form.close()` and `form.open("other_form")`. The events of every control are listed in [Events and handlers](https://albakore.github.io/pyzed/editor/events/), and on each page of the [library](https://albakore.github.io/pyzed/library/). Run a form from Python, or open it from a code-first app: ```python from pyzed import designer designer.run("ui/login.pzform") # the form is the app (same as `pyzed run`) class Main(pz.App): # or a form as one more window def view(self): return ui.button("Sign in…", lambda: designer.open_window("login", data="ada@example.com")) ``` #### `pz.Component`: reusable pieces A component is a class with **props** (annotated class attributes, passed by the parent), **state** (attributes it keeps between renders) and **events** (that it raises with `self.emit`). Calling it returns a widget. ```python class Stepper(pz.Component): """A number with − and + buttons.""" value: int = 0 # props step: int = 1 min: int = 0 max: int = 10 events = {"change": "The value changed (event.value is the new number)."} pressed: int = pz.state(0) # state, private to each instance def setup(self): # once, before the first render self.pressed = 0 def on_mount(self): # after the first render return None def change(self, delta): self.pressed += 1 new = max(self.min, min(self.max, self.value + delta)) return self.emit("change", new) # calls the parent's on_change def render(self): # on every render of the parent return pz.row([ ui.icon_button("minus", lambda: self.change(-self.step)), pz.text(str(self.value), size=18), ui.icon_button("plus", lambda: self.change(self.step)), ], spacing=8, align="center") def on_unmount(self): # it is no longer rendered print(f"pressed {self.pressed} times") # In any view: Stepper(value=self.copies, max=99, on_change=lambda e: setattr(self, "copies", e.value)) ``` | Method | When | Notes | | --- | --- | --- | | `setup()` | Once, before the first render. Props are already set. | Initialize state. | | `render()` | Every time the parent's `view()` calls the component. | Return widgets; read `self.` and the state. | | `on_mount()` | After the first render. | May return a `Command` or a message. The parent's `on_mount=` handler runs too. | | `on_unmount()` | When the parent stops rendering it. Its state is dropped. | Stop timers, close files. | | `emit(name, value)` | You call it. | Calls the parent's `on_` with a `pz.Event(value=..., source=self)`. Return its result. | | `handles(name)` | You call it. | Whether the parent passed `on_`: hide a button nobody listens to. | | `provide()` | Before each render. | A dict shared with everything rendered inside (`pz.use_context(name)`). | | `context(name)` | You call it. | The closest value shared with `pz.provide` or `provide()`. | | `Rating.create(...)` | In tests. | An instance without rendering it: `Rating.create(value=3).render()`. | Every component also takes the [common events](https://albakore.github.io/pyzed/guides/events/) (`on_click`, `on_drag`, `on_key_down`…), `style=`, `class_=`, `id=`, `animate=` and `key=`. See [Custom components](https://albakore.github.io/pyzed/guides/components/) for children, context, stylesheets and how components appear in the designer's toolbox. #### Creating your own events An event is a name plus a value that a piece of UI hands to whoever uses it. There are three ways to make one, from the most structured to the simplest. ##### 1. In a component: `events` and `emit` Declare the events and raise them with `self.emit(name, value)`. The parent listens with `on_=`: ```python class ColorRow(pz.Component): colors: list = () selected: str = "" events = { "pick": "A color was clicked (event.value is the color).", "remove": "The × of a color was pressed (event.value is the color).", } def render(self): return pz.row([ pz.row([ ui.color_swatch(c, size=28 if c == self.selected else 20, on_press=lambda c=c: self.emit("pick", c)), self.handles("remove") and ui.icon_button("x", lambda c=c: self.emit("remove", c), size="xs"), ], spacing=2) for c in self.colors ], spacing=8) ColorRow(colors=self.palette, selected=self.color, on_pick=lambda e: setattr(self, "color", e.value), on_remove=lambda e: self.palette.remove(e.value)) ``` - `events` is a dict `{name: description}` (the description shows in the designer and the reference) or a tuple of names. Subclasses add to the events they inherit. - The handler gets a `pz.Event` with `type` (the name), `value` and `source` (the component). A handler without arguments (`on_pick=self.refresh`) is called with nothing. - `emit` of an undeclared name raises an error, so typos show up at once. - Every component also emits `mount` and `unmount`. ##### 2. In a function: a callback parameter The components of `pyzed.ui` are plain functions with `on_*` parameters. Yours can be too: take the callback as a parameter and give it to a widget, or wrap it to pass a value. ```python def tag_list(tags, on_remove=None, on_add=None): """Tags as chips. on_remove(tag) when a chip's × is pressed, on_add() for the + button.""" return pz.row([ *[ui.chip(t, on_remove=on_remove and (lambda t=t: on_remove(t))) for t in tags], on_add and ui.icon_button("plus", on_add, tooltip="Add a tag"), ], spacing=6) tag_list(self.tags, on_remove=self.tags.remove, on_add=self.new_tag) ``` ##### 3. App-wide: messages and `pz.post` A message is an event for the whole app: any value that reaches `update`. Use one when the same thing can come from several places (a button, a shortcut, a menu, a timer, a worker thread): ```python from dataclasses import dataclass @dataclass class Downloaded: # a message with data name: str size: int def worker(url): # runs in a thread data = fetch(url) pz.post(Downloaded(url, len(data))) # any thread can send a message to update() class App(pz.App): def update(self, message): match message: case Downloaded(name, size): self.files.append((name, size)) ``` In Pyzed Design, a designed component's **custom events** are added in its *Events* card and raised from its code with `self.emit("change", value)`. See [Component designer](https://albakore.github.io/pyzed/editor/component-designer/). #### Where next - [Events](https://albakore.github.io/pyzed/guides/events/): the common events of every widget and the `pz.Event` object. - [Events of every component](https://albakore.github.io/pyzed/library/events/): how many events each component has, and which. - [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/): the same app written three ways. - [App architecture](https://albakore.github.io/pyzed/guides/architecture/): the view → message → update loop in detail. ### Imperative, declarative and functional > Three ways to write a Pyzed app: methods that change the state, messages handled by one update(), or an immutable state drawn by pure functions. The same to-do list written each way, and how to manage state in classes. Source: https://albakore.github.io/pyzed/guides/code-styles/ Pyzed does not force one way of writing an app. A handler can be a method, a message or a function that returns a new state, and all three mix in the same program. This page writes the same to-do list in each style, so you can compare them line by line, and says when each one fits. | Style | The state lives in | An event | Fits | | --- | --- | --- | --- | | **Imperative** | Attributes of the app (`self.tasks`). | Calls a method that changes them (`on_submit=self.add`). | Small apps, prototypes, designed forms. | | **Declarative** | Attributes of the app. | Produces a message (`on_submit=Add()`); one `update()` changes the state. | Apps with many ways to do the same thing, logging, undo, tests without a window. | | **Functional** | One immutable value (`State`). | Asks for a new state (`set_state(draft="")`); the view is a pure function. | Logic you want to test as plain functions, reusable view functions, caching with `@pz.memo`. | In the three of them the view is **declarative**: `view()` describes the whole window from the state, and Pyzed works out what changed. The styles differ in how events reach the state. #### The same app three ways **Imperative:** `examples/code_styles/imperative.py`: ```python """The same to-do list three ways (1 of 3): imperative. The state is plain attributes of the app and every event calls a method that changes them. See docs/guides/code-styles.md; the other two are declarative.py and functional.py. python examples/code_styles/imperative.py """ import pyzed as pz from pyzed import ui class Todo(pz.App): title = "Tasks" size = (420, 360) def __init__(self): self.tasks = [{"title": "Write the docs", "done": False}] self.draft = "" self.filter = "All" # -- event handlers: each one changes the state -------------------------- def type(self, text): self.draft = text def add(self): if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = "" def toggle(self, task, done): task["done"] = done def remove(self, task): self.tasks.remove(task) # -- the view reads the state ---------------------------------------------- def view(self): shown = [t for t in self.tasks if self.filter == "All" or t["done"] == (self.filter == "Done")] return pz.column([ ui.input(self.draft, self.type, placeholder="New task", on_submit=self.add), ui.segmented(["All", "To do", "Done"], self.filter, lambda f: setattr(self, "filter", f)), *[pz.row([ ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"]), pz.hspace(), ui.icon_button("trash-2", lambda t=t: self.remove(t), size="sm"), ], align="center") for t in shown], ], spacing=8, padding=16) if __name__ == "__main__": pz.run(Todo) ``` **Declarative:** `examples/code_styles/declarative.py`: ```python """The same to-do list three ways (2 of 3): declarative. Every event is a message (a small dataclass) and one ``update()`` decides what each one does. See docs/guides/code-styles.md; the other two are imperative.py and functional.py. python examples/code_styles/declarative.py """ from dataclasses import dataclass import pyzed as pz from pyzed import ui # -- the messages: everything that can happen ------------------------------------ @dataclass class Type: text: str @dataclass class Add: pass @dataclass class Toggle: index: int done: bool @dataclass class Remove: index: int @dataclass class Filter: name: str class Todo(pz.App): title = "Tasks" size = (420, 360) def __init__(self): self.tasks = [{"title": "Write the docs", "done": False}] self.draft = "" self.filter = "All" # -- one place decides how each message changes the state ------------------ def update(self, message): match message: case Type(text): self.draft = text case Add() if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = "" case Toggle(index, done): self.tasks[index]["done"] = done case Remove(index): del self.tasks[index] case Filter(name): self.filter = name # -- the view only describes the window and the messages ------------------- def view(self): rows = [(i, t) for i, t in enumerate(self.tasks) if self.filter == "All" or t["done"] == (self.filter == "Done")] return pz.column([ ui.input(self.draft, Type, placeholder="New task", on_submit=Add()), ui.segmented(["All", "To do", "Done"], self.filter, Filter), *[pz.row([ ui.checkbox(t["done"], lambda done, i=i: Toggle(i, done), label=t["title"]), pz.hspace(), ui.icon_button("trash-2", Remove(i), size="sm"), ], align="center") for i, t in rows], ], spacing=8, padding=16) if __name__ == "__main__": pz.run(Todo) ``` **Functional:** `examples/code_styles/functional.py`: ```python """The same to-do list three ways (3 of 3): functional. The state is one frozen dataclass, the view and the logic are pure functions, and the only class keeps the current state. See docs/guides/code-styles.md; the other two are imperative.py and declarative.py. python examples/code_styles/functional.py """ from dataclasses import dataclass, replace import pyzed as pz from pyzed import ui @dataclass(frozen=True) class Task: title: str done: bool = False @dataclass(frozen=True) class State: # the whole state, immutable tasks: tuple = (Task("Write the docs"),) draft: str = "" filter: str = "All" # -- pure functions: they change nothing, they return new values ----------------- def added(tasks, title): return tasks + (Task(title.strip()),) if title.strip() else tasks def toggled(tasks, index, done): return tuple(replace(t, done=done) if i == index else t for i, t in enumerate(tasks)) def removed(tasks, index): return tasks[:index] + tasks[index + 1:] def visible(tasks, filter): return [(i, t) for i, t in enumerate(tasks) if filter == "All" or t.done == (filter == "Done")] # -- the view: a pure function of the state --------------------------------------- def task_row(task, on_toggle, on_remove): # a function component return pz.row([ ui.checkbox(task.done, on_toggle, label=task.title), pz.hspace(), ui.icon_button("trash-2", on_remove, size="sm"), ], align="center") def view(state, set_state): return pz.column([ ui.input(state.draft, lambda text: set_state(draft=text), placeholder="New task", on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft="")), ui.segmented(["All", "To do", "Done"], state.filter, lambda f: set_state(filter=f)), *[task_row(t, lambda done, i=i: set_state(tasks=toggled(state.tasks, i, done)), lambda i=i: set_state(tasks=removed(state.tasks, i))) for i, t in visible(state.tasks, state.filter)], ], spacing=8, padding=16) # -- the only class: it keeps the current state ------------------------------------ class Todo(pz.App): title = "Tasks" size = (420, 360) state = State() def set_state(self, **changes): self.state = replace(self.state, **changes) def view(self): return view(self.state, self.set_state) if __name__ == "__main__": pz.run(Todo) ``` Run any of them with `python examples/code_styles/imperative.py` (or `declarative.py`, `functional.py`): the three windows look and behave the same. #### Imperative: methods change the state Every handler is a method or a lambda, and it changes `self`. Pyzed calls `view()` again afterwards. ```python def add(self): if self.draft.strip(): self.tasks.append({"title": self.draft.strip(), "done": False}) self.draft = "" ui.input(self.draft, self.type, on_submit=self.add) ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"]) ``` - A handler that takes no argument is called with nothing (`on_press`, `on_submit`); one of a control that produces a value gets it (`on_input(text)`, `on_toggle(done)`, `on_select(item)`). - To pass your own data, bind it with a default argument: `lambda t=t: self.remove(t)`. Without `t=t`, every lambda of the loop would see the last task. - It is the shortest style, and the one the handlers of a [designed form](https://albakore.github.io/pyzed/guides/classes/#designed-forms-pzform--handlers) use: `form.status.text = "Saved"`. #### Declarative: messages and one `update()` Handlers are **values**, the messages, which go to `update(message)`. A message says *what happened*; `update` decides *what it means*. ```python @dataclass class Type: text: str @dataclass class Toggle: index: int done: bool ui.input(self.draft, Type, on_submit=Add()) # Type(text) is built from the new text ui.checkbox(t["done"], lambda done, i=i: Toggle(i, done)) def update(self, message): match message: case Type(text): self.draft = text case Toggle(index, done): self.tasks[index]["done"] = done ``` - **A message is any value that is not callable**: a string (`"save"`), a tuple (`("delete", 3)`) or, best, a small dataclass, which reads well in `match`. - **Controls with a value** call what you give them with that value. A dataclass *class* is callable, so `on_input=Type` builds `Type(text)` and sends it to `update`. For events without a value pass an *instance*: `on_submit=Add()`. - A callable that returns a message also works: `lambda done, i=i: Toggle(i, done)`. - `update` can return a `Command` (focus, close, open a window) or start async work, like any handler. - Since every change goes through one method, you can log messages, keep a history to undo, replay them or test the logic without opening a window: ```python app = Todo() app.update(Type("Buy milk")) app.update(Add()) assert app.tasks[-1]["title"] == "Buy milk" ``` Messages also come from outside the view: shortcuts (`on_key`), timers (`pz.every(1, Tick())`), menus and threads (`pz.post(Downloaded(...))`). See [App architecture](https://albakore.github.io/pyzed/guides/architecture/#messages). #### Functional: an immutable state and pure functions The state is **one frozen dataclass**. The view is a function `view(state, set_state)` that only reads it, and the logic lives in functions that take data and return new data: ```python @dataclass(frozen=True) class State: tasks: tuple = () draft: str = "" def added(tasks, title): # pure: no self, no side effects return tasks + (Task(title.strip()),) if title.strip() else tasks def view(state, set_state): return ui.input(state.draft, lambda text: set_state(draft=text), on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft="")) class Todo(pz.App): # the only class: it keeps the current state state = State() def set_state(self, **changes): self.state = replace(self.state, **changes) def view(self): return view(self.state, self.set_state) ``` - Nothing changes in place: `set_state(...)` builds a new `State` with `dataclasses.replace`. Use tuples and `frozenset` instead of lists and sets. - Pure functions are trivial to test: `assert added((), "Milk") == (Task("Milk"),)`. - View functions are your components: `task_row(task, on_toggle, on_remove)` takes data and callbacks and returns widgets, like the functions of `pyzed.ui`. Since their arguments are plain values, they can be cached with [`@pz.memo`](https://albakore.github.io/pyzed/guides/performance/), which rebuilds them only when the arguments change. For a pure *reducer* (the Elm or Redux way), combine it with messages: `update` replaces the state with what a pure function returns. ```python def reduce(state: State, message) -> State: match message: case Type(text): return replace(state, draft=text) case Add(): return replace(state, tasks=added(state.tasks, state.draft), draft="") return state class Todo(pz.App): state = State() def update(self, message): self.state = reduce(self.state, message) def view(self): return view(self.state) # the view only sends messages ``` #### Managing state in classes Wherever the state lives, the same rules keep an app predictable: | Rule | Why | | --- | --- | | Put the initial state in `__init__` (or in class attributes for immutable defaults). | A class attribute that is a list or a dict is shared by every instance; `__init__` gives each app its own. | | Never change state inside `view()`. | `view()` runs after every event; compute what to show there, nothing else. | | Compute derived values in `view()` instead of storing them. | `shown = [t for t in self.tasks if ...]` can never go stale. | | Keep data in plain Python: dataclasses, dicts, lists. | Pyzed redraws from whatever you have; there are no observables to wrap. | | Replace lists instead of mutating them when a part of the view is cached. | `@pz.memo` compares arguments; `self.tasks = [*self.tasks, t]` is a new value. | | State of one window goes in its `Window`; shared state in the `App` (`self.app`). | Every window has its own `view()`, and reads the app through `self.app`. | | State of a reusable piece goes in a `pz.Component` (`pz.state(...)`). | Each instance keeps its own, and the parent only sees its props and events. | A component has the same three options inside: methods that change `self`, `self.emit(...)` to tell the parent, or a `render()` that only reads its props. See [Apps, windows, forms and components](https://albakore.github.io/pyzed/guides/classes/). #### Mixing styles Most real apps mix them: imperative handlers for small UI state (an open menu, the hovered row), messages for the actions that matter (save, delete, sync), and pure functions for the logic and the reusable parts of the view. ```python class Editor(pz.App): def __init__(self): self.menu_open = False # UI detail: imperative self.doc = Document() def update(self, message): # actions: messages match message: case Save(): return save(self.doc) # pure function, returns a Command def view(self): return pz.column([ toolbar(self.doc.name, on_save=Save()), # a view function ui.icon_button("menu", lambda: setattr(self, "menu_open", True)), ]) ``` Each page of the [library](https://albakore.github.io/pyzed/library/) shows the events of its component in the three styles, under **Events → Handling the events**. ### Layout and styling > Layout in Pyzed: rows, columns, containers, sizes, padding, alignment and colors to arrange native Python desktop interfaces. Source: https://albakore.github.io/pyzed/guides/layout/ #### Rows and columns (Image: Base widgets: buttons, inputs, sliders, progress and text) ```python pz.column([a, b, c], spacing=8, padding=16) # vertical pz.row([a, b, c], spacing=8, align="center") # horizontal ``` - `children` accepts nested lists and skips `None` and `False`, so you can write `self.error and ui.label(self.error, color="error")`. - A string or a number as a child becomes a `pz.text`. - `align` aligns children on the cross axis: `"start"`, `"center"`, `"end"`. #### Sizes `width` and `height` accept: | Value | Meaning | | --- | --- | | `240` | Fixed pixels. | | `"shrink"` | Just enough for the content. | | `"fill"` | All available space. | | `"fill:2"` | A share of the space: two `"fill:1"` and one `"fill:2"` split 25/25/50. | ```python pz.row([ pz.container(sidebar, width=240), pz.container(content, width="fill"), ]) ``` Push items to the edges with a flexible gap: ```python pz.row([ui.label("Title"), pz.hspace(), ui.button("Close", self.close)]) ``` :::caution[Filling children] A child with `"fill"` (for example, a horizontal `pz.rule()`) makes its column fill too. If you want the column to shrink, set `width="shrink"` on the column. ::: #### Padding `padding` accepts `8` (all sides), `(vertical, horizontal)` or `(top, right, bottom, left)`. #### Containers `pz.container` wraps a child to give it size, alignment and style: ```python pz.container( content, padding=(12, 16), width="fill", max_width=720, align_x="center", bg="#2f343e", border_color="#464b57", border_width=1, radius=8, shadow=("#00000066", (0, 4), 16), # (color, (dx, dy), blur) clip=True, ) ``` `pz.center(content)` centers it in all the available space. `pz.container(None, width=8, height=8, bg=...)` is a colored rectangle. #### Colors Colors accept `"#rgb"`, `"#rrggbb"`, `"#rrggbbaa"`, tuples `(r, g, b)` / `(r, g, b, a)` or `"transparent"`. In `pyzed.ui` components you can also use a theme token (`"accent"`, `"text_muted"`…). For derived colors: ```python from pyzed.theme import alpha, mix alpha(self.resolved_theme().accent, 0.2) # 20% opacity mix(self.resolved_theme().editor, "#ffffff", 0.05) # 5% lighter ``` #### Text ```python pz.text("Hello", size=16, weight="semibold", color="#dce0e5") pz.text("code", font="mono") pz.text("a very long line…", wrap=False) ``` Bundled fonts: IBM Plex Sans (`"sans"`, the default), IBM Plex Mono (`"mono"`) and Lucide (`"icons"`). See [icons and fonts](https://albakore.github.io/pyzed/theming/icons-fonts/). #### Overlays and scrolling - `pz.stack([below, above])` layers widgets; `pz.opaque(w)` stops clicks from passing through. - `pz.scrollable(content, direction="vertical" | "horizontal" | "both")` adds scrolling. With `anchor_bottom=True` it stays pinned to the end. - `pz.hover(base, overlay)` shows `overlay` only on mouse hover. - `pz.mouse_area(w, on_right_press=..., on_double_click=..., cursor="pointer")` detects mouse events. #### Grids `pz.grid(children, columns=2)` still gives a uniform grid. Pass **tracks** instead and it becomes a CSS-like grid: each track is a pixel size, a share of the free space (`"1fr"`, `"2fr"`) or `"auto"` (as big as its content). Tracks can be a list or a string. (Image: Track grid, flex rows and a dock) ```python pz.grid( [ pz.cell(header, column=0, row=0, column_span=3), pz.cell(sidebar, column=0, row=1, row_span=2), main, details, pz.cell(pz.text("centered"), align_x="center", align_y="center"), ], columns=[140, "1fr", "auto"], # or "140 1fr auto" rows="44 1fr 1fr", spacing=8, # row_spacing= for a different vertical gap height=240, ) ``` `pz.cell` places a child in a given column/row, makes it span several tracks and aligns it inside its cell. Children without a position fill the free cells row by row. #### Flex `pz.flex` lays children out along one axis, like CSS flexbox: ```python pz.flex([a, b, c], justify="between", align="center", gap=6) pz.flex([a, b], direction="column", justify="end") pz.flex([pz.flex_item(a), pz.flex_item(b, grow=2), pz.flex_item(c, basis=120)], gap=6) ``` `justify` is `"start"`, `"center"`, `"end"`, `"between"`, `"around"` or `"evenly"`. `pz.flex_item` sets how much of the free space a child takes (`grow`), or a fixed size (`basis`). `wrap=True` wraps a row onto new lines. #### Boxes `pz.box` is a container for anything, like an HTML `div`. It lays out its children and takes every [style property](https://albakore.github.io/pyzed/theming/styles/), classes and an id, so a whole screen can be built from boxes: ```python pz.box( pz.text("Plans").style(size=20, weight="bold"), pz.box([plan_card(p) for p in plans], display="grid", columns=3, gap=12), pz.box(ui.button("Cancel", cancel), ui.button("Continue", go), display="flex", justify="end", gap=8), ui.badge("New").style(position="absolute", top=8, right=8), class_="page", padding=24, gap=16, overflow="scroll", ) ``` - `display`: - `"block"` (the default) puts the children one under another, as wide as the box. - `"flex"` lays them along `direction` with `justify`, `align`, `gap` and `wrap`, like `pz.flex`. - `"grid"` uses `columns` and `rows`: a count of equal columns or tracks such as `"200 1fr auto"`. - `"stack"` puts them on top of each other. - `align` takes `"start"`, `"center"`, `"end"` or `"stretch"`. - `overflow` takes `"visible"`, `"hidden"` (clips the content) or `"scroll"` (also `"scroll_x"` or `"scroll_y"`). - A child styled with `position="absolute"` leaves the flow. It is placed by `top`/`right`/`bottom`/`left` inside the box, above the rest, with a higher `z_index` on top. - `class_` and `id` apply the [stylesheet](https://albakore.github.io/pyzed/theming/styles/#stylesheets) rules (a rule for `"box"` styles every box). `style=` and the keyword properties go on top, like an inline `style` attribute. #### Anchored layouts `pz.anchor_layout` places children freely, like a form made in Pyzed Design, and each child follows its **anchor** when the window is resized: it keeps its distance to the edges it is anchored to, stretches between opposite edges and stays centered otherwise. ```python pz.anchor_layout([ pz.anchor(ui.heading("Report"), x=16, y=12, width=368, height=28, anchor="top_wide"), pz.anchor(report_body, x=16, y=48, width=368, height=192, anchor="full", max_width=720), pz.anchor(ui.button("Close", on_close), x=284, y=252, width=100, height=32, anchor="bottom_right"), ], design=(400, 300)) ``` `x`, `y`, `width` and `height` are in the `design` size (by default the smallest size that holds every child). `anchor` is one of the 16 presets of `pyzed.anchors.PRESETS` (`"top_left"`, `"top_center"`, `"top_right"`, `"top_wide"`, `"center_left"`, `"center"`... `"bottom_wide"`, `"left_tall"`, `"center_tall"`, `"right_tall"` and `"full"`), words such as `"left right bottom"`, or a tuple of edges. `min_width`, `min_height`, `max_width` and `max_height` limit the size when a child stretches; `pivot` (`0` to `1` on each axis) is the point that stays put when a limit stops it, and that `rotate` (degrees clockwise) turns around. `pyzed.anchors.place` does the math on its own if you place boxes yourself. #### Docks and resizable panels `pz.resizable(content, edge="right", size=240)` gives a panel a handle on one edge that the user can drag. `min_size`/`max_size` limit it and `on_resize(size)` reports the new size when the drag ends. `ui.panel` and `ui.workspace` use it, so their side panels can be resized out of the box, and `pz.split` keeps the ratio the user drags to. `pz.Dock` is an editor-style dock: a center area with panels on its sides. Drag a tab onto a drop zone (or right-click it) to move a panel to another side, and drag a panel's inner edge to resize it. ```python pz.Dock( center=editor, panels=[ pz.DockPanel("project", "Project", tree, icon="folder", side="left"), pz.DockPanel("outline", "Outline", outline, icon="list-tree", side="left"), pz.DockPanel("log", "Log", log, icon="terminal", side="bottom"), ], layout=self.layout, # restore a saved layout on_layout=lambda e: setattr(self, "layout", e.value), ) ``` `on_layout` receives a plain dict (positions, sizes, active tabs and hidden sides) you can save and pass back as `layout=`. See [`examples/layout.py`](https://github.com/albakore/pyzed/blob/main/examples/layout.py). ### Commands and tasks > Pyzed commands: focus fields, close the app, edit the code editor, run background work and send messages from threads in Python GUIs. Source: https://albakore.github.io/pyzed/guides/commands/ A [`Command`](https://albakore.github.io/pyzed/reference/command/) is an effect the runtime carries out for you. Return it from `update`, `on_start`, `on_key` or any callback. ```python from pyzed import Command ``` | Command | Effect | | --- | --- | | `Command.focus(id)` | Focuses the field with that `id`. | | `Command.exit()` | Closes the application. | | `Command.set_editor_text(id, text)` | Replaces an editor's text. | | `Command.set_editor_cursor(id, line, col)` | Moves an editor's cursor (1-based). | | `Command.scroll_to_end(id)` | Scrolls a `scrollable` to the end. | | `Command.scroll_to(id, x, y)` | Scrolls a `scrollable` to a position in pixels. | | `Command.copy(text)` | Copies to the clipboard. | | `Command.paste(then)` | Reads the clipboard and calls `then(text)`. | | `Command.paste_image(then)` | Reads an image from the clipboard and calls `then(png)` with its PNG bytes (`None` if there is none). | | `Command.pick_files(then, ...)` | Opens the system's file dialog and calls `then(paths)`; `filters=[("Images", ["png", "jpg"])]`, `multiple=False`, `folder=True`. | | `Command.message(msg)` | Sends `msg` to `update` on the next cycle. | | `Command.perform(fn, then)` | Runs `fn()` in the background (a thread, or the asyncio loop if it is `async def`) and passes the result to `then`. | | `Command.batch([...])` | Several at once (returning a list works too). | | `Command.open_window(w)`, `close_window`, `drag_window`… | Control windows. See [multiple windows](https://albakore.github.io/pyzed/guides/windows/#window-commands). | #### Focusing a field ```python def open_search(self): self.searching = True return Command.focus("search") def view(self): return ui.input(self.query, self.set_query, id="search") if self.searching else ... ``` #### Background work `Command.perform` runs a slow function in the background so the interface doesn't freeze and, when it finishes, calls `then(result)` on the UI thread. If the function is `async def`, it runs on Pyzed's asyncio loop; if it returns a generator, `then` is called with each value. For async code you can also return the coroutine directly: see [async and subscriptions](https://albakore.github.io/pyzed/guides/async/). ```python import json import urllib.request class Repos(pz.App): def __init__(self): self.repos, self.loading = [], False def on_start(self): return self.load() def load(self): self.loading = True return Command.perform(self.download, self.received) @staticmethod def download(): with urllib.request.urlopen("https://api.github.com/users/albakore/repos") as r: return json.load(r) def received(self, repos): self.repos, self.loading = repos, False def view(self): if self.loading: return pz.center(ui.muted("Loading…")) return pz.column([ui.list_item(r["name"], icon="book") for r in self.repos], padding=8) ``` If `fn` raises an exception, it is printed and `then` is not called. Catch it inside `fn` and return an error value if you want to show it. #### Messages from other threads `pz.post(message)` sends a message to the app from **any thread**. Use it for progress, long-running processes, sockets or timers: ```python import subprocess import threading class Build(pz.App): def __init__(self): self.lines = [] def update(self, message): kind, value = message if kind == "line": self.lines.append(value) return Command.scroll_to_end("log") def build(self): def worker(): proc = subprocess.Popen(["cargo", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) for line in proc.stdout: pz.post(("line", line.rstrip())) threading.Thread(target=worker, daemon=True).start() def view(self): return pz.column([ ui.button("Build", self.build, variant="solid"), ui.terminal(self.lines, id="log"), ], spacing=8, padding=12) ``` `pz.post` raises `RuntimeError` if no app is running. The GIL is released while the event loop runs, so your threads make progress normally. #### Closing the app ```python def on_key(self, combo): if combo == "ctrl-q": return Command.exit() ``` ### Async and subscriptions > Async in Pyzed: run coroutines, generators, timers and streams in Python desktop apps without blocking or freezing the user interface. Source: https://albakore.github.io/pyzed/guides/async/ Pyzed runs your coroutines on **a single asyncio loop** that runs in a background thread. That way you can use `aiohttp`, `httpx`, `websockets` and any async library with no setup, and the window never blocks. #### What `update` or a callback can return ```mermaid sequenceDiagram participant UI as UI thread (Iced) participant App as update() participant Aio as asyncio loop UI->>App: click → message App-->>Aio: return coroutine / generator Note over UI: the window keeps responding Aio-->>App: result → update(result) App->>UI: view() Aio-->>App: each yielded value → update (async generator) App->>UI: view() ``` | Return value | What Pyzed does | | --- | --- | | `None` | Redraws. | | a `Command` or a list of them | Runs them. | | a **coroutine** (`async def`) | Runs it on the loop and sends its result to `update`. | | a **generator** | Iterates it in a thread and sends each value to `update`. | | an **async generator** | Iterates it on the loop and sends each value to `update`. | | any other value | Sends it to `update` as a message. | `None` values yielded by a generator are ignored. #### A request: coroutine or `Command.perform` ```python import httpx class Repos(pz.App): def __init__(self): self.repos, self.loading = [], False def on_start(self): return self.load() async def load(self): # returning a coroutine runs it self.loading = True async with httpx.AsyncClient() as client: r = await client.get("https://api.github.com/users/albakore/repos") return ("loaded", r.json()) # goes to update() def update(self, message): kind, data = message if kind == "loaded": self.repos, self.loading = data, False ``` :::caution[State and coroutines] The coroutine body runs on the asyncio loop, not on the UI thread. Changing `self` from there works for simple things, but the safest approach is to **return** the result and modify state in `update` or in the `then` callback. ::: With `Command.perform(function, then)` the result goes to a specific callback instead of `update`. `function` can be a regular function (runs in a thread) or `async def` (runs on the loop): ```python async def fetch_quote(): await asyncio.sleep(1.2) return "Readability counts." class Quotes(pz.App): def load(self): self.loading = True return pz.Command.perform(fetch_quote, self.loaded) def loaded(self, quote): # on the UI thread self.quote, self.loading = quote, False ``` `then` is called even when `function` returns nothing (with `None`), so it also works as "tell me when it's done": `Command.perform(db.migrate, lambda _: self.reload())`. #### Progress: async generators Each `yield` reaches `update` while the work keeps going in the background: ```python async def download(index, size): received = 0 while received < size: await asyncio.sleep(0.05) received = min(size, received + 100_000) yield index, "progress", received / size yield index, "finished", size class Downloads(pz.App): def start(self, item): item.state = "running" return download(item.index, item.size) # returning the generator starts it def update(self, message): index, event, value = message if event == "progress": self.downloads[index].progress = value ``` To start several at once, wrap them in `Command.perform`: `return [pz.Command.perform(self.start(d)) for d in self.downloads]`. #### Subscriptions: timers and streams `subscriptions()` returns the message sources that should be **active right now**. Pyzed re-evaluates it after every `update`: a subscription exists as long as you return it and stops as soon as it no longer appears. ```python class Stopwatch(pz.App): def __init__(self): self.running, self.elapsed = False, 0.0 def subscriptions(self): return [pz.every(0.01, "tick")] if self.running else [] def update(self, message): if message == "tick": self.elapsed += 0.01 ``` | Function | What it does | | --- | --- | | `pz.every(seconds, message)` | Sends `message` every `seconds` (like Iced's `time::every`). If `message` is a callable, it is called. | | `pz.listen(function)` | Runs `function()` (a generator or async generator) while the subscription is active and sends each value to `update`. | ```python async def feed(): async with websockets.connect("wss://example.com/prices") as ws: async for message in ws: yield json.loads(message) class Ticker(pz.App): def subscriptions(self): return [pz.listen(feed)] if self.connected else [] ``` Subscriptions are identified by a **key** (by default, the message or the function). If you need two identical ones with different parameters, pass `key="..."`. :::tip[Animations] `ui.spinner()` and other clock-driven drawings only move when the view is redrawn. Keep a `pz.every(1 / 30, lambda: None)` subscription active while they are shown. ::: #### Utilities - `pz.spawn(coroutine)` schedules a coroutine on Pyzed's loop without waiting for its result (returns a `concurrent.futures.Future`). - `pz.asyncio_loop()` returns the loop, in case a library needs it. - `pz.post(message)` still works from any thread. ### Services > Timers, shortcuts, file watchers, processes, background workers, web requests, the clipboard, settings, file dialogs, notifications and tray icons for Python desktop apps with Pyzed, with events delivered on the UI thread. Source: https://albakore.github.io/pyzed/guides/services/ **Services** are the parts of an app that raise events but draw nothing: `pz.Timer`, `pz.Shortcut`, `pz.FileWatcher`, `pz.Process`, `pz.Worker`, `pz.HttpRequest`, `pz.Clipboard`, `pz.Settings`, `pz.FileDialog`, `pz.Notification` and `pz.TrayIcon`. They are the same objects as the [non-visual components](https://albakore.github.io/pyzed/editor/non-visual-components/) of Pyzed Design, and they work in a `pz.App`, in a form, or in a script with no window at all. ```python from pathlib import Path import pyzed as pz from pyzed import ui class Inbox(pz.App): def __init__(self): self.files = sorted(p.name for p in Path("inbox").glob("*")) self.watcher = pz.FileWatcher("inbox", on_change=self.refresh) self.watcher.start() def refresh(self, changed): # runs on the UI thread self.files = sorted(p.name for p in Path("inbox").glob("*")) def view(self): return pz.column([ui.list_item(name, icon="file-text") for name in self.files], padding=16) pz.run(Inbox) ``` (Image: A File watcher listing the files created and deleted in a folder) #### Handlers and threads Give handlers as `on_=` keywords, or later with `service.on("event", handler)` (`None` removes one). A handler with a parameter receives the event's value; one without parameters is just called. Each class lists its events in `Service.events`. Watchers, processes, workers and web requests do their work in **background threads**, but their events reach the handlers on **the app's thread** (through `pz.post`), so a handler can change the app's state and return commands like any callback. With no app running, in a script or a test, the handler is called right away in the thread that raised the event. `service.enabled = False` mutes every event; `stop()` ends what the service is doing. #### The services | Class | Raises | What to know | | --- | --- | --- | | `pz.Timer(interval=1000, repeat=True)` | `tick` (the count) | Milliseconds, like Visual Basic and Qt. Return `timer.subscriptions()` from `App.subscriptions`. `start()`, `stop()`, `running`. | | `pz.Shortcut("mod-s")` | `activate` (the combo) | Call `shortcut.handle(combo)` from `on_key`. `"Ctrl+S"`, `"ctrl-s, cmd-s"` and `mod-` (Cmd on macOS, Ctrl elsewhere) all work. | | `pz.FileWatcher(path, pattern="*", recursive=False, interval=500)` | `created`, `modified`, `deleted` (a path), `change` (the list) | Compares file times and sizes every `interval` ms, so it works the same everywhere, network drives included. | | `pz.Process(command, directory=None, env=None, shell=False)` | `started`, `output`, `error_output` (a line), `finished` (exit code), `failed` | `start()`, `write(text)`, `kill()`, `wait()`, `running`, `exit_code`. A string command is split like a shell would. | | `pz.Worker(work)` | `progress`, `done` (the result), `failed` (the exception) | `run(value)` calls `work(value)` in a thread; it can `report(progress)` and check `cancelled`. | | `pz.HttpRequest(url, method="GET", headers=None, body=None, timeout=30)` | `response` (an `HttpResponse`), `failed` | `send()`; `send(json={...})` sends JSON with POST. Error statuses are responses too: check `response.ok`. | | `pz.Clipboard(watch=False, interval=500)` | `change`, `paste` (the text) | `copy(text)` and `paste()` return commands. With `watch`, return `subscriptions()` from your app. | | `pz.Settings(name, folder=None, defaults=None)` | `change` (`(key, value)`) | A dict saved as JSON at once (`autosave`), per user, in Pyzed's settings folder. | | `pz.FileDialog(mode="open", filters=..., directory=None, file_name="")` | `chosen` (path or list), `cancelled` | `show()` returns the command that opens it; modes `open`, `open_many`, `save`, `folder`. | | `pz.Notification(title, text)` | `shown`, `failed` | `show(text=None)` returns whether it could be shown. | | `pz.TrayIcon(icon=None, tooltip="", menu=())` | `click`, `double_click` (Windows), `menu` (the item), `failed` | `show()` returns whether it could; `hide()`. Changes to `icon`, `tooltip` and `menu` show at once. | | `pz.SystemInfo(interval=0, disk_path=None)` | `update` (a dict) | `os`, `os_version`, `hostname`, `user`, `cpu_count`, `language`, `memory_total`, `memory_available`, `disk_free`; `folder("downloads")`, `env(name)`. | | `pz.Launcher(target="")` | `opened`, `failed` (the target) | `open(target)` with the system's program (files, folders, links, `mailto:`), `reveal(path)`, `email(to, subject, body)`. | | `pz.NetworkMonitor(host="1.1.1.1", port=53, interval=5000)` | `change` (bool), `online`, `offline` | Tries a connection every `interval` ms in the background; `online` holds the last answer. | | `pz.PowerMonitor(interval=30000, low_level=20)` | `change` (`(percent, plugged)`), `low`, `plugged`, `unplugged` | `percent` is `None` on computers without a battery (`has_battery`). | ##### A timer and a shortcut ```python import time import pyzed as pz class Clock(pz.App): def __init__(self): self.now = "" self.clock = pz.Timer(1000, on_tick=self.tick) self.quit = pz.Shortcut("mod-q", on_activate=lambda: pz.Command.exit()) def tick(self): self.now = time.strftime("%H:%M:%S") def subscriptions(self): return self.clock.subscriptions() def on_key(self, combo): return self.quit.handle(combo) def view(self): return pz.text(self.now, size=48) ``` ##### A command and its output ```python self.log = [] self.build = pz.Process("cargo build --release", directory="engine", on_output=self.log.append, on_error_output=self.log.append, on_finished=lambda code: self.done(code == 0)) self.build.start() ``` ##### Work in the background ```python def resize_all(paths): for i, path in enumerate(paths): if worker.cancelled: return None shrink(path) worker.report((i + 1) * 100 // len(paths)) return len(paths) worker = pz.Worker(resize_all, on_progress=self.set_progress, on_done=self.finished) worker.run(photos) ``` ##### A web API ```python api = pz.HttpRequest("https://api.github.com/repos/albakore/pyzed", on_response=self.show_stars) api.send() def show_stars(self, response): if response.ok: self.stars = response.json()["stargazers_count"] ``` ##### Settings and dialogs ```python prefs = pz.Settings("my-app", defaults={"theme": "One Dark"}) prefs["theme"] = "One Light" # saved at once prefs.get("theme") # "One Light", next time too dialog = pz.FileDialog("open", filters="Images: png, jpg", on_chosen=self.load_image) def open_clicked(self): return dialog.show() # a command: return it ``` ##### The operating system The services of `pyzed.system` (also at the top level: `pz.SystemInfo`, `pz.Launcher`...) read the computer and open things with the programs the user already has: ```python info = pz.SystemInfo() f"{info.os} {info.os_version}, {info.cpu_count} cores" # "Windows 11, 8 cores" info.folder("downloads") # the user's Downloads folder info.disk_free // 2**30 # free GB launcher = pz.Launcher(on_failed=lambda target: print("Could not open", target)) launcher.open("https://example.com") # the default browser launcher.open("report.pdf") # the PDF viewer launcher.reveal("report.pdf") # its folder, with the file selected network = pz.NetworkMonitor(on_offline=lambda: print("Offline"), on_online=lambda: print("Back")) network.start() ``` For files, `ui.file_input` is a path field with a button that opens the system's dialog, and `ui.DropZone` takes files dragged from the desktop. A window also gets every dropped file in `on_file_drop(path)` (`on_file_hover` and `on_file_leave` while they are dragged over it). See the [System](https://albakore.github.io/pyzed/library/drop-zone/) pages of the library. ##### An icon in the tray `pz.TrayIcon` puts an icon in the notification area on Windows, the menu bar on macOS and the panel on Linux, with a tooltip and a menu. The menu is a list, or text with one item per line: `-` is a separator, `[x]` and `[ ]` make check items that flip when clicked, and indented lines make a submenu. ```python self.tray = pz.TrayIcon("icon.png", "Backups", menu=""" Open [x] Pause Speed Slow Fast - Quit """, on_click=self.open, on_menu=self.menu) self.tray.show() def menu(self, item): if item == "Pause": self.paused = self.tray.checked("Pause") elif item == "Quit": return pz.Command.exit() ``` `pz.TrayItem(text, id=None, enabled=True, checked=None, items=[])` gives an item an `id` (what `on_menu` receives) or disables it. Without an `icon` it shows a blue dot. On Linux the icon is a StatusNotifierItem, without GTK: KDE, Cinnamon, XFCE, MATE, LXQt and most panels show it, and GNOME does with the AppIndicator extension. `pz.TrayIcon.supported()` says whether the desktop shows tray icons; where it does not, `show()` returns `False` and `failed` says why. #### Without a window Nothing here needs a running app. Scripts and tests can wait for the result: ```python import pyzed as pz process = pz.Process(["git", "status", "--short"], on_output=print) process.start() process.wait() # blocks until it ends; returns the exit code request = pz.HttpRequest("https://example.com") request.send() print(request.wait(10).status) # 200 ``` `Process.wait()`, `Worker.wait()` and `HttpRequest.wait()` block until the work ends (with an optional `timeout`) and return the exit code, the result or the response. ### Multiple windows > Multiple windows in Pyzed: open, close, move, resize and control native desktop windows from Python with pz.Window, each with its own theme. Source: https://albakore.github.io/pyzed/guides/windows/ An app can have as many windows as you want. The main window is your `App`; the others are subclasses of `pz.Window`, each with its own `view()`, title, size and theme. (Image: Multiple windows) ```python import pyzed as pz from pyzed import ui class Settings(pz.Window): title = "Settings" size = (420, 300) theme = "One Light" # optional: defaults to the app's theme def view(self): return pz.container( pz.column([ ui.heading("Settings", level=2), ui.field("Autosave", ui.switch(self.app.autosave, self.app.set_autosave)), ui.button("Close", self.close), ], spacing=12), padding=20, ) class Main(pz.App): def __init__(self): self.autosave = True self.settings = Settings() def set_autosave(self, value): self.autosave = value def open_settings(self): if not self.settings.is_open: return self.settings.open() return pz.Command.focus_window(self.settings) def view(self): return pz.center(ui.button("Open settings", self.open_settings, icon="settings")) pz.run(Main) ``` #### How it works ```mermaid flowchart LR subgraph APP["App (shared state)"] ST[("self.*")] end M["Main window"] -->|view| ST W1["Window A"] -->|"view / self.app"| ST W2["Window B"] -->|"view / self.app"| ST M -->|"return Window().open()"| W1 W1 -->|"events"| U["update()"] W2 -->|"events"| U M -->|"events"| U U --> ST ``` - `window.open()` (or `Command.open_window(window)`) returns the command that opens it. - `self.app` points to the app inside each window, so they **share state**: callbacks like `self.app.save` work directly. - Messages produced in a window go to its `update()`, which by default forwards them to `app.update()`. - `window.is_open` tells whether it is open. Closing the main window ends the app. #### Options Class or instance attributes: | Attribute | Description | | --- | --- | | `title` | Text or callable. | | `size`, `min_size`, `max_size` | Sizes in logical pixels. | | `position` | `"center"` or `(x, y)`. | | `resizable`, `decorations` | `decorations=False` removes the system frame entirely. | | `frame` | `"custom"` draws the title bar, window buttons and menus in Pyzed (see [Window frames](#window-frames-your-own-title-bar)). | | `transparent`, `maximized`, `fullscreen` | Initial state. | | `scale` | Interface scale. | | `theme` | A `Theme`, a name or `themes.auto(...)`. `None` uses the app's theme. | | `transition`, `exit_transition` | How it animates when it opens and closes: `"fade"`, `"zoom"`, `"pop"`, `"slide_up"`... a preset or a `pz.Animation`. See [Animation](https://albakore.github.io/pyzed/animation/transitions/#window-transitions). | `window_size` holds the current size and `window_position` where the window is on the screen (both updated by the runtime; the position is `None` on Wayland). `window_maximized` and `window_focused` say whether it is maximized and has the keyboard focus. #### Hooks | Method | When it is called | | --- | --- | | `on_open()` | When it opens. | | `on_close()` | When the user asks to close it. Return `False` to keep it open (for example, to ask whether to save). | | `on_resize(width, height)` | When it is resized. | | `on_key(combo)` | Key presses while that window is focused. | #### Window commands They all accept the window as the last argument; without it they act on the window that produced the message. | Command | Effect | | --- | --- | | `Command.open_window(w)` / `w.open()` | Opens the window. | | `Command.close_window(w)` / `w.close()` | Closes it. | | `Command.drag_window(w)` / `w.drag()` | Starts moving it with the mouse (for custom title bars). | | `Command.minimize_window(w)` / `w.minimize()` | Minimizes it. | | `Command.toggle_maximize_window(w)` / `w.toggle_maximize()` | Maximizes or restores it. | | `Command.maximize_window(True, w)` | Maximizes it (`False` restores it). | | `Command.drag_resize_window("se", w)` | Starts resizing it from an edge or corner (for custom frames). | | `Command.show_window_menu(w)` | Shows the system's window menu (Windows only). | | `Command.set_fullscreen(True, w)` | Fullscreen. | | `Command.resize_window(width, height, w)` | Resizes it. | | `Command.move_window(x, y, w)` | Moves it. | | `Command.focus_window(w)` | Brings it to the front. | ##### Window frames: your own title bar `frame = "custom"` replaces the system's frame with one drawn by Pyzed: a title bar that moves the window (double click maximizes it), the minimize, maximize and close buttons in the style of this system, menus on the left and borders that resize the window. `frame_menus()` returns the menus. (Image: Window frames of every platform) ```python class Notes(pz.App): title = "Notes" frame = "custom" # or "macos", "windows", "gnome", "ubuntu", "kde" def frame_menus(self): return [ ui.Menu("File", [ui.MenuItem("Open…", self.open, keys="ctrl-o"), ui.SEPARATOR, ui.MenuItem("Quit", self.close)]), ui.Menu("Edit", [ui.MenuItem("Undo", self.undo, keys="ctrl-z")]), ] def view(self): return self.editor() ``` The menus sit behind a ☰ button: a click opens the first one and shows every menu's name, and moving the pointer to another name opens that one. To put more in the bar (a project picker, a Run button), call `ui.window_frame` in `view()` yourself; then the window is not framed twice: ```python def view(self): return ui.window_frame( self.editor(), menus=self.frame_menus(), start=[ui.title_button("pyzed", chevron=True)], end=[ui.button("Run", self.run, icon="play", size="sm")], ) ``` | Value of `frame` | What you get | | --- | --- | | `None` (default) or `"system"` | The system's frame. | | `"custom"` or `True` | Pyzed's title bar with this system's buttons. On macOS the system keeps drawing its own traffic lights over the bar. | | `"macos"`, `"windows"`, `"gnome"`, `"ubuntu"`, `"kde"` | That platform's buttons on every system. | `window_maximized` and `window_focused` say whether the window is maximized and has the focus (the runtime updates them), so the maximize button shows the restore symbol and the bar dims in the background. Maximized, the window has no border to resize. The pieces work on their own too: `ui.window_title_bar` is the bar alone (with no callbacks it only shows, which is how Pyzed Design previews forms), `ui.window_controls` the three buttons and `pz.window_edges` the borders that resize a window without decorations. For a bar made from scratch, use the commands `Command.drag_window`, `Command.drag_resize_window`, `Command.toggle_maximize_window` and `Command.show_window_menu`. **Platform notes.** On Windows the window keeps the system's shadow and rounded corners. Wayland moves and resizes the window as on X11 (the compositor does it). Under WSL Pyzed uses X11, and `"custom"` draws Windows buttons, the desktop the window shows up on. `ui.system_platform()` tells which style `"custom"` picks. ##### Draggable tabs and detachable windows `ui.tab_bar` tabs can be dragged. `on_drag(id)` says a drag started; while `dragging` names the tab, `on_drag_over(index)` reports the insertion point under the pointer (or `None` off the row) and `drop_index` draws the mark there. `on_drop(id, event)` comes when it is released, with the pointer in the window (`event.window_x`, `event.window_y`, outside it when the tab was dropped outside) and on the screen (`event.screen`). Each window's `window_position` and `window_size` say where it is, so you can open the tab in a new window, or stack it into the window under the pointer: ```python class Detached(pz.Window): size = (640, 420) def __init__(self, name, position): self.name = self.title = name if position is not None: self.position = position def view(self): return ui.tab_bar([ui.Tab.for_file(self.name)], self.name) class Editor(pz.App): def __init__(self): self.files, self.active = ["main.py", "app.py", "README.md"], "main.py" self.dragging, self.over = None, None def start(self, name): self.dragging = name def hover(self, index): self.over = index def drop(self, name, event): over, self.dragging, self.over = self.over, None, None if over is not None: # released on the row: reorder i = self.files.index(name) self.files.insert(over - (over > i), self.files.pop(i)) return None width, height = self.window_size or self.size if 0 <= event.window_x < width and 0 <= event.window_y < height: return None # released inside the window, off the row self.files.remove(name) self.active = self.files[0] if self.files else None where = (event.screen_x - 80, event.screen_y - 12) if event.screen else None return Detached(name, where).open() def view(self): return ui.tab_bar([ui.Tab.for_file(f) for f in self.files], self.active, on_select=lambda f: setattr(self, "active", f), on_drag=self.start, on_drop=self.drop, on_drag_over=self.hover, dragging=self.dragging, drop_index=self.over) ``` To stack a tab into another of your windows, compare `event.screen` with that window's `window_position` and `window_size`. Wayland does not tell apps where their windows are: there `event.screen` and `window_position` are `None`, so offer a menu command too (Pyzed Design has **Move tab to new window** and **Move tab to main window**). :::note `pz.run()` can only be called once per process (a winit limitation). For more windows, use `pz.Window`. ::: ### Drawing with canvas > Draw custom graphics in Python desktop apps with pz.canvas and pyzed.draw: shapes, text, transforms and mouse events, rendered in Rust. Source: https://albakore.github.io/pyzed/guides/canvas/ `pz.canvas` is a 2D drawing surface. Shapes are created with `pyzed.draw` and are plain dictionaries, so building them every frame is cheap. examples/game_of_life.py: thousands of cells drawn on one canvas, stepped by pz.every." /> ```python import pyzed as pz from pyzed import draw pz.canvas([ draw.background("#1e2127"), draw.circle(100, 100, 40, fill="#74ade8"), draw.line(0, 0, 200, 200, stroke="#d07277", width=2, dash=[6, 4]), draw.text(100, 180, "Hello", color="#dce0e5", align_x="center"), ], width=200, height=200) ``` Angles are in degrees, `y` grows downwards, and colors accept the same values as `pz.text`. #### Shapes (Image: Shapes drawn with pyzed.draw) | Function | Shape | | --- | --- | | `draw.background(color)` | Fills the whole canvas. | | `draw.rect(x, y, w, h, fill=, stroke=, width=, radius=, dash=)` | Rectangle (rounded with `radius`). | | `draw.rects([(x, y, w, h), ...], fill=)` | Many rectangles of one color in a single shape (fast for grids). | | `draw.circle(x, y, r, ...)` | Circle. | | `draw.line(x1, y1, x2, y2, stroke=, width=, dash=, cap=)` | Line; `cap="round"` rounds the ends. | | `draw.polyline(points, closed=False)` / `draw.polygon(points)` | Lines joining points / closed shape. | | `draw.arc(x, y, r, start, end, pie=False)` | Arc (0° = 3 o'clock, clockwise); `pie=True` closes it through the center. | | `draw.path(ops)` | Free-form path with SVG-like operations: `("M", x, y)`, `("L", x, y)`, `("Q", cx, cy, x, y)`, `("C", ...)`, `("A", cx, cy, r, start, end)`, `("Z",)`. | | `draw.text(x, y, text, color=, size=, font=, weight=, align_x=, align_y=)` | Text anchored at `(x, y)`. | | `draw.image(source, x, y, w, h, rotation=, opacity=, pixelated=)` | A bitmap (sprite), decoded once and cached. See [Making games](https://albakore.github.io/pyzed/games/). | #### Transforms `draw.translate(x, y)`, `draw.rotate(degrees)` and `draw.scale(x, y)` affect the shapes that come **after** them. Scope them with `draw.save()` / `draw.restore()`, or use `draw.group`: ```python def hand(angle, length, color): return draw.group( [draw.line(0, 0, 0, -length, stroke=color, width=3, cap="round")], x=100, y=100, rotation=angle, ) pz.canvas([draw.circle(100, 100, 90, stroke="#5c6370", width=2), *hand(self.minutes * 6, 70, "#dce0e5"), *hand(self.seconds * 6, 85, "#d07277")], width=200, height=200) ``` #### Mouse events `pz.canvas` accepts callbacks that receive coordinates relative to the canvas: ```python pz.canvas(shapes, width=400, height=300, on_press=lambda x, y: self.paint(x, y), on_move=lambda x, y: self.move(x, y), cursor="crosshair") ``` See the [`canvas` reference](https://albakore.github.io/pyzed/reference/widgets/#canvas) for the full list. #### Performance Give the canvas an `id` to cache its geometry between frames: it is only re-tessellated when the shapes change. For thousands of cells, use `draw.rects` instead of one `draw.rect` per cell. The `pyzed.ui` charts (`sparkline`, `bar_chart`, `donut`, `spinner`) are built with `canvas`: see [`python/pyzed/ui/display.py`](https://github.com/albakore/pyzed/blob/main/python/pyzed/ui/display.py) for an example. ### Events > The common events every Pyzed widget has (click, double click, drag, hover, focus, keys, resize) and how your Python handlers receive them. Source: https://albakore.github.io/pyzed/guides/events/ Any widget can react to the events that graphical editors usually offer: clicks, double clicks, hovering, dragging, focus, keys and size changes. Attach them with `.on(...)`: ```python import pyzed as pz card = pz.container(pz.text("Drag me"), padding=12, bg="#2c313a").on( click=self.select, # takes no argument: just called double_click=lambda e: self.rename(e.x), # takes one: gets a pz.Event drag=self.move, # e.dx, e.dy since the last drag event key_down=self.shortcut, # after a click gives it the focus cursor="grab", ) ``` `pz.on(widget, ...)` does the same. Calling `.on()` again merges the handlers, and `None` removes one. A handler can also be a message (any value that is not callable), which goes to `update`, like every other Pyzed callback. (Image: Events and components) #### The events | Group | Event | When | | --- | --- | --- | | Mouse | `click` | Pressed and released with the left button. | | | `double_click` | Two clicks in quick succession (after the second `click`). | | | `right_click`, `middle_click` | The right or middle button was pressed. | | | `mouse_down`, `mouse_up` | Any button (`event.button` is `"left"`, `"right"`, ...). | | | `mouse_enter`, `mouse_leave` | The pointer moved onto or off the widget. | | | `mouse_move` | The pointer moved over the widget. | | | `scroll` | The wheel turned (`event.dx`, `event.dy` in lines). | | Drag | `drag_start`, `drag`, `drag_end` | Left button held and moved a few pixels; `drag` reports the movement since the previous one. | | Focus | `focus`, `blur` | A click on the widget gives it the focus; a click elsewhere takes it away. | | Keyboard | `key_down`, `key_up` | Keys while focused (`event.key` such as `"ctrl-s"`). | | Layout | `resize` | The widget got a new size, including its first one. | `pz.events.CATALOG` has this list with descriptions (the designer shows them). #### The `Event` object | Field | Meaning | | --- | --- | | `type` | The event name. | | `x`, `y` | Pointer position relative to the widget's top-left corner (`position` gives both). | | `dx`, `dy` | Scroll amount, or drag movement since the previous `drag`. | | `width`, `height` | The widget's size (`size` gives both). | | `button` | `"left"`, `"right"`, `"middle"`... for mouse buttons. | | `key` | The key combo for key events. | | `shift`, `ctrl`, `alt`, `logo` | Modifiers held. | | `value`, `source` | For custom events: the value and the component that emitted it. | A handler gets the event only if it has a required positional parameter (or `*args`). `lambda: ...` and `lambda i=i: ...` are called with nothing, so the usual way of capturing a loop variable keeps working. #### Own events and common events Every widget reacts to events in two layers: | | Own events | Common events | | --- | --- | --- | | What | The `on_*` parameters of a component: `on_press`, `on_input`, `on_select`, `on_close`… | The 18 events of this page: `click`, `mouse_enter`, `drag`, `key_down`, `resize`… | | Which widgets | Each component its own (a button has 1, a tab bar 7, a mouse area 10). | Every widget, whatever it is. | | How | `ui.input(text, self.typed, on_submit=self.send)` | `widget.on(double_click=self.rename)` | | The handler gets | The component's value: the text, the checked state, the item, the row… | Nothing, or a `pz.Event`. | Both mix freely on the same widget: ```python ui.list_item("main.py", self.open, icon="file").on( # on_press: its own event right_click=lambda e: self.menu(e.x, e.y), # a common event mouse_enter=lambda: setattr(self, "hovered", "main.py"), ) ``` [Events of every component](https://albakore.github.io/pyzed/library/events/) lists the own events of each component, what its handler gets and how many events it can have in total. Each page of the [library](https://albakore.github.io/pyzed/library/) has an **Events** section with them, written in three styles. #### Recipes ##### Hover and click ```python class Cards(pz.App): def __init__(self): self.hovered = None self.opened = None def card(self, name): hot = self.hovered == name return pz.container(pz.text(name), padding=16, radius=8, bg="#3b4048" if hot else "#2c313a").on( mouse_enter=lambda: setattr(self, "hovered", name), mouse_leave=lambda: setattr(self, "hovered", None), click=lambda: setattr(self, "opened", name), cursor="pointer", ) def view(self): return pz.row([self.card(n) for n in ("Inbox", "Drafts", "Sent")], spacing=8) ``` :::tip For hover *colors* alone you don't need events: `pz.Style(hover={"background": "..."})` and the `hover_bg` of buttons change them without calling Python. Use `mouse_enter` / `mouse_leave` when the state matters to your app. ::: ##### A context menu where the pointer is ```python class Board(pz.App): def __init__(self): self.menu_at = None def view(self): board = pz.container(ui.muted("Right-click anywhere"), width="fill", height="fill").on( right_click=lambda e: setattr(self, "menu_at", (e.x, e.y)), # where, inside the board ) x, y = self.menu_at or (0, 0) menu = ui.menu([ui.MenuItem("New note", lambda: setattr(self, "menu_at", None), icon="plus")]) return ui.popover(board, menu if self.menu_at else None, x=x, y=y, on_dismiss=lambda: setattr(self, "menu_at", None)) ``` ##### Drag to move ```python class Desk(pz.App): def __init__(self): self.x, self.y = 40, 40 def move(self, e): # e.dx, e.dy: movement since the last drag self.x += e.dx self.y += e.dy def view(self): note = pz.container(pz.text("Drag me"), padding=12, radius=8, bg="#e5c07b", color="#282c34") return pz.stack([pz.pin(note.on(drag=self.move, cursor="grab"), x=self.x, y=self.y)], width="fill", height="fill") ``` ##### Double click to rename ```python class Rename(pz.App): def __init__(self): self.name, self.editing = "notes.py", False def view(self): if self.editing: return ui.input(self.name, lambda t: setattr(self, "name", t), id="name", on_submit=lambda: setattr(self, "editing", False)) return ui.label(self.name).on(double_click=self.start) def start(self): self.editing = True return pz.Command.focus("name") # a handler can return a command ``` ##### Ctrl + click to select several ```python class Files(pz.App): def __init__(self): self.selected = set() def pick(self, name, e): if e.ctrl or e.logo: # Ctrl (Cmd on macOS) adds to the selection self.selected ^= {name} else: self.selected = {name} def view(self): return pz.column([ ui.list_item(n, None, icon="file", selected=n in self.selected).on( click=lambda e, n=n: self.pick(n, e)) for n in ("a.py", "b.py", "c.py") ], width=240) ``` ##### Change the layout with the size ```python class Gallery(pz.App): def __init__(self): self.narrow = False def view(self): items = [ui.card(ui.muted(f"Photo {i}"), width=160) for i in range(6)] layout = pz.column(items, spacing=8) if self.narrow else pz.row(items, spacing=8, wrap=True) return pz.container(layout, width="fill").on(resize=lambda e: setattr(self, "narrow", e.width < 520)) ``` ##### Ctrl + wheel to zoom ```python class Zoom(pz.App): def __init__(self): self.zoom = 1.0 def view(self): return pz.container(pz.text("Zoom me", size=16 * self.zoom), width="fill", height="fill").on( scroll=lambda e: setattr(self, "zoom", max(0.5, min(4, self.zoom + e.dy * 0.1))) if e.ctrl else None, double_click=lambda: setattr(self, "zoom", 1.0), ) ``` ##### Focus and keys on any widget ```python class Field(pz.App): def __init__(self): self.focused, self.last_key = False, "" def view(self): border = "#74ade8" if self.focused else "#3e4452" return pz.container(pz.text(self.last_key or "Click me, then press keys"), padding=16, border_color=border, border_width=1, radius=6).on( focus=lambda: setattr(self, "focused", True), blur=lambda: setattr(self, "focused", False), key_down=lambda e: setattr(self, "last_key", e.key), ) ``` #### In the three styles A handler of `.on(...)` can be a callable or a message, like any other handler, so the common events fit the [three styles](https://albakore.github.io/pyzed/guides/code-styles/): ```python # Imperative: the handler changes self ui.card(ui.muted("Hover me"), selected=self.hovered).on( mouse_enter=lambda: setattr(self, "hovered", True), mouse_leave=lambda: setattr(self, "hovered", False)) # Declarative: the handlers are messages, update() decides @dataclass class Hover: on: bool ui.card(ui.muted("Hover me"), selected=self.hovered).on(mouse_enter=Hover(True), mouse_leave=Hover(False)) def update(self, message): match message: case Hover(on): self.hovered = on # Functional: a pure view asks for a new state def view(state, set_state): return ui.card(ui.muted("Hover me"), selected=state.hovered).on( mouse_enter=lambda: set_state(hovered=True), mouse_leave=lambda: set_state(hovered=False)) ``` A message given to `.on()` reaches `update` as it is, without the `pz.Event`. When you need the event's data, build the message in a lambda: `drag=lambda e: Moved(e.dx, e.dy)`. #### How it works The events are implemented once, in a native wrapper widget (`src/events.rs`), instead of in each component. The wrapper only observes: it never stops an event, so a button inside a card still gets its click, and the card gets one too. ```mermaid sequenceDiagram participant OS as Window system participant Area as Event wrapper (Rust) participant Child as Wrapped widget participant Py as Your handler OS->>Area: mouse, keyboard and redraw events Area->>Child: forwards every event first Area->>Area: works out click, double click, drag, focus, resize Area->>Py: pz.Event(type, x, y, ...) Py-->>Area: None, a Command or a message Note over Py: the window redraws after each handler ``` Components made with [`pz.Component`](https://albakore.github.io/pyzed/guides/components/) accept these events too (`on_click=...`), and add their own: see [creating your own events](https://albakore.github.io/pyzed/guides/classes/#creating-your-own-events). In a [designed form](https://albakore.github.io/pyzed/editor/events/), the events of each control are wired in the Events panel. ### Custom components > Build reusable Pyzed components with their own props, state, logic and events, like React components, and use them in the visual designer. Source: https://albakore.github.io/pyzed/guides/components/ A component is a class that groups widgets with their logic, like a React component: - **Props** are annotated class attributes with defaults. The parent passes them on every render. - **State** is any other attribute you set. It stays between renders. - `render()` returns the component's widgets. - `events` declares the custom events it raises with `self.emit(name, value)`. ```python import pyzed as pz class Rating(pz.Component): """Stars the user can click.""" value: int = 0 # props max: int = 5 events = {"change": "A star was picked (event.value is the rating)."} icon = "star" # how it appears in the designer's toolbox def setup(self): # once, before the first render self.hover = None # state def render(self): shown = self.hover or self.value return pz.row([ pz.text("★" if i <= shown else "☆", size=20).on( click=lambda i=i: self.emit("change", i), mouse_enter=lambda i=i: setattr(self, "hover", i), mouse_leave=lambda: setattr(self, "hover", None), ) for i in range(1, self.max + 1) ], spacing=2) ``` Calling a component returns a widget, so it goes anywhere a widget does: ```python class Review(pz.App): stars = 3 def view(self): return pz.column([ Rating(value=self.stars, on_change=lambda e: setattr(self, "stars", e.value)), pz.text(f"{self.stars} of 5"), ]) ``` (Image: Events and components) See [`examples/components.py`](https://github.com/albakore/pyzed/blob/main/examples/components.py) for a star rating, a tag editor and a draggable note. #### Events - **Custom events** are declared in `events`, as a tuple of names or a `{name: description}` dict. The parent handles them with `on_=`. - `self.emit(name, value)` calls the parent's handler with a `pz.Event` whose `value` and `source` are set. Return its result from your callback, so a `Command` or a message reaches the runtime. - **Common events**, such as `on_click`, `on_drag` and `on_key_down`, work on every component without any code. See [Events](https://albakore.github.io/pyzed/guides/events/). - **Lifecycle events**: `on_mount` and `on_unmount` tell the parent when the component appears and disappears. - **`style=`** styles the component like any widget (`Rating(value=3, style="card")`), unless it declares a `style` prop of its own. See [Styles](https://albakore.github.io/pyzed/theming/styles/). #### State and keys State can also be declared on the class with `pz.state(default)`. Unlike a prop, the parent cannot pass it, every instance starts with its own copy of the default (so lists and dicts are safe), and the designer lists it next to the props: ```python class Counter(pz.Component): step: int = 1 # prop count: int = pz.state(0) # state def render(self): return ui.button(f"{self.count}", lambda: setattr(self, "count", self.count + self.step)) ``` State is kept per position in the parent's `view()`: the second `Rating` in a view keeps its own state as long as the order stays the same. When components come from a list whose items move, give each one a stable `key`: ```python pz.column([TodoItem(key=todo.id, todo=todo) for todo in self.todos]) ``` A component that is no longer rendered is unmounted, and its state is dropped. ```mermaid flowchart LR View["App.view()"] -->|"Rating(value=3)"| Slot{"Instance at this
position or key?"} Slot -->|no| New["new instance
setup()"] Slot -->|yes| Same["same instance
(state kept)"] New --> Props["props set"] Same --> Props Props --> Changed{"props, state or
context changed?"} Changed -->|yes| Render["render() → widgets"] Changed -->|no| Last["last drawing, reused"] Render -->|first time| Mount["on_mount()"] View -.->|not rendered again| Unmount["on_unmount()"] ``` `render()` only runs when something it depends on changed: its props, its state, the context it reads, the theme or a component inside it. Otherwise the last drawing is reused, so a view with hundreds of components stays fast. If `render()` reads something else (the time, a global), call `self.refresh()` when it changes or set `cache = False` on the class. See [components drawn only when they change](https://albakore.github.io/pyzed/guides/performance/#components-drawn-only-when-they-change). #### Children, context and styles Components nest like HTML elements and React components: ```python class Card(pz.Component): title: str = "" children: list = () # the positional arguments, like JSX children stylesheet = { # CSS-like rules for this component (& is the component) "&": {"background": "panel", "padding": 16, "radius": 8, "border": (1, "border")}, "&:hover": {"border_color": "accent"}, } def render(self): return pz.box(pz.text(self.title).style(weight="bold"), *self.children, gap=8) Card(pz.text("Three new messages"), ui.button("Open", self.open), title="Inbox", class_="wide", animate="fade_in_up") ``` - A prop called `children` takes the positional arguments. Lists are flattened, and `None` and `False` are skipped. - `stylesheet` adds rules to [`pz.stylesheet`](https://albakore.github.io/pyzed/theming/styles/#stylesheets) when the class is defined. `&` stands for the component: `"&"`, `"&:hover"`, `"&.wide"`. - Every component takes `style=`, `class_=`, `id=` and `animate=` (presets for [`widget.animate`](https://albakore.github.io/pyzed/animation/)), unless it declares props with those names. - Context shares values with every component rendered inside, without passing props down each level: ```python with pz.provide(user=self.user): page = Dashboard() # any component inside reads it class Avatar(pz.Component): def render(self): return ui.avatar(pz.use_context("user").name) # or self.context("user") ``` A component can also share values with what it renders by returning them from a `provide()` method. Children passed in as arguments are rendered before the component, so they see the context around it, not its own. See [`examples/web_layout.py`](https://github.com/albakore/pyzed/blob/main/examples/web_layout.py) for a page built this way. #### Lifecycle | Hook | When | | --- | --- | | `setup()` | Once, before the first render. Props are already set. Initialize state here. | | `render()` | On every render of the parent. | | `on_mount()` | After the first render. It may return a `Command` or a message. | | `on_unmount()` | When the component is no longer rendered. | Hooks run after the view is drawn, never in the middle of it. #### In the designer Every `pz.Component` subclass appears in the [designer](https://albakore.github.io/pyzed/editor/)'s toolbox: - **Props** become properties. `bool` gets a switch, `int` and `float` a number box, `Literal[...]` a drop-down, `list` one item per line, and a prop named `color` or `icon` gets its own picker. Unions, optionals, dicts, sets and tuples (`int | None`, `dict[str, int]`...) get an editor built from the type, with a row per item. The type hints of `pyzed` pick a richer editor, like export hints in Godot: | Annotation | Editor | | --- | --- | | `pz.Color` | A color picker (theme token or `#rrggbb`). | | `pz.Icon` | An icon picker. | | `pz.Multiline` | A text area. | | `Annotated[int, pz.Range(0, 10)]` | A number box limited to 0 to 10 (`step` is optional). | They are plain `str` and `int` at run time: `tint: pz.Color = "accent"`. - **Custom events** become events you can write code for. Class attributes control how it appears: | Attribute | Default | | --- | --- | | `label` | The class name, split into words. | | `icon` | `"component"` (any Lucide icon). | | `category` | `"Custom"`. | | `size` | `(160, 40)`, the size when placed on a form. | | `designer` | `True`; `False` keeps it out of the toolbox. | | `type_name` | The class name in snake case; the id saved in forms. | The docstring's first paragraph is shown as the description. To build a component by drawing it instead of writing `render()`, use the designer's [component mode](https://albakore.github.io/pyzed/editor/component-designer/). ### Keyboard and shortcuts > Keyboard shortcuts in Pyzed apps: key bindings with on_key, and how to show the shortcuts in menus, buttons and the rest of the UI. Source: https://albakore.github.io/pyzed/guides/keyboard/ Define `on_key(self, combo)` in your app to receive **every** key press, like a global keymap. `combo` is a string such as `ctrl-shift-p` (the same format Zed keymaps use): | Key | `combo` | | --- | --- | | Ctrl+Shift+P | `"ctrl-shift-p"` | | Cmd+S (macOS) / Win+S | `"cmd-s"` | | Alt+Z | `"alt-z"` | | Esc | `"escape"` | | Arrows | `"up"`, `"down"`, `"left"`, `"right"` | | Others | `"enter"`, `"tab"`, `"space"`, `"backspace"`, `"delete"`, `"home"`, `"end"`, `"pageup"`, `"pagedown"`, `"f1"`… | Modifiers always come in this order: `ctrl-`, `alt-`, `shift-`, `cmd-`. Letters arrive in lowercase. ```python KEYMAP = { "ctrl-shift-p": "open_palette", "ctrl-p": "open_files", "ctrl-b": "toggle_left", "ctrl-s": "save", } class App(pz.App): def on_key(self, combo): # support Cmd on macOS and Ctrl elsewhere with the same table action = KEYMAP.get(combo.replace("cmd-", "ctrl-")) if action: return getattr(self, action)() ``` `on_key` can return the same things as a callback: `None`, a `Command` or a message for `update`. :::note `on_key` also receives keys already handled by a focused field (for example, the letters you type in an `input`). Only respond to the combos you care about, and check the state when a shortcut only makes sense in a given context (`if self.palette_open and combo == "escape"`). Keys used by a focused dropdown, switch, check box, slider, segmented control or radio group (the arrows, Space…) do **not** reach `on_key`; see [Controls with the keyboard focus](https://albakore.github.io/pyzed/guides/accessibility/#controls-with-the-keyboard-focus). Tab and Shift+Tab also move the focus between fields when no widget used them; see [Accessibility](https://albakore.github.io/pyzed/guides/accessibility/#moving-between-fields-with-tab). ::: #### Held keys and key releases `on_key` fires once per press. For keys that act *while* they are held, define `on_key_up(self, key)`, which receives each release by name (`"left"`, `"space"`, `"a"`, without modifiers), or ask at any moment which keys are down: - `pz.pressed()` returns the keys held right now as a `frozenset` of names. - `pz.is_pressed("left", "a")` is `True` while any of them is held. ```python class Game(pz.App): def on_key_up(self, key): if key == "space": self.charging = False def tick(self, dt): if pz.is_pressed("left", "a"): self.x -= 200 * dt ``` Key presses and releases only become messages when the app or one of its windows defines `on_key` or `on_key_up`, so an app without them does not redraw on every key. `pz.pressed()` works either way. The [games section](https://albakore.github.io/pyzed/games/#input) shows them in a frame loop, along with `pyzed.game` helpers such as `axis` and `vector`. #### Showing shortcuts - `ui.kbd("ctrl-shift-p")` draws the keys. On macOS it uses symbols (⌃⇧P), elsewhere words. - `ui.button(..., keys="ctrl-s")`, `ui.icon_button(..., tooltip="Save", keys="ctrl-s")`, `ui.MenuItem(..., keys=...)`, `ui.PaletteItem(..., keys=...)` and `ui.tooltip(..., keys=...)` show the shortcut next to the action, drawn as key caps. Pass `keys=` rather than writing the keys into the text: `f"Save ({keys})"` goes stale when the user changes the binding. - `ui.keys_text("ctrl-shift-p")` gives `"Ctrl+Shift+P"` for a plain-text label or status line. - Force the style with `pyzed.ui.base.KEY_STYLE = "symbols"` or `"words"`. ### Languages and dialogs > Translate a Pyzed desktop app into several languages, switch the language while it runs, and use the ready-made translated dialogs. Source: https://albakore.github.io/pyzed/guides/i18n/ #### Languages `pyzed.i18n` keeps one catalog of texts per language. Views look texts up with `pz.t`, so switching the language from a handler redraws the whole app in the new one. ```python import pyzed as pz from pyzed import i18n i18n.add("en", {"greeting": "Hello, {name}!", "files": {"one": "{count} file", "other": "{count} files"}}) i18n.add("es", {"greeting": "¡Hola, {name}!", "files": {"one": "{count} archivo", "other": "{count} archivos"}}) i18n.set_language(i18n.system_language()) # or "es", "pt_BR"... class Hello(pz.App): def view(self): return pz.column([ pz.text(pz.t("greeting", name="Kev")), pz.text(pz.t("files", count=3)), pz.button("Español", on_press=lambda: i18n.set_language("es")), ]) ``` - Placeholders like `{name}` are filled from the keyword arguments. - A text with `one`/`other` (and optionally `zero`) forms is a plural; `count=` picks the form. - Nested tables become dotted keys: `{"menu": {"open": "Open"}}` is `pz.t("menu.open")`. - Missing texts fall back from `"es_AR"` to `"es"`, then to the fallback language (`"en"`, see `i18n.set_fallback`), then to the key itself. Catalogs can live in files, one per language, next to your app: ```text locales/ ├── en.json {"greeting": "Hello, {name}!"} └── es.toml greeting = "¡Hola, {name}!" ``` ```python i18n.load("locales") # ["en", "es"] ``` Pyzed's own components (the dialogs below, the dock's menu) are translated too. English and Spanish are built in; their keys start with `pyzed.` (`pyzed.ok`, `pyzed.cancel`...), so `i18n.add("fr", {"pyzed": {"ok": "OK", "cancel": "Annuler"}})` adds French. #### Dialogs `pyzed.ui` has ready-made dialogs. Keep which one is open in your state and wrap the view with `ui.dialog_layer`, which dims the window and centers the dialog: (Image: A confirmation dialog) ```python from pyzed import ui class Files(pz.App): asking = False def delete(self): ... self.asking = False def view(self): dialog = None if self.asking: dialog = ui.confirm("Delete 3 files?", self.delete, lambda: setattr(self, "asking", False), danger=True) return ui.dialog_layer(self.main_view(), dialog, on_dismiss=lambda: setattr(self, "asking", False)) ``` | Dialog | What it does | | --- | --- | | `ui.alert(message, on_close, kind=)` | A message with an OK button; `kind` adds an info, success, warning or error icon. | | `ui.confirm(message, on_confirm, on_cancel, danger=)` | A yes/no question; `danger=True` makes the button a red Delete. | | `ui.prompt(title, value, on_input, on_submit, on_cancel)` | Asks for a line of text (Enter submits). | | `ui.choice(message, [(label, on_press), ...], on_cancel)` | Your own buttons; the last one is the main one. | | `ui.unsaved_changes(name, on_save, on_discard, on_cancel)` | "Save your changes before closing?" | For anything else, `ui.modal(content, title=, footer=[...])` is the same surface with your own content. See [`examples/dialogs.py`](https://github.com/albakore/pyzed/blob/main/examples/dialogs.py). ### Tests and screenshots > Test Pyzed apps headlessly: render your Python GUI without a window for unit tests, documentation screenshots and continuous integration. Source: https://albakore.github.io/pyzed/guides/testing/ `app.screenshot(path)` renders the app's current state **off-screen** and saves a PNG. It needs no window, display or GPU (it uses the software renderer), so it works in CI. ```python app = MyApp() app.items = ["one", "two"] # set up the state you want to show app.screenshot("docs/list.png", size=(480, 320), scale=2) ``` - `size`: logical size; defaults to `App.size`. - `scale`: PNG pixel density (2 = retina). - `wait`: seconds to wait before capturing, in case `on_start` started some work. Every image in this documentation is generated this way: #### Testing the logic Since the state lives in your object, `update` and callbacks are tested like plain Python: ```python def test_counter(): app = Counter() app.update("increment") app.update("increment") assert app.count == 2 ``` #### Testing that the view renders `view()` must return a `Widget` without raising. A screenshot makes a good smoke test: ```python def test_view_renders(tmp_path): app = MyApp() app.screenshot(tmp_path / "ui.png", size=(800, 600)) assert (tmp_path / "ui.png").stat().st_size > 0 ``` If `view()` raises, the screenshot shows the traceback instead of the UI, just like the window. ### Hot reload > Edit a Pyzed app while it runs: new methods, new attributes and new initial values reach the open window on save, keeping the data the app already has. Source: https://albakore.github.io/pyzed/guides/hot-reload/ Hot reload updates the open window every time you save a file of your project. The app is not restarted: the same objects keep their data and start running the new code. From code, use `pz.run(MyApp, reload=True)` or set the `PYZED_RELOAD=1` environment variable. Forms update on save too (see [Running and the console](https://albakore.github.io/pyzed/editor/console-and-running/#live-updates-on-save)); this page is about apps written in Python. #### What a save changes | You change | What happens in the open window | | --- | --- | | `view()`, a method, a helper module, a style | The new code runs from the next redraw. | | A method that something else keeps (`self.on_done = self.save`, a timer, a thread, `functools.partial`) | That reference runs the new code too. | | A new attribute in `__init__` (`self.tags = []`) | The live object gets it right away, nothing else changes. | | The initial value of an attribute (`self.step = 1` → `self.step = 5`) | It takes the new value if the app has not changed it while running; otherwise the app's value stays. | | A new `pz.state(...)` or a new attribute in a component's `setup()` | Every instance of the component gets it. | | A class attribute (`size = (400, 300)`, `title`) | It comes from the new class. | | Code that fails to import, or a `view()` that raises | The error shows in the window until you save a fix. | Each reload prints a line per class that changed, so you can see what was picked up: ```text pyzed: reloaded Notes: methods changed: add, view; new state: tags pyzed: reloaded Notes: updated state: step ``` #### An example Start this app with `pyzed dev notes.py`, add a few notes, then make the edits below one at a time and save: ```python import pyzed as pz from pyzed import ui class Notes(pz.App): title = "Notes" size = (420, 320) def __init__(self): self.notes = [] self.draft = "" def type_draft(self, text): self.draft = text def add(self): if self.draft.strip(): self.notes.append(self.draft.strip()) self.draft = "" def view(self): return pz.column([ pz.row([ui.input(self.draft, self.type_draft, on_submit=self.add, placeholder="New note"), ui.button("Add", self.add)], spacing=8), *[pz.text(note) for note in self.notes], ], spacing=8, padding=16) if __name__ == "__main__": pz.run(Notes) ``` 1. **A new attribute.** Add `self.starred = set()` to `__init__` and a star button per note in `view()`. The notes you typed are still there and `self.starred` exists at once. 2. **A new method.** Add `def clear(self): self.notes.clear()` and a button that calls it. It works on the next click. 3. **A new initial value.** Add `self.limit = 10` to `__init__`, save, then change it to `5`: the app sees `5`. If the app had changed `limit` itself in the meantime, its value would be kept. #### How state is brought up to date When a class changed, Pyzed runs its `__init__` again on a **scratch copy**, with the arguments the object was created with (for a component, `setup()` with its current props), and compares it with the live object: - attributes the live object does not have are copied over; - plain values (numbers, text, booleans, and small lists, tuples, sets and dicts of them) whose initial value changed in the code are updated, but only if the live value is still the old initial one; - everything else is left alone. The scratch copy is then thrown away, and what its `__init__` printed is hidden. Classes whose code did not change are not touched, so saving an unrelated file never runs your `__init__`. Objects of your own classes that Pyzed did not create (a `Todo` in a list, for example) get the new attributes that only need `self`, globals or builtins: `self.done = False` works, `self.label = text.upper()` needs the `text` argument and is skipped. #### When `__init__` has side effects If `__init__` opens a file, a device, a socket or starts a thread, running it again on a scratch copy is usually harmless, but you can turn it off for that class: ```python class Player(pz.App): reload_init = False # hot reload keeps the state as it is: no scratch copy def __init__(self): self.stream = open_audio_device() ``` Better yet, move that work to `on_start()`, which runs once when the window opens. If the scratch `__init__` fails, the reload still applies and the console says why the state was not updated. #### Migrating state by hand For changes Pyzed cannot guess (renaming an attribute, changing its type), define `on_reload()` on the app or a window. It runs after every reload and may return a `Command`: ```python class Notes(pz.App): def on_reload(self): if isinstance(self.notes, list) and self.notes and isinstance(self.notes[0], str): self.notes = [{"text": n, "done": False} for n in self.notes] ``` #### Inspecting a reload `pz.hot_reload.current()` returns the running `Reloader`. Its `changes` list has one `Change` per class that changed in the last reload (`changed`, `added` and `removed` methods, `new_state` and `updated_state`), `count` says how many reloads succeeded and `error` holds the traceback of a failed one. Set `verbose = False` to stop the console lines. #### Limits - A running `async` task or generator keeps the code it started with until it ends. - Removing an attribute from `__init__` does not delete it from the live object. - Code that already ran (the `if __name__ == "__main__":` block, `on_start()`) does not run again. - A class with `__slots__` that change cannot switch to the new version; restart the app in that case. ### How it works > How Pyzed works: the bridge between Python and Iced (Rust), the immutable widget tree, editor state and why the GUI stays fast. Source: https://albakore.github.io/pyzed/guides/how-it-works/ ``` Python Rust (pyzed._core, PyO3) Iced 0.14 App.view() ──► Widget tree ──────► render.rs: Widget → iced::Element ──► layout + wgpu/tiny-skia App.update() ◄── messages ◄────── app.rs: Message, Command → Task ◄──── events (mouse, keyboard) ``` #### Pieces ```mermaid flowchart TB subgraph PY["Python (your code)"] A["App / Window"] --> W["view() → pz.* / ui.*"] U["update()"] L["asyncio loop + threads"] end subgraph RS["Rust (native extension, PyO3)"] B["build Iced elements"] --> I["Iced 0.14"] I --> G["wgpu / tiny-skia"] end W -->|"_core.Widget"| B I -->|"events → messages"| U L -->|"pz.post / results"| U U --> W ``` | File | Responsibility | | --- | --- | | `src/widget.rs` | The (immutable) `Widget` class and the functions that build it (`text`, `row`…). | | `src/render.rs` | Turns the `Widget` tree into an `iced::Element` every frame. | | `src/app.rs` | The runtime: `Message`, `Command`, dispatch to Python, keyboard, `post`, `run` and `screenshot`. | | `src/style.rs` | Conversion of colors, sizes, padding and fonts from Python values. | | `src/highlighter.rs` | Syntax highlighting with syntect (two-face grammars) and theme colors. | | `src/fonts.rs` | Bundled fonts (IBM Plex, Lucide) and `load_font`. | | `python/pyzed/` | The `App` class, themes, icons, CLI and the `pyzed.ui` components. | #### Performance - **Widgets are Rust data.** Building the tree from Python creates small, immutable objects; no live Iced objects exist in Python. - **The heavy lifting is in Rust.** Layout, text measurement, highlighting, scrolling and drawing happen in Rust; Python only runs when there is a message. - **The GIL is released** while Iced waits for events, so Python threads keep running. - **Native state for the editor.** Each editor's buffer (text, cursor, history) lives in Rust and is identified by `id`, so typing doesn't rebuild the text in Python. #### Messages Every interactive widget keeps a reference to the Python value you gave it. When Iced produces an event, `app.rs` turns it into a call: 1. If it is a callable, it calls it (with the control's value, if any). 2. If it returns a `Command`, it translates it into an Iced `Task`. 3. If it returns any other value (or the message was not callable), it calls `app.update(value)`. 4. It asks for `view()` again and Iced draws the difference. #### Contributing ```bash maturin develop --release pytest tests cargo fmt --check && cargo clippy -- -D warnings ``` The documentation lives in [`docs/`](https://github.com/albakore/pyzed/tree/main/docs). See [editing the documentation](https://albakore.github.io/pyzed/contributing/docs/). ### Visual designer > Build Python desktop windows by drag and drop, wire their events to Python and run them: a visual GUI designer like Visual Basic or Qt Designer. Source: https://albakore.github.io/pyzed/guides/designer/ Pyzed Design is a form editor in the spirit of Visual Basic, the WinForms designer in Visual Studio and Qt Designer, built with Pyzed itself. You draw a window with the mouse, set properties in a grid, double-click a control to write the code of its event, and press **F5** to run it with all its logic. (Image: The designer with the login example open) Everything about it now lives in its own manual, **[Pyzed Design](https://albakore.github.io/pyzed/editor/)**: | Topic | Page | | --- | --- | | The window and its areas | [Overview](https://albakore.github.io/pyzed/editor/) | | Placing, moving, aligning, snapping, the outline | [Toolbox and canvas](https://albakore.github.io/pyzed/editor/toolbox-and-canvas/) | | The property grid, lists and tables | [Properties](https://albakore.github.io/pyzed/editor/properties/) | | Panels, flex rows and columns, grids, anchors | [Layouts and containers](https://albakore.github.io/pyzed/editor/layouts/) | | The Style section, the color picker, themes | [Styles and themes](https://albakore.github.io/pyzed/editor/styles-and-themes/) | | Events and writing handlers | [Events and handlers](https://albakore.github.io/pyzed/editor/events/) | | Completion, diagnostics, language servers | [Code editor](https://albakore.github.io/pyzed/editor/code-editor/) | | F5, the debug console, hot reload | [Running and the console](https://albakore.github.io/pyzed/editor/console-and-running/) | | uv projects, dependencies, ruff, ty, git | [Projects](https://albakore.github.io/pyzed/editor/projects/) | | Reusable components | [Component designer](https://albakore.github.io/pyzed/editor/component-designer/) | | New forms, templates, `.pzform` files, scripting | [Files, templates and scripting](https://albakore.github.io/pyzed/editor/files-and-scripting/) | | Menus, the command palette, docks | [Menus and commands](https://albakore.github.io/pyzed/editor/menus-and-commands/) | | Every shortcut | [Keyboard shortcuts](https://albakore.github.io/pyzed/editor/keyboard-shortcuts/) | | Preferences | [Settings](https://albakore.github.io/pyzed/editor/settings/) | #### Debug console See [Running and the console](https://albakore.github.io/pyzed/editor/console-and-running/#the-console). #### Panels See [Panels and docks](https://albakore.github.io/pyzed/editor/menus-and-commands/#panels-and-docks). #### Code editor See [Code editor](https://albakore.github.io/pyzed/editor/code-editor/): completion, diagnostics, code lens, folding and the ty and ruff language servers. #### Telemetry See [Settings → Telemetry](https://albakore.github.io/pyzed/editor/settings/#telemetry). #### Components **Common**: Label, Heading, Button, Text box, Text area, Check box, Switch, Radio group, Drop-down, Slider, Number box, List box, Link and Search box. **Containers**: Panel, Group box, Card. **Layout**: Flex row, Flex column, Grid, Spacer. **Navigation**: Tab bar, Segmented. **Display**: Progress bar, Badge, Chip, Callout, Avatar, Key binding, Stat, Chart, Table, Image, Icon, Markdown, Spinner, Divider and Timer. (Image: Every built-in component) To add your own, see [Files, templates and scripting](https://albakore.github.io/pyzed/editor/files-and-scripting/#adding-your-own-components) and the [component designer](https://albakore.github.io/pyzed/editor/component-designer/). #### How it is built See [Files, templates and scripting → How it is built](https://albakore.github.io/pyzed/editor/files-and-scripting/#how-it-is-built). ### Component designer > Draw reusable Pyzed components visually, with properties, state, methods and custom events, and nest them like Godot scenes. Source: https://albakore.github.io/pyzed/guides/component-designer/ Pyzed Design has a second mode for building **components** instead of windows: you draw the component like a form, declare its properties and events in a panel, write the logic of its methods, and place it in forms or inside other components, like scenes in Godot. A designed component **is** a [`pz.Component`](https://albakore.github.io/pyzed/guides/components/). (Image: A ProfileCard component that contains a Rating component) The full guide is in the Pyzed Design manual: **[Component designer](https://albakore.github.io/pyzed/editor/component-designer/)**. #### Property types See [Property types](https://albakore.github.io/pyzed/editor/component-designer/#property-types). #### Inheritance See [Inheritance](https://albakore.github.io/pyzed/editor/component-designer/#inheritance). ### Projects with uv > Create and manage Pyzed desktop apps as standard uv projects, with the UI in its own folder and ruff, ty and git ready from the first command. Source: https://albakore.github.io/pyzed/guides/projects/ A Pyzed project is a normal [uv](https://docs.astral.sh/uv/) project. The interface lives in its own folder (`ui/`) and the rest of the code stays where uv puts it, so the frontend and the backend are separated but can still import each other. Dependencies are managed by uv, [ruff](https://docs.astral.sh/ruff/) lints and formats, [ty](https://docs.astral.sh/ty/) checks types, and the project starts as a git repository. #### Create a project ``` shop/ ├── pyproject.toml # uv project + [tool.pyzed] ├── uv.lock ├── .venv/ # created by uv sync ├── .python-version ├── .gitignore ├── src/shop/ # backend: plain Python, no UI code │ └── __init__.py └── ui/ # frontend: forms and their handlers ├── main.pzform └── main.py ``` The starter form calls a function of the backend, so you can see the import across folders working: ```python # ui/main.py from shop import greeting def greet_click(form, event): form.message.text = greeting(form.name_input.text or "world") ``` | Option | Effect | | --- | --- | | `--app` | No `src/`: backend modules go next to `pyproject.toml` (`uv init --app`). | | `--python 3.12` | Python version of the project (uv downloads it if needed). | | `--no-git` | Do not create a git repository. | | `--no-sync` | Only write `pyproject.toml`; do not resolve or install anything (offline). | | `--script` | The old single-file app (`main.py` + `requirements.txt`), without uv. | #### Add Pyzed to an existing uv project If you start projects with `uv init`, keep doing it and add the UI afterwards: ```bash uv init --package shop && cd shop uvx --from pyzed-ui pyzed init # or `pyzed init` if pyzed is installed ``` `pyzed init` never touches your code: it adds the `[tool.pyzed]` table, creates `ui/` with a starter form, and adds `pyzed-ui` (and `ruff`/`ty` as dev dependencies) with `uv add`. Run it in a folder without `pyproject.toml` and it runs `uv init` first. ```toml [tool.pyzed] ui = "ui" # the UI folder main = "ui/main.pzform" # opened by `pyzed design`/`pyzed run` and started by `pyzed build` ``` #### Importing code outside the UI folder Forms and their handlers can import anything in the project. `pyzed run`, `pyzed design` and `pyzed build` *activate* the project first, which puts these folders on `sys.path`: 1. `ui/`: helper modules next to the handlers (`from helpers import money`). 2. `src/`: the backend package, even before `uv sync` installs it. 3. The project root: modules next to `pyproject.toml` (`--app` layout). 4. The `.venv` site-packages, when the designer runs on a different interpreter of the same Python version, so every dependency installed with `uv add` resolves too. Anything else (a sibling repository, a shared library) is just another dependency: add it with a path and uv installs it in editable mode. ```bash pyzed add --editable ../billing # uv add --editable ../billing ``` From your own scripts, do the same with `pyzed.project`: ```python from pyzed import project project.find().activate() # the project containing the current folder ``` #### Dependencies Every command runs uv in the project folder and prints its output. | Command | Runs | | --- | --- | | `pyzed add httpx` | `uv add httpx` | | `pyzed add "httpx==0.27.2"` | Changes the constraint of a package already there. | | `pyzed add --dev pytest` | `uv add --dev pytest` (`--group docs` for other groups). | | `pyzed remove httpx` | `uv remove httpx` (finds the group by itself). | | `pyzed upgrade` | `uv lock --upgrade` + `uv sync` (every package). | | `pyzed upgrade httpx` | `uv lock --upgrade-package httpx` + `uv sync`. | | `pyzed sync` | `uv sync`: makes `.venv` match `uv.lock`. | | `pyzed deps [--outdated]` | Lists direct dependencies with their locked (and newest) versions. | | `pyzed check [--fix]` | `ruff check` and `ty check` with the project's `.venv`. | Plain `uv` commands work just as well; Pyzed only reads `pyproject.toml` and `uv.lock`. #### The Project tab of the designer The designer has a **Project** tab next to the toolbox. It opens the project the form belongs to, or offers **New project** and **Open…** (a folder browser with your recent projects) when there is none. (Image: The Project tab: the project's files and folders) - **Project** shows the project's files as a tree, like in a code editor, colored by git status. Search files and folders from its toolbar or with Ctrl+P; right-click for new file, new folder, rename, delete, copy path and reveal in the file manager. Forms and components open in the designer; other files open in the system's app. - **Dependencies**, a tab of its own next to it: add a package (tick *dev* for the dev group), change its version (pencil), upgrade it or remove it. Its toolbar syncs the environment, checks for updates (shown in green) and upgrades everything. Below, **Tools** shows the versions of uv, git, ruff and ty found; **Lint**, **Format** and **Types** run ruff and ty and show their output. - **Create repository** (in the header) runs `git init` and writes a `.gitignore`. Once there is a repository the header shows its branch. Commands run in the background, so the designer stays responsive while uv downloads packages. When the designer is not running inside the project's environment (for example the standalone Pyzed Design, or `pyzed design` installed with `uv tool`), **F5** saves the form and starts it with `uv run pyzed run --reload`, so it has all the project's dependencies and updates on every save. #### Pyzed Design as a standalone app Each [release](https://github.com/albakore/pyzed/releases) includes **Pyzed Design** for Linux, Windows and macOS (`pyzed-design-*.zip`): the designer compiled with its own Python and the `uv` binary. Unzip it and run it; nothing else needs to be installed. Projects created from it are normal uv projects with their own `.venv`. To build it yourself on the current platform: -.zip `} pip={` {venv} {activate} pip install pyzed-ui pyinstaller uv python scripts/build_designer.py # dist/Pyzed Design/ and dist/pyzed-design--.zip `} python={` {py} -m pip install pyzed-ui pyinstaller uv {py} scripts/build_designer.py # dist/Pyzed Design/ and dist/pyzed-design--.zip `} /> ##### Updates Pyzed Design checks GitHub for a newer release at launch (once a day) and every few hours while it is open (turn it off in **Help → Check for updates automatically**, or check now with **Help → Check for updates**). The download for your platform starts on its own, is checked against the release's SHA-256 and unpacked in the background; a softly blinking **Restart to update** button then appears at the top right of the title bar. It quits, swaps the app folder (the `.app` on macOS) for the new one and starts it again. After an update, the What's new tab opens on its own with the notes of the new version, and **Help → What's new** shows them at any time. **Skip this version** hides an update until a newer one comes out. **Help → Get preview releases** switches to the preview channel, which also takes pre-releases (`X.Y.Z-rc.N`). The installers ([Download](https://albakore.github.io/pyzed/download/#how-it-installs)) put the app in a folder of your own, so it can replace itself; otherwise download the new installer or zip again. When the designer runs from Python (`pip` or `uv`), the tab shows the command that upgrades the package instead. The same updater is available to your own compiled apps in `pyzed.updater`: ```python from pyzed import updater release = updater.latest("me/my-app") # the newest GitHub Release if updater.is_newer(release.version, "1.0.0"): asset = release.asset_for(updater.asset_name("my-app")) # my-app--.zip app = updater.extract(updater.download(asset)) updater.replace_on_exit(app) # then quit; the new version starts ``` #### From Python ```python from pyzed import project shop = project.init("shop", git=True) # returns a Project shop.add("httpx") # every command returns a Result(code, output) shop.upgrade("httpx") print([(d.name, d.locked) for d in shop.dependencies()]) print(shop.check().output, shop.typecheck().output) shop.git_init() ``` To package the app for each platform, see [Build and distribute](https://albakore.github.io/pyzed/packaging/build/). ### MCP server for AI agents > Connect AI coding agents (Cursor, VS Code, Zed and other MCP clients) to Pyzed's MCP server so they know its API, generate and check GUI code. Source: https://albakore.github.io/pyzed/guides/mcp/ Pyzed ships an [MCP](https://modelcontextprotocol.io) server. Connect it to your AI agent (Claude Code, Claude Desktop, Cursor, VS Code, Zed...) and the agent stops guessing: it looks up the exact signature of every widget and component, reads this documentation, follows Pyzed's best practices, generates apps, components and designer forms, checks the code it writes and even looks at a screenshot of the result. #### Install The server is an optional extra of the package (it needs Python 3.10 or newer): You rarely run it by hand: the agent starts it. With [uv](https://docs.astral.sh/uv/) the agent can run it without installing anything first, using `uvx --from "pyzed-ui[mcp]" pyzed mcp`. `pyzed mcp --transport streamable-http` serves it over HTTP on `127.0.0.1` instead. Any program on your computer can reach it there without signing in, so `screenshot` and the project tools, which read and write files and run code, are left out unless you add `--allow-code`. The project tools work in the folder the server was started in; `pyzed mcp --root PATH` picks another one. They never reach outside it, nor files that may hold secrets (`.env`, keys, `.netrc`...). #### Connect your agent ##### Claude Code ```bash claude mcp add pyzed -- uvx --from "pyzed-ui[mcp]" pyzed mcp ``` Inside a Pyzed project created with `pyzed new`, use the project's own environment so the server sees the same Pyzed version and your components: `claude mcp add pyzed -- uv run pyzed mcp`. ##### Claude Desktop, Cursor and other clients Most clients read a JSON file with the same shape (`claude_desktop_config.json`, `.cursor/mcp.json`...): ```json { "mcpServers": { "pyzed": { "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` ##### VS Code In `.vscode/mcp.json`: ```json { "servers": { "pyzed": { "type": "stdio", "command": "uvx", "args": ["--from", "pyzed-ui[mcp]", "pyzed", "mcp"] } } } ``` If Pyzed is already installed in an environment, `"command": "pyzed", "args": ["mcp"]` works everywhere. #### What the agent gets ##### Tools | Tool | What it does | | --- | --- | | `plan_app` | Where to start for an app or screen: the closest blocks and examples, the components and guide topics it needs, the docs to read and the steps. Agents call it first. | | `get_guide` | How Pyzed works (`"overview"`), its rules (`"best-practices"`), runnable recipes (`"cookbook"`: app structure, background work, forms, virtual lists, animation, shaders, games, custom window frames, anchored layouts) or any topic of `list_guides` (`"games"`, `"shaders"`, `"performance"`, `"integrations"`...), in English or Spanish. | | `list_guides` | Every topic `get_guide` takes. | | `list_components` | Every widget (`pz.*`), ready-made component (`pz.ui.*`), designer toolbox type and `pz.Component` class, with a one-line summary. Filter by `kind`, `query`, `category` or `role` (the color role of designer types: `layout`, `input`, `action`, `navigation`, `display`, `media`, `system`, `custom`). | | `describe_component` | The signature, documentation and parameters of one widget or component (`"pz.column"`, `"ui.button"`), or the props and events of a designer type (`"designer:button"`). | | `search_reference`, `read_reference` | Search and read the [reference](https://albakore.github.io/pyzed/reference/) of every module, class and function (`pyzed.game`, `pyzed.updater`, `ui.virtual_list`...). | | `editor_keymap` | The default key bindings of the code editor and how to change them. | | `search_icons` | Lucide icon names for `icon=` and `pz.icon`. | | `list_themes` | The built-in themes and the color tokens styles accept (`"accent"`, `"panel"`...). | | `list_docs`, `read_docs` | The pages of this documentation, in English or Spanish. | | `list_examples`, `read_example` | The source of the examples, by `kind`: apps, blocks (whole screens to copy), games, showcase (shaders, effects), backend integrations (FastAPI, SQLAlchemy, SQLModel, Django), tutorial and designer. | | `new_app` | A runnable `pz.App` script: a simple starting point or an editor-style workspace. | | `new_component` | A `pz.Component` skeleton with its props, state and events. | | `new_form` | A visual-designer form: the `.pzform` layout and its handlers module. | | `validate_code` | Checks code without running it: names that don't exist in `pz` or `pz.ui`, keyword arguments a function doesn't take, apps without `view()`, components without `render()`, side effects in `view()`, lambdas in loops that don't capture the loop variable, and ruff's pyflakes checks when ruff is installed. | | `validate_form` | Checks a `.pzform` (types, props, events, names, containers) and that its handlers module defines every handler. | | `screenshot` | Renders the code off-screen (no window, no GPU) and returns a PNG, so the agent can see what it built. | | `animation_properties`, `new_animation`, `check_animation` | [Timeline animations](https://albakore.github.io/pyzed/animation/timeline/): what a track can animate, a new `.pzanim` library from clips and tracks, and checking one. | | `list_shader_nodes`, `new_shader`, `check_shader` | The nodes of [shader graphs](https://albakore.github.io/pyzed/shading/shader-graphs/), a new `.pzshad` from a template, and compiling one to WGSL with its errors by node. | ##### Project tools With these the agent builds a whole app in a folder, from the project to a clean check: | Tool | What it does | | --- | --- | | `project_info` | The project: name, version, entry (its `script`, else the main form), forms, components, shaders, package, dependencies, files and environment (the `.venv` it runs in, the system, WSL). | | `list_files`, `read_file` | List and read the files of the folder. | | `write_files` | Creates or replaces several files at once and checks every Python file, form and shader graph it wrote. | | `create_project` | A new uv project, like `pyzed new` (pyzed-ui, ruff, ty, `ui/main.pzform`, `src/`, git). | | `add_dependency` | `uv add` in the project. | | `check_project` | Pyzed's checks of every file, plus ruff and ty. | | `run_tests` | `uv run pytest` in the project. | | `screenshot_project` | A screenshot of a script, form, component or shader graph, in the project's own environment; by default the entry. | | `project_settings`, `set_project_settings` | The project's own settings: its [renderer](https://albakore.github.io/pyzed/editor/settings/#rendering) (GPU or CPU, graphics API, GPU, vsync, in `[tool.pyzed.rendering]`) and the editor and designer preferences it shares in `.pyzed/settings.json` (tab size, grid...). | | `start_live_preview`, `live_preview_status`, `stop_live_preview` | Open the app in a real window with hot reload while the agent builds it: every file it writes updates the window on save, so you watch the app take shape. The status returns what the app printed, tracebacks included. | ##### Resources and prompts Clients that support resources can attach `pyzed://guide`, `pyzed://best-practices`, `pyzed://cookbook`, `pyzed://reference/{name}`, `pyzed://components`, `pyzed://themes`, `pyzed://docs`, `pyzed://component/{name}` and `pyzed://docs/{slug}` (with `--` instead of `/`, such as `guides--layout`). The prompts `create_app`, `create_component` and `create_form` start a task with the right steps: read the guides, look up existing components, generate, validate and preview. `build_project` builds a complete app in the folder: read the guides and examples, create the project, write it in small steps, then check, test and look at it until everything is clean. #### Example session > Make a Pyzed component that shows a rating with stars and lets the user change it. A typical agent run with the server connected: 1. `get_guide("best-practices")`, then `list_components(query="rating")` to check nothing similar exists. 2. `new_component("star rating", props=[{"name": "value", "type": "int", "default": 0}], events={"change": "A star was picked."}, icon="star")`. 3. Fills in `render()` with `pz.row`, `pz.text(...).on(click=...)` and `self.emit("change", i)`, looking up `describe_component("pz.text")` for the parameters. 4. `validate_code(code)` and `screenshot(code + "def preview():\n return StarRating(value=3)")` to see it. #### How it works The server lives in the package (`pyzed.mcp`), so what it knows always matches the installed version: signatures and docstrings come from the code itself, the descriptions from this reference and the designer's toolbox from its registry, including your own `pz.Component` classes. Documentation pages and examples are read from a source checkout when there is one, and otherwise downloaded from GitHub at the installed version's tag. `screenshot` runs the code in a separate process with a 60 second limit and replaces `pz.run`, so no window opens. It uses the same off-screen renderer as [tests and screenshots](https://albakore.github.io/pyzed/guides/testing/). `pyzed.mcp.knowledge`, `pyzed.mcp.projects`, `pyzed.mcp.scaffold` and `pyzed.mcp.validate` are plain Python and work without the MCP SDK, for example from your own scripts or tests. #### More on AI agents The [AI agents](https://albakore.github.io/pyzed/ai/) section collects everything else: [prompting guide](https://albakore.github.io/pyzed/ai/prompting/) with a real MCP session, [llms.txt and editor rules](https://albakore.github.io/pyzed/ai/llms-txt/) with `AGENTS.md`, Cursor and Copilot templates, and [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/). ### AI agents in your app > pyzed.agents: add AI to Python desktop apps with Anthropic, OpenAI, Gemini, OpenRouter or Ollama, give models tools and stream answers into your UI. Source: https://albakore.github.io/pyzed/guides/agents/ `pyzed.agents` is the engine behind the designer's [Agent panel](https://albakore.github.io/pyzed/editor/ai-agents/), and it works in any Pyzed app: one API for several model providers, tools written as plain Python functions, streaming events for the UI, saved threads, and keys kept in the operating system's keychain. #### A first agent ```python from pyzed import agents agents.set_key("anthropic", "sk-ant-…") # once: saved in the system keychain @agents.tool def stock(product: str) -> int: """How many units of a product are in stock. product: the product's name. """ return {"chair": 4, "desk": 0}.get(product, 0) agent = agents.Agent("anthropic:claude-opus-5-5", tools=[stock], system="You help the shop's staff.") print(agent.run("Can I sell two desks today?")) ``` `run` calls the model, runs the tools it asks for, sends their results back and repeats until the model answers. It returns the last answer and never raises: failures (a bad key, no network) come back as an `"error"` event and as the returned text. `agent.messages` keeps the conversation, so the next `run` continues it. #### Models and providers A model is named `":"`: | Provider | Example | Credentials | | --- | --- | --- | | `anthropic` | `anthropic:claude-opus-5-5` | `set_key`, `ANTHROPIC_API_KEY` (an `ant auth login` profile also works), or [identity federation](#claude-without-an-api-key) | | `openai` | `openai:` | `set_key` or `OPENAI_API_KEY` | | `google` | `google:` | `set_key` or `GEMINI_API_KEY` | | `openrouter` | `openrouter:anthropic/claude-opus-5-5` | `set_key`, `OPENROUTER_API_KEY` or `agents.login.openrouter_login()` | | `xai`, `mistral`, `deepseek`, `groq` | `groq:` | `set_key` or `XAI_API_KEY`, `MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`, `GROQ_API_KEY` | | `github` | `github:openai/gpt-4.1` | GitHub Models: `agents.login.github_device_code()` (device flow), a fine-grained token with the Models permission or `GITHUB_TOKEN` | | `vertex` | `vertex:google/gemini-2.5-pro` | Gemini on Vertex AI: `agents.login.google_login()` or the gcloud credentials of the computer; `settings.provider("vertex").project` | | `ollama`, `lmstudio` | `ollama:qwen3:8b` | none (local) | | `custom` | `custom:` | the server URL in the settings, and a key if needed | `agents.list_models("openai")` asks a provider for its models. Claude goes through the official `anthropic` SDK with adaptive thinking, the `effort` setting, prompt caching and the server-side refusal fallback; every other provider through the OpenAI-compatible Chat Completions API. #### Settings ```python settings = agents.AgentSettings(model="ollama:qwen3:8b", effort="medium", profile="ask", rules="Answer in Spanish.") settings.provider("ollama").base_url = "http://gpu-box:11434/v1" agent = agents.Agent(tools=[stock], settings=settings) ``` | Field | Meaning | | --- | --- | | `model` | Default model. | | `effort` | `low`, `medium`, `high`, `xhigh` or `max` (Claude). | | `fast` | Fast mode on the models in `providers.FAST_MODELS` (Claude Opus 5.5 and Opus 5, Claude API only): faster output at a higher price. | | `profile` | `"write"` (every tool), `"ask"` (only tools without `writes=True`) or `"minimal"` (no tools). | | `mode` | The Agent panel's permission mode, a shortcut over `profile` and `confirm_writes`: `"ask"`, `"bypass"`, `"read"` or `"chat"`. | | `confirm_writes` | Call `confirm(tool, args)` before a tool marked `writes=True` (on by default). Without a `confirm` callback those tools are refused; turn it off to let them run unasked. | | `rules` | Text added to the system prompt. | | `keep_history` | Whether Pyzed Design saves threads on this computer (on by default). | | `providers[id].base_url` / `.models` | Server URL and the models offered for a provider. | `agents.providers.load()` and `save()` read and write `agents.json` in Pyzed's settings folder (the designer's settings); keys are never part of it. #### Tools `@agents.tool` builds the JSON schema from the signature: `str`, `int`, `float`, `bool`, `list`, `dict` and `Optional[...]`; arguments with a default are optional. The first paragraph of the docstring describes the tool and `name: text` lines describe the arguments. Return a string or anything JSON can encode. ```python @agents.tool(writes=True, title="Placing an order") def order(product: str, units: int) -> dict: """Orders units of a product from the supplier.""" ... ``` | Option | Meaning | | --- | --- | | `writes=True` | It changes something: skipped in the `"ask"` profile, and confirmed first when `confirm_writes` is on. | | `ui=True` | Runs through the agent's `executor` (see below). | | `title` | Short text for your UI while it runs. | The model's arguments are checked against the schema before the function runs; a wrong or missing argument, or an exception, goes back to the model as an error so it can fix its call. Ready-made tools: `agents.pyzed_tools()` (Pyzed's components, icons, themes, docs, generators and checks, the same as the [MCP server](https://albakore.github.io/pyzed/guides/mcp/)) and `agents.file_tools(folder)` (list, read, search and write files, never outside `folder`). The file tools never open keys or credentials (`.env`, `*.pem`, `*.key`, SSH keys...; `agents.tools.SECRET_FILES`), so a prompt injection in a project file cannot send them to the provider; pass `hidden=` to change the list. #### Images Models that take images can look at them in a message, and tools can return them: ```python image = agents.Image.open("mockup.png") # PNG, JPEG, GIF or WebP, up to agents.agent.IMAGE_LIMIT agent.run([{"type": "text", "text": "Build this screen."}, agents.image_block(image)]) @agents.tool def chart() -> list: """Draws the sales chart.""" return ["The chart:", agents.Image(render_png())] # text and images go back to the model ``` `image_block` also takes the bytes or a path. A tool that returns an `Image`, or a list with text and images, answers with them; providers without images in tool results get them in the next message. #### MCP servers `agents.mcp_client` connects to [Model Context Protocol](https://modelcontextprotocol.io) servers, local (stdio) or remote (Streamable HTTP), and turns their tools into agent tools: ```python from pyzed.agents import mcp_client, providers fetch = providers.MCPServerConfig("fetch", "uvx", ["mcp-server-fetch"]) github = providers.MCPServerConfig("github", url="https://api.githubcopilot.com/mcp/", secret_names=["Authorization"]) mcp_client.set_secrets("github", {"Authorization": "Bearer …"}) # in the keychain servers = mcp_client.Servers([fetch, github], cwd=".") agent = agents.Agent("anthropic:claude-opus-5-5", tools=servers.tools()) ... servers.close() ``` Tools are named `mcp__` and the ones the server doesn't mark `readOnlyHint` are `writes=True`, so they are confirmed first. `secret_names` lists the environment variables (stdio) or headers (HTTP) whose values `set_secrets` keeps in the keychain. A server that fails is skipped; pass `on_error(server, message)` to `tools()` to hear about it. `mcp_client.MCPClient` talks to one server directly (`list_tools`, `call`). #### External agents `agents.acp` runs agents that speak the [Agent Client Protocol](https://agentclientprotocol.com) (Claude Code, Gemini CLI, Codex…) as their own program and streams their work as the same events as `Agent`: ```python from pyzed.agents import acp config = acp.preset("claude-code") # or "gemini", "codex", or providers.ACPAgentConfig(name, command, args) own = acp.pyzed_mcp_server() # Pyzed's MCP server, or None without the mcp package agent = acp.ACPAgent(config, cwd="my_app", confirm=lambda tool, args: True, mcp_servers=[own] if own else []) print(agent.run("Add a dark theme to the app", on_event=print)) agent.close() ``` The agent reads and writes files through Pyzed, only inside `cwd` and never `SECRET_FILES`. Its permission requests follow `settings.mode`: `"bypass"` allows them, `"read"` allows reads only, `"chat"` refuses them and `"ask"` calls `confirm`. `acp.installed(config)` tells whether its program is on the `PATH`, and the settings keep them in `AgentSettings.acp_agents` (and MCP servers in `mcp_servers`; `acp.mcp_entries(settings.mcp_servers)` hands those to the agent too). Your own tools can go to the external agent too: `mcp_host.ToolServer` serves an `Agent`'s tools over MCP on `127.0.0.1` (a one-time address and a bearer token), which is how Pyzed Design lets Claude Code control the editor: ```python from pyzed.agents import mcp_host server = mcp_host.ToolServer(agents.Agent(tools=[my_tool]), name="my_app", instructions="How to use them") agent = acp.ACPAgent(config, cwd="my_app", mcp_servers=[server.acp_entry(), server.bridge_entry()]) ... server.close() ``` Given both, the agent gets the HTTP entry when it supports it and the stdio bridge otherwise. `acp.claude_signed_in()` tells whether Claude Code is signed in on this computer, and `acp.claude_login_command()` is the command that opens it to sign in with a Claude subscription. #### Streaming into the UI `run` blocks, so in an app call it from a worker thread and hand the events to the UI. Pyzed's views run on the UI thread, so put events on a queue and drain it from a `pz.every` subscription: ```python import queue, threading import pyzed as pz from pyzed import agents, ui class Chat(pz.App): def __init__(self): self.agent = agents.Agent("anthropic:claude-opus-5-5") self.events = queue.Queue() self.answer, self.prompt, self.busy = "", "", False def send(self): self.busy, self.answer = True, "" threading.Thread(target=self.agent.run, args=(self.prompt,), kwargs={"on_event": self.events.put}, daemon=True).start() def poll(self): while not self.events.empty(): event = self.events.get() if event.kind == "text": self.answer += event.text elif event.kind == "done": self.busy = False def subscriptions(self): return [pz.every(1 / 30, self.poll, key="chat")] if self.busy else [] def view(self): return pz.column([ ui.markdown(self.answer), ui.input(self.prompt, lambda v: setattr(self, "prompt", v), on_submit=self.send), ], spacing=8, padding=16) ``` Events: `text` and `thinking` (a new piece of the answer or of the reasoning summary), `tool_call` and `tool_result` (`tool`, `args`, `call_id`, `result`, `error`), `turn` (one model answer finished, with `usage`), `error` and `done`. `agent.stop()` cancels from any thread. Tools that must touch your app's state should run on the UI thread too: pass `executor=` (a function that runs a callable there and returns its result) and mark them `ui=True`. The designer does exactly that, which is why the agent's edits are ordinary undo steps. #### Keys and sign-in ```python agents.set_key("openai", "sk-…") # True: in the keychain; False: no keychain, kept for this session only agents.get_key("openai") # the saved key, or the environment variable agents.delete_key("openai") agents.secrets.store().backend_name # "Keyring" backend in use, or "memory (this session only)" key = agents.login.openrouter_login() # OpenRouter: browser + PKCE, returns a key agents.set_key("openrouter", key) agents.login.key_page("anthropic") # the others: opens the provider's API keys page... agents.providers.looks_like_key("anthropic", copied_text) # ...and tells whether a copied text is its key def check(key): # why the provider refuses the key, or None try: agents.list_models("anthropic", key=key) except agents.AgentError as err: return str(err) # ...or a sign-in page served on 127.0.0.1: it links to the keys page and returns the key pasted there key = agents.login.key_portal("anthropic", check) ``` GitHub and Google sign in without a key too, with OAuth apps you register (their IDs go in the provider's settings, or `PYZED_GITHUB_CLIENT_ID` / `PYZED_GOOGLE_CLIENT_ID`): ```python code = agents.login.github_device_code(client_id) # show code.user_code and open code.verification_uri token = agents.login.github_device_token(client_id, code) # waits until it is approved agents.providers.save_token("github", token) # in the keychain; refreshed when it expires token = agents.login.google_login(client_id, client_secret) # browser + PKCE, scope cloud-platform agents.providers.save_token("vertex", token) ``` Without signing in, `vertex` uses the gcloud credentials of the computer (`gcloud auth application-default login`) and `github` takes a fine-grained token with the Models permission as its key. Besides these, only OpenRouter offers OAuth to other apps. For the rest, Pyzed Design opens the key page and watches the clipboard for a few minutes; when a text that `looks_like_key` is copied, it checks it with `list_models(key=...)` and saves it with `set_key`. Your app can do the same. Keys live in the system keychain (`keyring`, service `pyzed`); `PYZED_KEYRING=0` disables it, and `agents.secrets.use(SecretStore(my_backend))` plugs in your own vault (any object with keyring's `get_password` / `set_password` / `delete_password`). The lookup order, the environment variable of each provider and how to keep keys safe are in [API keys and credentials](https://albakore.github.io/pyzed/ai/api-keys/). ##### Claude without an API key With workload identity federation, a token from your identity provider (GitHub Actions, Google Cloud, Azure, Okta, Kubernetes…) is exchanged for a short-lived Claude token, so no API key is stored. Create the issuer and the rule in the Claude Console, then: ```python settings = agents.AgentSettings() settings.federation = agents.providers.FederationSettings( rule_id="fdrl_…", organization_id="…", service_account_id="svac_…", token_source="command", token_command="gcloud auth print-identity-token") agent = agents.Agent("anthropic:claude-opus-5-5", settings=settings) ``` `token_source` is `"file"` (`token_file`, or `ANTHROPIC_IDENTITY_TOKEN_FILE`), `"command"` (prints a fresh token) or `"env"` (`token_env`, `ANTHROPIC_IDENTITY_TOKEN` by default). A saved key wins over federation. `providers.credential_source("anthropic", settings=settings)` tells where the credentials come from. #### Threads ```python store = agents.ThreadStore() # agents/threads in Pyzed's settings folder thread = agents.Thread(model="ollama:qwen3:8b") agent = agents.Agent(thread.model, messages=thread.messages) agent.run("Hello") store.save(thread) # JSON only you can read; no keys store.list() # newest first ``` #### Testing Pass `stream=` to `Agent` to replace the model with a script, so tests run offline and always the same way: ```python from pyzed.agents import llm def fake(model, system, messages, tools, *, settings, on_event, cancel): if len(messages) == 1: return llm.Turn([{"type": "tool_use", "id": "1", "name": "stock", "input": {"product": "desk"}}], "tool_use") return llm.Turn([{"type": "text", "text": "No desks left."}]) agent = agents.Agent("x", tools=[stock], stream=fake, settings=agents.AgentSettings()) assert agent.run("Desks?") == "No desks left." ``` #### A complete chat app [Building AI apps](https://albakore.github.io/pyzed/ai/ai-apps/) builds a full streaming chat with an OpenAI-compatible API, stop, errors and tests without network, and shows how to drive `pyzed.agents` from `pz.Command.perform` and `pz.post`. ### Accessibility > Accessibility in Pyzed desktop apps: keyboard focus with Tab, the system's reduce motion setting and the state of screen reader support. Source: https://albakore.github.io/pyzed/guides/accessibility/ Pyzed apps can be used from the keyboard and respect the motion preference of the system. Screen readers are not supported yet: see [Screen readers](#screen-readers) for why and what comes next. #### Moving between fields with Tab Tab moves the keyboard focus to the next field of the window and Shift+Tab to the previous one, in the order they appear in `view()`. After the last field it goes back to the first. Fields are `text_input`s, `text_editor`s and `code_editor`s (and the components built on them, like `ui.input` or `ui.search`), dropdowns (`pz.pick_list` and `ui.select`) and the controls wrapped in `pz.focusable`: the library's `ui.switch`, `ui.checkbox`, `ui.slider`, `ui.segmented` and `ui.radio_group`. Each window cycles through its own fields only. A `code_editor` with `tab_size` keeps Tab to indent, like a code editor. Editors without it pass the key on, so Tab leaves them. `on_key` sees the key first (`"tab"` or `"shift-tab"`). If it returns something (a message or a `Command`), the app handled it and the focus stays; returning `None` lets the focus move. Turn the behavior off for a whole app or window with `tab_focus = False`: ```python class App(pz.App): tab_focus = False # Tab does nothing unless on_key handles it def on_key(self, combo): if combo == "tab" and self.completion_open: return self.accept_completion() ``` `Command.focus_next()` and `Command.focus_previous()` do the same from code, and `Command.focus(id)` jumps to a specific field (pass `id=` to the control). #### Controls with the keyboard focus A click or Tab gives a control the focus, and it keeps it after you use it, like a text field. A ring in the theme's focus color shows around it once you use the keys (a click alone draws none, like a browser's `:focus-visible`); a dropdown lights its border instead. | Control | Keys while it has the focus | | --- | --- | | `ui.select`, `pz.pick_list` | ↓/↑ (and →/←) pick the next or previous option **without opening the menu**, Home/End the first and last, letters jump to the option that starts with them. Enter, Space, Alt+↓ or F4 open the menu; there ↑/↓ move, Enter picks and Esc closes. | | `ui.switch` | Space or Enter flip it. | | `ui.checkbox` | Space toggles it (a mixed one becomes checked). | | `ui.segmented`, `ui.radio_group` | The arrows pick the previous or next choice, skipping disabled ones; Home/End the first and last. | | `ui.slider` | The arrows move one `step`, with Shift (or Page Up/Page Down) ten; Home/End go to `min`/`max`. | | `ui.number_field` | ↑/↓ step it (ten with Shift), Home/End go to `min`/`max`. | The keys a focused control uses do not reach the app's `on_key`, so ↓ on a dropdown does not also scroll a list or move a selection elsewhere. Every other key (shortcuts like Ctrl+S) still does. Your own controls get the same with `pz.focusable(content, on_key, keys=[...])`: while it has the focus, the combos in `keys` call `on_key(combo)` and nothing else sees them. ```python def stars(self): t = self.theme row = pz.row([pz.icon("star", color=t.warning if i < self.rating else t.text_muted) for i in range(5)], spacing=2) def rate(combo): self.rating = max(0, min(5, self.rating + (1 if combo == "right" else -1))) return pz.focusable(row, rate, keys=["left", "right"], id="rating", ring_color=self.theme.border_focused) ``` :::note Plain buttons don't take the focus yet: use them with the mouse or give important actions a shortcut (see [Keyboard and shortcuts](https://albakore.github.io/pyzed/guides/keyboard/)). ::: #### Reduce motion When the system asks apps to reduce motion, Pyzed turns its animations off: every `animate()`, `transition()`, `presence()`, window transition and animated scroll shows its final state at once. The setting is read when the app starts: | System | Setting | | --- | --- | | macOS | System Settings → Accessibility → Display → Reduce motion | | Windows | Settings → Accessibility → Visual effects → Animation effects (off) | | Linux (GNOME) | Settings → Accessibility → Reduce animation (`enable-animations`) | | Linux (KDE) | System Settings → Animation speed set to *Instant* | `pz.system_reduced_motion()` returns what the system says (`None` if it can't tell). Your app can still decide: ```python pz.set_reduced_motion(True) # always off, e.g. from your app's settings pz.set_reduced_motion(False) # always on pz.set_reduced_motion(None) # follow the system again (the default) ``` The `PYZED_REDUCED_MOTION` environment variable (`1` or `0`) overrides both. Screenshots always show the resting state. See [Animation](https://albakore.github.io/pyzed/animation/motion-and-tests/). #### Screen readers Pyzed draws its interface with Iced, which paints every widget itself on the GPU instead of using the native controls of the system. Screen readers (VoiceOver, Narrator, NVDA, Orca) need an *accessibility tree* that describes those widgets, and Iced 0.14 does not produce one. The usual way to provide it in Rust is [AccessKit](https://accesskit.dev/), and we evaluated adding it to Pyzed for 1.1: - **Iced 0.14 has no AccessKit support.** Neither `iced` nor `iced_winit` depend on it, and winit 0.30 leaves accessibility to adapters such as `accesskit_winit`. - **The window is owned by Iced.** `accesskit_winit` must be created before the window is shown and receive every window event; Iced's event loop gives no place to do either. The platform adapters (`accesskit_windows` and `accesskit_macos` subclass the native window, `accesskit_unix` talks to AT-SPI) could be attached from the raw window handle, but that is three separate integrations, each tested with a real screen reader. - **Widgets don't describe themselves.** Iced's widget operations report text, text fields, scrollables and focus, but no roles (button, checkbox, slider), states or actions ("press"), so Pyzed would have to build the tree from its own widget descriptions and keep it in sync with the layout every frame. That makes it a project of its own, not something to ship half done in a minor release. Accessibility is on Iced's roadmap and forks of Iced already ship AccessKit, so the plan is: 1. Follow Iced upstream and adopt its accessibility support when it lands; Pyzed's widgets map one to one to Iced's, so they would gain it with little extra work. 2. Meanwhile, make the keyboard path complete (focus for buttons and other controls when Iced allows it) and keep labels in the UI text rather than only in icons. If screen reader support matters for your app, please say so in the [issues](https://github.com/albakore/pyzed/issues): it helps decide priorities. ### Large data and performance > Python GUI performance with Pyzed: show hundreds of thousands of rows with virtual lists and tables, and skip unchanged views with @pz.memo. Source: https://albakore.github.io/pyzed/guides/performance/ Pyzed calls `view()` after every message and turns the widget tree it returns into Iced widgets. For most apps that takes well under a millisecond. Two things make it slow: building **many rows** that are not even on screen, and rebuilding **big subtrees** that did not change. Pyzed has a tool for each. #### Fast by default You don't have to turn any of this on. Pyzed does it in every app, following what fast native editors do to keep their UI fast: start drawing as soon as possible, and don't redo work nobody asked for. - **Fast start.** `import pyzed` loads only what an app needs to open a window. The designer, the API reference, asyncio and the package metadata load the first time you use them. - **Fonts in parallel.** Scanning the system fonts starts the moment Pyzed is imported, on its own thread, while Python runs the rest of your imports. - **Native GPU APIs first.** The GPU renderer starts Vulkan, Metal or DirectX 12. The OpenGL backend, slower to start and heavier on memory, is started only when none of those works. - **Cheap widget trees.** Building containers, buttons and theme colors costs less, and the native core allocates with [mimalloc](https://github.com/microsoft/mimalloc), which is much faster than the system allocator with the many small objects every `view()` creates. - **Shared pieces of components.** `ui.button`, `ui.icon_button`, `ui.kbd` and tooltips reuse the widgets that only depend on the theme and on their text, icon or keys. Widgets never change once built, so one copy serves every button that says "Open". A view of 1,000 buttons and labels builds in about a third less time. - **The CPU renderer when there is no real GPU.** On a virtual machine, a remote desktop or a computer without a graphics driver, the GPU that Vulkan, DirectX or OpenGL offer is an emulation running on the CPU (Mesa's llvmpipe and lavapipe, Windows' WARP). There Pyzed draws with its CPU renderer (tiny-skia) instead: it is faster and takes about 100 MB less memory. - **No rebuilds for keys nobody listens to.** Key presses and releases only reach your app (and rebuild the view) when a window defines `on_key` or `on_key_up`. Typing in a field rebuilds the view once per key instead of three times. - **Hovering costs nothing.** Buttons, list rows and anything styled with `hover=` light up under the pointer on their own, without a message or a new `view()`. Pointer callbacks (`on_move`, `on_enter`, `on_exit`, `mouse_move`...) run as soon as the pointer moves, and when one returns [`pz.UNCHANGED`](#callbacks-that-change-nothing-on-screen) nothing is rebuilt nor redrawn. The library's own components and Pyzed Design do that whenever a move changes nothing. - **Styles resolved once.** `widget.style(...)` caches what it resolves by its properties and the theme, so the same hover color on every row of a list is worked out once, not once per row per `view()`. - **Markdown parsed once.** A long chat or document in `pz.markdown` blocks is parsed the first time it is drawn and kept while it is on screen, however many blocks there are. - **Long lists build what is in view.** `ui.table`, `ui.virtual_list` and Pyzed Design's List box (from 50 items) only build the rows you can see. - **Components are drawn when they change.** A [`pz.Component`](#components-drawn-only-when-they-change) whose props, state and context are the same as last time gives back its last drawing instead of running `render()` again. A view of 500 rows of components rebuilds in 4 ms instead of 26 ms when one of them changes. - **So are form controls.** A form made in Pyzed Design, running or on the designer's canvas, only builds again the controls whose properties changed. The designer does the same with its console, outline and toolbox: its view went from 21 ms to 4.5 ms with an 85-control form and 400 log lines. - **Files are listed off the UI thread.** The Open dialog, the agent's `@` list and the shader file picker read the project folder once and then refresh it on a worker thread, so typing in them never waits for the disk. Measured with the [benchmark app](https://albakore.github.io/pyzed/why/benchmarks/) (640×480 window, a label and a button per row, Xvfb without a GPU, medians of 5 launches) on the same machine: | | Before | Now | | --- | --- | --- | | `import pyzed` | 178 ms | 78 ms | | First frame, 10 rows | 454 ms | 323 ms | | Update of 1,000 rows | 40.7 ms | 34.9 ms | | Memory, 10 rows (RSS) | 174 MB | 146 MB | :::note `WGPU_BACKEND=gl` (or `vulkan`, `metal`, `dx12`) still picks a GPU backend yourself, `ICED_BACKEND=wgpu` keeps the GPU renderer even on an emulated GPU (what [shaders](https://albakore.github.io/pyzed/shading/shaders/) need), and `ICED_BACKEND=tiny-skia` always uses the CPU renderer. A project can set them for good in `[tool.pyzed.rendering]` of its `pyproject.toml`, or in **Settings → Project → Rendering** ([Rendering](https://albakore.github.io/pyzed/editor/settings/#rendering)). ::: #### Virtual lists and tables A virtual list builds only the rows in view. Rows are built when they scroll into view and dropped when they leave it, so a list of a million items costs the same as one of fifty. ```python import pyzed as pz from pyzed import ui class Logs(pz.App): def __init__(self): self.lines = [f"[{i:06}] request handled" for i in range(500_000)] def view(self): return ui.virtual_list( self.lines, lambda line: pz.text(line, font="mono", size=12), item_height=20, id="logs", ) ``` - [`ui.virtual_list(items, render)`](https://albakore.github.io/pyzed/library/virtual-list/) is a themed, scrolling list: `render(item)` returns the widget for one item. - [`ui.table`](https://albakore.github.io/pyzed/library/table/) is virtualized too: pass it all your rows. - [`pz.virtual_column(count, item, item_height=...)`](https://albakore.github.io/pyzed/reference/widgets/#virtual_column) is the building block: `item(index)` returns row `index`. Put it inside any `pz.scrollable`. Every row has the same height (`item_height`), which is what lets Pyzed know where each row goes without building it. To scroll to row `i`, scroll the list's `id` to `i * (item_height + spacing)`: ```python pz.Command.scroll_to("logs", 0, 1_000 * 20) # row 1000 ``` Measured on a 200 000-row table, building the view went from 3.7 s to 0.2 ms. :::tip `items` and `rows` should be lists (or other sequences): other iterables are copied into a list on every `view()`. Sorting a huge table on every `view()` is the remaining cost; keep the rows sorted in your state, or sort them in a `@pz.memo` function. ::: #### Callbacks that change nothing on screen A message makes Pyzed call `view()` again and lay out and draw the window. That is what you want after a click, but a pointer that moves over a big view sends a message per move. When a callback only remembers something that is not drawn, return `pz.UNCHANGED`: the message ends there, with no `view()`, no layout and no redraw. ```python class Canvas(pz.App): def __init__(self): self.pointer = (0, 0) # used when the menu opens, not drawn self.hovered = None # drawn highlighted def moved(self, x, y): self.pointer = (x, y) hovered = self.item_at(x, y) if hovered == self.hovered: return pz.UNCHANGED # nothing on screen depends on the move self.hovered = hovered # this one is drawn: the view is rebuilt def view(self): return pz.mouse_area(self.board(), on_move=self.moved) ``` It works for pointer callbacks (`on_move`, `on_enter`, `on_exit` of `pz.mouse_area` and the `mouse_move`, `mouse_enter`, `mouse_leave`... events of `.on(...)`), messages posted with `pz.post`, and anything your `update()` returns. Return it only when nothing that `view()`, `title` or `subscriptions()` read has changed: the window keeps showing what it showed. :::tip For a highlight under the pointer you don't need a callback at all: `.style(hover={"background": "ghost_hover"})` draws it in the native widget. ::: #### Components drawn only when they change Every [component](https://albakore.github.io/pyzed/guides/components/) keeps its last drawing. On the next `view()` it is reused, without calling `render()`, while all of these are the same: - its **props** and its **state** (any attribute you set on `self`). Lists, dicts, sets and plain objects are compared by their contents, so `self.items.append(x)` is seen; - the values it reads with `pz.use_context` (or `self.context`); - the theme, the language, the window size and the stylesheet; - the components drawn inside it: when a child changes its state, every component that contains it is drawn again too. Callbacks you pass as props or create in `render()` don't get in the way: they are compared by their code and the values they captured, so a new `lambda: self.pick(i)` with the same `i` counts as the same callback. ```python class Row(pz.Component): name: str = "" stars: int = 0 def render(self): # runs only when name, stars or the Rating inside change return pz.row([ui.label(self.name), Rating(value=self.stars)]) ``` Two cases need a hand, because the component can't see what changed: - `render()` reads something that is neither a prop, nor state, nor context: the clock, a global, data changed by another object. Call `self.refresh()` when it changes, or put `cache = False` on the class to draw it on every `view()` (live meters do that). - A value whose changes can't be compared (a NumPy array, a native handle, a huge object graph) turns the cache off for that component on its own: it is drawn every time, as before. A component that gets widgets as `children` is drawn whenever the parent builds them again, since they are new widgets each time; it costs no more than drawing it. #### Threads Your `view()`, `update()` and callbacks run on the UI thread, one at a time, as in every native toolkit. Python runs one thread at a time anyway (the GIL), so the fastest UI is the one that does less on that thread, which is what the caches above are for. What Pyzed does on other threads: - Iced lays out and renders the frame in Rust, outside the GIL, and the GPU draws it. - System fonts are scanned, project files are listed and the designer's agents, language servers and builds run on worker threads. - Your slow work belongs there too: [async tasks](https://albakore.github.io/pyzed/guides/async/) or a `threading.Thread` that sends its result with `pz.post(...)`. #### Memoized subtrees `@pz.memo` caches what a function returns by its arguments. While the arguments are equal, the next `view()` reuses the same widget tree instead of building it again: ```python @pz.memo def sidebar(files: tuple, selected: str): return pz.column([ui.list_item(f, icon="file", selected=f == selected) for f in files]) class Editor(pz.App): def view(self): return pz.row([sidebar(tuple(self.files), self.selected), self.editor()]) ``` How arguments are compared: | Argument | Compared by | | --- | --- | | Numbers, strings, tuples, frozen dataclasses... | Value (their hash). | | Lists, dicts and sets | Their contents. | | Anything else (your objects, `self`) | Identity: mutating it in place does not refresh the cache. | The current theme and language are always part of the key, so switching them rebuilds every memoized subtree. :::caution A memoized function only sees its arguments. If it reads `self.something` or a global, the cached subtree keeps the old value until the arguments change. Pass everything it depends on, or a version number you bump when the data changes. ::: - `@pz.memo(maxsize=256)` keeps more results (64 by default). It is handy on a function called once per row, such as the `render` of a virtual list. - `function.cache_clear()` empties one function's cache, `pz.clear_memos()` all of them. - `pz.memo_stats()` returns `{"hits": ..., "misses": ...}` to check that a subtree is reused. #### Other tips - Keep `view()` free of slow work: load files, query databases and sort data in handlers or [async tasks](https://albakore.github.io/pyzed/guides/async/), and keep the results in your state. - Draw thousands of shapes with one [canvas](https://albakore.github.io/pyzed/guides/canvas/) instead of thousands of widgets. - `pz.every` subscriptions call `view()` on every tick: stop them when nothing moves. ### Stability and known limitations > What Pyzed 1.x promises about its Python API, how features are deprecated over releases, and what the framework does not do yet. Source: https://albakore.github.io/pyzed/guides/stability/ #### Versioning From 1.0.0 on, Pyzed follows [semantic versioning](https://semver.org/): - **Patch** releases (`1.0.x`) only fix bugs. - **Minor** releases (`1.x.0`) add features and never break code that uses the public API. - **Major** releases (`2.0.0`) are the only ones that can remove or change public API. The public API is everything documented in the [API reference](https://albakore.github.io/pyzed/reference/app/) and in these guides. Names that start with an underscore, and anything under `pyzed.designer`, are internal and may change in any release. `pyzed.agents`, `pyzed.mcp` and the animation API are **provisional** in 1.x: they follow the same rules, but a minor release may still adjust details that turn out wrong, and the change is always listed under *Breaking Changes* in the [release notes](https://albakore.github.io/pyzed/changelog/). #### Deprecations Before public API is removed, it keeps working for at least one minor release and warns with a `DeprecationWarning` that names its replacement. The removal happens in the next major release. Pyzed Design files (`.pzform` and components) carry a format version. Pyzed opens files of its own version and older ones, and refuses a file saved by a newer Pyzed instead of silently dropping what it does not know. #### Known limitations | Area | Limitation | | --- | --- | | Accessibility | No screen reader support yet, and focus moves between fields only from code (`Command.focus`), not with Tab. | | Effects | `glass`, `bloom` and `glow` are drawn with gradients and shadows: nothing behind a panel is blurred. | | Animations | 11 properties can be animated (opacity, position, scale, colors, radius, border width and glow); size, rotation and padding cannot. | | Large data | Virtual lists and tables need rows of the same height. Every update still calls `view()`; [`@pz.memo`](https://albakore.github.io/pyzed/guides/performance/) skips the subtrees that did not change. | | Platforms | Linux wheels need glibc 2.28 or newer, or musl (Alpine), where the audio engine has no sound devices. Pyzed Design builds are not signed yet. | | Python | Free-threaded Python (3.13t, 3.14t) is not supported. Pyzed needs Python 3.10 or newer. |